Add `title` element (#5618)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Johann Birnick 2025-08-06 10:13:41 +02:00 committed by GitHub
parent 6de2d9586a
commit d0026093d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 164 additions and 2 deletions

View File

@ -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<LinkElem> = |elem, engine, _| {
.pack())
};
const TITLE_RULE: ShowFn<TitleElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::h1)
.with_body(Some(elem.resolve_body(styles).at(elem.span())?))
.pack())
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
let span = elem.span();

View File

@ -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<LinkElem> = |elem, engine, _| {
Ok(body.linked(dest))
};
const TITLE_RULE: ShowFn<TitleElem> = |elem, _, styles| {
Ok(BlockElem::new()
.with_body(Some(BlockBody::Content(elem.resolve_body(styles).at(elem.span())?)))
.pack())
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
const SPACING_TO_NUMBERING: Em = Em::new(0.3);

View File

@ -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::<EnumElem>();
global.define_elem::<TermsElem>();
global.define_elem::<LinkElem>();
global.define_elem::<TitleElem>();
global.define_elem::<HeadingElem>();
global.define_elem::<FigureElem>();
global.define_elem::<QuoteElem>();

View File

@ -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<Content>,
}
impl TitleElem {
pub fn resolve_body(&self, styles: StyleChain) -> HintedStrResult<Content> {
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<TitleElem> {
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
}
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>A cool title</h1>
<h2>Some level one heading</h2>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Some Title</h1>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My title</title>
</head>
<body>
<h1>My display title</h1>
</body>
</html>

12
tests/ref/html/title.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My title</title>
</head>
<body>
<h1>My title</h1>
<h2>A level one heading</h2>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
tests/ref/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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()