diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs index 5916d3147..ba0b8942f 100644 --- a/crates/typst-html/src/css.rs +++ b/crates/typst-html/src/css.rs @@ -26,7 +26,6 @@ impl Properties { } /// Adds a new property in builder-style. - #[expect(unused)] pub fn with(mut self, property: &str, value: impl Display) -> Self { self.push(property, value); self diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs index ee1034f62..54d79d024 100644 --- a/crates/typst-html/src/rules.rs +++ b/crates/typst-html/src/rules.rs @@ -1,17 +1,18 @@ use std::num::NonZeroUsize; +use comemo::Track; use ecow::{eco_format, EcoVec}; use typst_library::diag::{warning, At}; use typst_library::foundations::{ - Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, + Content, Context, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, }; use typst_library::introspection::{Counter, Locator}; use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; 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, + FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, OutlineElem, OutlineEntry, + ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, }; use typst_library::text::{ HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, @@ -36,6 +37,8 @@ pub fn register(rules: &mut NativeRuleMap) { rules.register(Html, FIGURE_RULE); rules.register(Html, FIGURE_CAPTION_RULE); rules.register(Html, QUOTE_RULE); + rules.register(Html, OUTLINE_RULE); + rules.register(Html, OUTLINE_ENTRY_RULE); rules.register(Html, REF_RULE); rules.register(Html, CITE_GROUP_RULE); rules.register(Html, TABLE_RULE); @@ -274,6 +277,56 @@ const QUOTE_RULE: ShowFn = |elem, _, styles| { Ok(realized) }; +const OUTLINE_RULE: ShowFn = |elem, engine, styles| { + let (title, entries) = elem.realize(engine, styles)?; + let list = HtmlElem::new(tag::ol) + .with_attr(attr::class, "outline") + .with_styles(css::Properties::new().with("list-style", "none")) + .with_body(Some(Content::sequence(entries))) + .pack() + .spanned(elem.span()); + Ok(title.unwrap_or_default() + list) +}; + +const OUTLINE_ENTRY_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); + + let prefix = elem.prefix(engine, context, span)?; + let body = elem.body().at(elem.span())?; + + let inner = if let Some(prefix) = prefix { + let separator = if let Some(elem) = elem.element.to_packed::() { + match elem.caption.get_ref(styles) { + Some(caption) => caption.get_separator(styles), + None => FigureCaption::local_separator_in(styles), + } + } else { + SpaceElem::shared().clone() + }; + Content::sequence([ + HtmlElem::new(tag::span) + .with_attr(attr::class, "prefix") + .with_body(Some(prefix)) + .pack() + .spanned(elem.span()), + separator, + body, + ]) + } else { + body + }; + + let loc = elem.element_location().at(span)?; + let dest = Destination::Location(loc); + + Ok(HtmlElem::new(tag::li) + .with_body(Some(LinkElem::new(dest.into(), inner).pack())) + .pack() + .spanned(elem.span())) +}; + const REF_RULE: ShowFn = |elem, engine, styles| elem.realize(engine, styles); const CITE_GROUP_RULE: ShowFn = |elem, engine, _| elem.realize(engine); diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs index c78f94d55..72fcf7acb 100644 --- a/crates/typst-layout/src/rules.rs +++ b/crates/typst-layout/src/rules.rs @@ -20,8 +20,8 @@ use typst_library::math::EquationElem; 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, + LinkElem, ListElem, OutlineElem, OutlineEntry, ParElem, ParbreakElem, QuoteElem, + RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, }; use typst_library::pdf::EmbedElem; use typst_library::text::{ @@ -412,38 +412,8 @@ const FOOTNOTE_ENTRY_RULE: ShowFn = |elem, engine, styles| { }; const OUTLINE_RULE: ShowFn = |elem, engine, styles| { - let span = elem.span(); - - // Build the outline title. - let mut seq = vec![]; - if let Some(title) = elem.title.get_cloned(styles).unwrap_or_else(|| { - Some(TextElem::packed(Packed::::local_name_in(styles)).spanned(span)) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(span), - ); - } - - let elems = engine.introspector.query(&elem.target.get_ref(styles).0); - let depth = elem.depth.get(styles).unwrap_or(NonZeroUsize::MAX); - - // Build the outline entries. - for elem in elems { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let level = outlinable.level(); - if outlinable.outlined() && level <= depth { - let entry = OutlineEntry::new(level, elem); - seq.push(entry.pack().spanned(span)); - } - } - - Ok(Content::sequence(seq)) + let (title, entries) = elem.realize(engine, styles)?; + Ok(Content::sequence(title.into_iter().chain(entries))) }; const OUTLINE_ENTRY_RULE: ShowFn = |elem, engine, styles| { @@ -454,6 +424,7 @@ const OUTLINE_ENTRY_RULE: ShowFn = |elem, engine, styles| { let prefix = elem.prefix(engine, context, span)?; let inner = elem.inner(engine, context, span)?; let block = if elem.element.is::() { + // Equation has no body and no levels, so indenting makes no sense. let body = prefix.unwrap_or_default() + inner; BlockElem::new() .with_body(Some(BlockBody::Content(body))) diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index ac3676eea..0fa1ad818 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -19,7 +19,7 @@ use crate::layout::{ VAlignment, }; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; -use crate::text::{Lang, Region, TextElem}; +use crate::text::{Lang, TextElem}; use crate::visualize::ImageElem; /// A figure with an optional caption. @@ -526,23 +526,23 @@ impl FigureCaption { Ok(realized) } - /// Gets the default separator in the given language and (optionally) - /// region. - fn local_separator(lang: Lang, _: Option) -> &'static str { - match lang { - Lang::CHINESE => "\u{2003}", - Lang::FRENCH => ".\u{a0}– ", - Lang::RUSSIAN => ". ", - Lang::ENGLISH | _ => ": ", - } + /// Retrieves the locale separator. + pub fn get_separator(&self, styles: StyleChain) -> Content { + self.separator + .get_cloned(styles) + .unwrap_or_else(|| Self::local_separator_in(styles)) } - fn get_separator(&self, styles: StyleChain) -> Content { - self.separator.get_cloned(styles).unwrap_or_else(|| { - TextElem::packed(Self::local_separator( - styles.get(TextElem::lang), - styles.get(TextElem::region), - )) + /// Gets the default separator in the given language and (optionally) + /// region. + pub fn local_separator_in(styles: StyleChain) -> Content { + styles.get_cloned(Self::separator).unwrap_or_else(|| { + TextElem::packed(match styles.get(TextElem::lang) { + Lang::CHINESE => "\u{2003}", + Lang::FRENCH => ".\u{a0}– ", + Lang::RUSSIAN => ". ", + Lang::ENGLISH | _ => ": ", + }) }) } } diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 4bda02ba3..754811df6 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -248,6 +248,53 @@ impl OutlineElem { type OutlineEntry; } +impl Packed { + /// Realizes the title and entries. + pub fn realize( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<(Option, Vec)> { + let span = self.span(); + + // Build the outline title. + let title = self + .title + .get_cloned(styles) + .unwrap_or_else(|| { + Some( + TextElem::packed(Packed::::local_name_in(styles)) + .spanned(span), + ) + }) + .map(|title| { + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(span) + }); + + let elems = engine.introspector.query(&self.target.get_ref(styles).0); + let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX); + + // Build the outline entries. + let mut entries = vec![]; + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); + }; + + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + entries.push(entry.pack().spanned(span)); + } + } + + Ok((title, entries)) + } +} + impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/tests/ref/html/outline-figure-html.html b/tests/ref/html/outline-figure-html.html new file mode 100644 index 000000000..37fb6a38c --- /dev/null +++ b/tests/ref/html/outline-figure-html.html @@ -0,0 +1,22 @@ + + + + + + + +

Contents

+
    +
  1. Figure 1: A
  2. +
  3. Figure 2: B
  4. +
+
+

An A

+
Figure 1: A
+
+
+

An B

+
Figure 2: B
+
+ + diff --git a/tests/ref/html/outline-html.html b/tests/ref/html/outline-html.html new file mode 100644 index 000000000..b7a3bc1b9 --- /dev/null +++ b/tests/ref/html/outline-html.html @@ -0,0 +1,24 @@ + + + + + + + +

Contents

+
    +
  1. 1 A
  2. +
  3. 2 B
  4. +
  5. 3 C
  6. +
  7. 4 D
  8. +
  9. 4.1 E
  10. +
  11. 4.2 F
  12. +
+

1 A

+

2 B

+

3 C

+

4 D

+

4.1 E

+

4.2 F

+ + diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index 49fd7d7cb..d75961032 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -252,6 +252,23 @@ A = B = C +--- outline-html html --- +#set heading(numbering: "1.1") + +#outline() + += A += B += C += D +== E +== F + +--- outline-figure-html html --- +#outline(target: figure) +#figure([An A], caption: [A]) +#figure([An B], caption: [B]) + --- issue-2048-outline-multiline --- // Without the word joiner between the dots and the page number, // the page number would be alone in its line.