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 00000000..4f7431b9 Binary files /dev/null and b/tests/ref/title-show-set.png differ diff --git a/tests/ref/title-with-body-auto.png b/tests/ref/title-with-body-auto.png new file mode 100644 index 00000000..3c861186 Binary files /dev/null and b/tests/ref/title-with-body-auto.png differ diff --git a/tests/ref/title-with-body.png b/tests/ref/title-with-body.png new file mode 100644 index 00000000..d71df8b1 Binary files /dev/null and b/tests/ref/title-with-body.png differ diff --git a/tests/ref/title.png b/tests/ref/title.png new file mode 100644 index 00000000..9495e96b Binary files /dev/null and b/tests/ref/title.png differ diff --git a/tests/suite/model/title.typ b/tests/suite/model/title.typ new file mode 100644 index 00000000..6a7ff131 --- /dev/null +++ b/tests/suite/model/title.typ @@ -0,0 +1,24 @@ +// Test title element. + +--- title render html --- +#set document(title: "My title") +#title() += A level one heading + +--- title-with-body render html --- +#set document(title: "My title") +#title[My display title] + +--- title-with-body-auto render --- +#set document(title: "My title") +#title(auto) + +--- title-show-set --- +#show title: set text(blue) +#title[A blue title] + +--- title-unset --- +// Error: 2-9 document title was not set +// Hint: 2-9 set the title with `set document(title: [...])` +// Hint: 2-9 or provide an explicit body with `title[..]` +#title()