From d0026093d4410b45a82f9217b548e7969a262f24 Mon Sep 17 00:00:00 2001 From: Johann Birnick <6528009+jbirnick@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:13:41 +0200 Subject: [PATCH] Add `title` element (#5618) Co-authored-by: Laurenz --- crates/typst-html/src/rules.rs | 9 ++- crates/typst-layout/src/rules.rs | 9 ++- crates/typst-library/src/model/mod.rs | 3 + crates/typst-library/src/model/title.rs | 77 ++++++++++++++++++++++++ tests/ref/html/title-and-heading.html | 11 ++++ tests/ref/html/title-basic.html | 10 +++ tests/ref/html/title-with-body.html | 11 ++++ tests/ref/html/title.html | 12 ++++ tests/ref/title-show-set.png | Bin 0 -> 1231 bytes tests/ref/title-with-body-auto.png | Bin 0 -> 615 bytes tests/ref/title-with-body.png | Bin 0 -> 1068 bytes tests/ref/title.png | Bin 0 -> 1455 bytes tests/suite/model/title.typ | 24 ++++++++ 13 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 crates/typst-library/src/model/title.rs create mode 100644 tests/ref/html/title-and-heading.html create mode 100644 tests/ref/html/title-basic.html create mode 100644 tests/ref/html/title-with-body.html create mode 100644 tests/ref/html/title.html create mode 100644 tests/ref/title-show-set.png create mode 100644 tests/ref/title-with-body-auto.png create mode 100644 tests/ref/title-with-body.png create mode 100644 tests/ref/title.png create mode 100644 tests/suite/model/title.typ diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs index 0f73ef8c..472e155b 100644 --- a/crates/typst-html/src/rules.rs +++ b/crates/typst-html/src/rules.rs @@ -11,7 +11,7 @@ use typst_library::layout::{OuterVAlignment, Sizing}; use typst_library::model::{ Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem, - RefElem, StrongElem, TableCell, TableElem, TermsElem, + RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem, }; use typst_library::text::{ HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem, @@ -32,6 +32,7 @@ pub fn register(rules: &mut NativeRuleMap) { rules.register(Html, ENUM_RULE); rules.register(Html, TERMS_RULE); rules.register(Html, LINK_RULE); + rules.register(Html, TITLE_RULE); rules.register(Html, HEADING_RULE); rules.register(Html, FIGURE_RULE); rules.register(Html, FIGURE_CAPTION_RULE); @@ -161,6 +162,12 @@ const LINK_RULE: ShowFn = |elem, engine, _| { .pack()) }; +const TITLE_RULE: ShowFn = |elem, _, styles| { + Ok(HtmlElem::new(tag::h1) + .with_body(Some(elem.resolve_body(styles).at(elem.span())?)) + .pack()) +}; + const HEADING_RULE: ShowFn = |elem, engine, styles| { let span = elem.span(); diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs index a3616f96..089b4e67 100644 --- a/crates/typst-layout/src/rules.rs +++ b/crates/typst-layout/src/rules.rs @@ -21,7 +21,7 @@ use typst_library::model::{ Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem, LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem, - QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, + QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem, Works, }; use typst_library::pdf::EmbedElem; use typst_library::text::{ @@ -47,6 +47,7 @@ pub fn register(rules: &mut NativeRuleMap) { rules.register(Paged, ENUM_RULE); rules.register(Paged, TERMS_RULE); rules.register(Paged, LINK_RULE); + rules.register(Paged, TITLE_RULE); rules.register(Paged, HEADING_RULE); rules.register(Paged, FIGURE_RULE); rules.register(Paged, FIGURE_CAPTION_RULE); @@ -216,6 +217,12 @@ const LINK_RULE: ShowFn = |elem, engine, _| { Ok(body.linked(dest)) }; +const TITLE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::new() + .with_body(Some(BlockBody::Content(elem.resolve_body(styles).at(elem.span())?))) + .pack()) +}; + const HEADING_RULE: ShowFn = |elem, engine, styles| { const SPACING_TO_NUMBERING: Em = Em::new(0.3); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index a0f7e11a..015fdd27 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -20,6 +20,7 @@ mod reference; mod strong; mod table; mod terms; +mod title; pub use self::bibliography::*; pub use self::cite::*; @@ -39,6 +40,7 @@ pub use self::reference::*; pub use self::strong::*; pub use self::table::*; pub use self::terms::*; +pub use self::title::*; use crate::foundations::Scope; @@ -54,6 +56,7 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); + global.define_elem::(); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst-library/src/model/title.rs b/crates/typst-library/src/model/title.rs new file mode 100644 index 00000000..d1c8b48f --- /dev/null +++ b/crates/typst-library/src/model/title.rs @@ -0,0 +1,77 @@ +use crate::diag::{Hint, HintedStrResult}; +use crate::foundations::{Content, Packed, ShowSet, Smart, StyleChain, Styles, elem}; +use crate::introspection::Locatable; +use crate::layout::{BlockElem, Em}; +use crate::model::DocumentElem; +use crate::text::{FontWeight, TextElem, TextSize}; + +/// A document title. +/// +/// This should be used to display the main title of the whole document and +/// should occur only once per document. In contrast, level 1 +/// [headings]($heading) are intended to be used for the top-level sections of +/// the document. +/// +/// Note that additional frontmatter (like an author list) that should appear +/// together with the title does not belong in its body. +/// +/// In HTML export, this shows as a `h1` element while level 1 headings show +/// as `h2` elements. +/// +/// # Example +/// ```example +/// #set document( +/// title: [Interstellar Mail Delivery] +/// ) +/// +/// #title() +/// +/// = Introduction +/// In recent years, ... +/// ``` +#[elem(Locatable, ShowSet)] +pub struct TitleElem { + /// The content of the title. + /// + /// When omitted (or `{auto}`), this will default to [`document.title`]. In + /// this case, a document title must have been previously set with + /// `{set document(title: [..])}`. + /// + /// ```example + /// #set document(title: "Course ABC, Homework 1") + /// #title[Homework 1] + /// + /// ... + /// ``` + #[positional] + pub body: Smart, +} + +impl TitleElem { + pub fn resolve_body(&self, styles: StyleChain) -> HintedStrResult { + match self.body.get_cloned(styles) { + Smart::Auto => styles + .get_cloned(DocumentElem::title) + .ok_or("document title was not set") + .hint("set the title with `set document(title: [...])`") + .hint("or provide an explicit body with `title[..]`"), + Smart::Custom(body) => Ok(body), + } + } +} + +impl ShowSet for Packed { + fn show_set(&self, _styles: StyleChain) -> Styles { + const SIZE: Em = Em::new(1.7); + const ABOVE: Em = Em::new(1.125); + const BELOW: Em = Em::new(0.75); + + let mut out = Styles::new(); + out.set(TextElem::size, TextSize(SIZE.into())); + out.set(TextElem::weight, FontWeight::BOLD); + out.set(BlockElem::above, Smart::Custom(ABOVE.into())); + out.set(BlockElem::below, Smart::Custom(BELOW.into())); + out.set(BlockElem::sticky, true); + out + } +} diff --git a/tests/ref/html/title-and-heading.html b/tests/ref/html/title-and-heading.html new file mode 100644 index 00000000..c09062ec --- /dev/null +++ b/tests/ref/html/title-and-heading.html @@ -0,0 +1,11 @@ + + + + + + + +

A cool title

+

Some level one heading

+ + diff --git a/tests/ref/html/title-basic.html b/tests/ref/html/title-basic.html new file mode 100644 index 00000000..c9995358 --- /dev/null +++ b/tests/ref/html/title-basic.html @@ -0,0 +1,10 @@ + + + + + + + +

Some Title

+ + diff --git a/tests/ref/html/title-with-body.html b/tests/ref/html/title-with-body.html new file mode 100644 index 00000000..7b58813b --- /dev/null +++ b/tests/ref/html/title-with-body.html @@ -0,0 +1,11 @@ + + + + + + My title + + +

My display title

+ + diff --git a/tests/ref/html/title.html b/tests/ref/html/title.html new file mode 100644 index 00000000..b1a59cdf --- /dev/null +++ b/tests/ref/html/title.html @@ -0,0 +1,12 @@ + + + + + + My title + + +

My title

+

A level one heading

+ + diff --git a/tests/ref/title-show-set.png b/tests/ref/title-show-set.png new file mode 100644 index 0000000000000000000000000000000000000000..4f7431b983c85f183a7201470c60d4f93f48f1de GIT binary patch literal 1231 zcmV;=1Tg!FP)0+tpGLPW)`u-Ds+w%DR z1$Nok>iNgu_q*Elv(@xClHqW%>WRMXBZuBFj^O9={K?_>PoL#gqUDUh?v26jSEJ@% zr{@xV+wArH>GS-2x9j2W``+#P4}9ASdD^GX@~hGFdA94X)AO;^^Rm_SHIU&-oaBbR z?Y`UfKbGQ}$nZm$;~j(DSfu90;P(-I+nmYp+w1zu;`ih5`*^kMb+hW?@B2QL;+Dqm zi@@%PzU_Lp>wmfINSouC$M773-IBxaMw;VPq2->+@j8>?3wqk-@%%)Y<7}_#j=}C= zsOMRv=GW=@L6_pw=lRX#_|N6|oyqX=_x*OX>Z8r^WUAOGa>9)#Vr*7QuB zBOk9>W_Q@>3~gZAyZv44?8VOf=e+yQ z9B4Ee&A;OZnY*SjD+7|l$Mz-SFLcq6l~r86ggKjX3_uYqxD}F=qg;>KdEnJ*9F^ZA zpV5K2a=oJ74TXV$m^M!R?Aew{KL9OX+JW-YMgULwbM6NouyYIwe5P{mWX(A40V+(HHXUKC5E!X z)D7^>&=t6cX`Ttc#!ia2@~CYLgeQc-NJ{pH`y-nn8D$8(45Axo^>xeq zhI8}s^kSR(AG?e!mcx^b1)K~+CJlXkwqYwIO*FylyO;vrvqA~KV{c!BGX_4nbsIpx z5NWC~R&f0HaJFg>@Q?yZcz`dIa6Om8R~|<#0GC0mR)r%{*XiW&UM`Ztp~&H+8;HX_mlPgkHh?kq01Yez@B_X$*ak_0=#sxTcuV0T2-rx|H((F%+zl}DY}6W?*tNUl z#BoT{$bKu(Ax3~9X06S~MH+xQGGV0V4SW!Pz&;2dC)epr427g^u&c8^2#9ky?N!wX zOE$HRIQ-8QV7yltUslU=T4vNgdE9At_H-jXM)Ve(zu>fQk48xcZJ*tkmrroDL&f6< tOkmoCYKSROs{O=_l`ie((`YpR!!HGOk;Oi1FyjCK002ovPDHLkV1gQ;#z6o8 literal 0 HcmV?d00001 diff --git a/tests/ref/title-with-body-auto.png b/tests/ref/title-with-body-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..3c861186a61c68dde05a6d6f38d92267d4327f3c GIT binary patch literal 615 zcmV-t0+{`YP)0006mNkl@Af(VUJ!mNC;$iG06eW=yTge) z$2k9PU=(*(3sOsc13U6&$UXqBi)`v-YEGahbcN^cSg-lMfnl|Teg{ZU?PW?`yOM#C zLgO!BrOJ5t2@(hS0<@mI8q(~2ebhD zEU5&BjlnxkLz5l{GSmW4rTP$eR$1_=lNOO)Sf(d!b~1E7!RA18%&|eZCE2_ruXNW!e*af=un6*F89SqO!8MbR`B)%Z}w5V zIs1ISPW>@M{+Pv<#HeEY0l$tda0EV(033k-zd!mFm2EGRjrITl002ovPDHLkV1l!B B8@m7i literal 0 HcmV?d00001 diff --git a/tests/ref/title-with-body.png b/tests/ref/title-with-body.png new file mode 100644 index 0000000000000000000000000000000000000000..d71df8b1a7968705420f317878af0fde78b08db5 GIT binary patch literal 1068 zcmV+{1k?M8P)f!554_lX@>m_i>iJNl5H{#Z zhGlw3%>u5i;L`N3Pa!7*k-l zJ_xKSRxRaXYIu~w9@*8RhX%t*_(=z(~8?g({-pV2MtPO(c`W_IX+c>oe06NA=n+;wmdmu&( z2h7kc(nQZ#dD{wOqAfxfW7)KsPuF7~oF0wQpaDl=uI>ZQsn_+Oa=@|{X&DOl#=7gG zKdmrR^j#Z#Ipn$Rx)At648JWcvlD(j3D)U^P6*N%fY}-vg0=6(a@DGZR`~Z!a(yI| zX4NLlo?2{$3q05Jfi3~?m(XDBgjdAFSGp5esha?fsGh-^dA_b{PE$?pCrDnQ+pCr> zT4I6gIjCXqt;jVJG~D!}&L}s-F_F`D!dtw&Ga#@A61@#Ru|TKqLkv_iPv?E?>-$v6 z4uj#o!z`S*>46k1@PcJicXyLJ*I;<`N|~=NIWgWnv>m#p%|+i^mq}&`)1BdeXa?_1-oF3CTtyz3`rb4xY6K;t%}(aIT~=?>R{mJ!<&Kk z{j-3z5;Yp|5e*(P>7E5%(9wW@T&sZ@r@D$yBzBum-Xl>N)i${A%Y^JdoCu$NMgvl^ zs)9t?O({OIBc;I#_l7)?J9T=E^I(#76_77tg2{3LtZ=M+M1({-5#FnzK$%E4+$(tl z@Jv~@I=~~siEx5GI|xsd^ES9nq!Pf1@E(175S}18K$z~N(_M}O9ZrM~=(PGq@i@Zh6azaIK&CW?!D?;l8YjedFCjj9Wd%8eXGqorn4+<3t!*7_6&< m!FqpjeTy#G1-sz?5d0UiMflZ*f?cct0000+1@|i=e>g# z>JC5bhyC#X3bwo4)aP>0c;!{p_gzx>Oy9t+#8xr}fF&XWEZm$MI3L)(;kw{yo#Y!B zdPv|!fM--oxv8Sz0|8rT0`PP1l|Y&`m!&ZfBUj4ddj_S-&LAZ5BMPO}if;qyXl$3u^xfV0-8Z8afxE53kwtd{E%P zT+AH;BCUx*ZQ6dwm9a+coaT!mQOkA3o55QJ-t1L)bC>ykI$671X^FA8KeXd?S_0nJ zJIUq$1M$Ot*bn<*KTONRHV1gh`sB?D8td$w2}^ws|ImIFc(OhrWi*y+jwJgWcJ!2J z>Kh_f8f~3?>T~$F?>ZUFZ-6UG;8IzcmC`aBTrR!=E-L__2=Z$H9QjR!hhCeki?4%S zqvHp?Y+Zhr%ymZf%5lFpI=o-07e4disPPZ4VM}L8k1%w$!h!+sciGz{_~1BpYya!u zlz^(uGK;CQgh_uhPcQA3Xl^raVg(S&1Q}ooPz}CJ%vd;Fx&Vc*yG}K`F z9opVm3pBu0L7m`h{n`r;l>5Bih3az;lvMn53TFyaLv0w_1pl`>dy3>5WKV{BE< zvl^TQa5c8`hmWsfU9d)$_R+_-ImQ?ap_V4VwYUKn)Z$rr&Xv^w*zk142DrhECzsW; z^gQWyLrsN$Ncdqt+_Lb|pFhs0(eg7={sGsmf{Q|B4~}73I@yk z>NjOS@on8{a=@3bI$eImq+?AXV{q3Lj?xvpDac(1j~EIWM-hv`Xx zyq9Jq*TH=>k5s(@Uc0I5nnlc179e#I2Tp7M%H(!jifzMYg&vmlwv*GWC=% zZ~M18&yWeuj$3a#Y;E5+d*>Hv+;*6MGrrw?(Lay