mirror of
https://github.com/typst/typst
synced 2025-08-17 00:18:33 +08:00
Support for outline in HTML
This commit is contained in:
parent
c58ef50a0a
commit
f4be243396
@ -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
|
||||
|
@ -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<QuoteElem> = |elem, _, styles| {
|
||||
Ok(realized)
|
||||
};
|
||||
|
||||
const OUTLINE_RULE: ShowFn<OutlineElem> = |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<OutlineEntry> = |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::<FigureElem>() {
|
||||
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<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
|
||||
|
||||
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
|
||||
|
@ -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<FootnoteEntry> = |elem, engine, styles| {
|
||||
};
|
||||
|
||||
const OUTLINE_RULE: ShowFn<OutlineElem> = |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::<OutlineElem>::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::<dyn Outlinable>() 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<OutlineEntry> = |elem, engine, styles| {
|
||||
@ -454,6 +424,7 @@ const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {
|
||||
let prefix = elem.prefix(engine, context, span)?;
|
||||
let inner = elem.inner(engine, context, span)?;
|
||||
let block = if elem.element.is::<EquationElem>() {
|
||||
// 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)))
|
||||
|
@ -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<Region>) -> &'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 | _ => ": ",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -248,6 +248,53 @@ impl OutlineElem {
|
||||
type OutlineEntry;
|
||||
}
|
||||
|
||||
impl Packed<OutlineElem> {
|
||||
/// Realizes the title and entries.
|
||||
pub fn realize(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<(Option<Content>, Vec<Content>)> {
|
||||
let span = self.span();
|
||||
|
||||
// Build the outline title.
|
||||
let title = self
|
||||
.title
|
||||
.get_cloned(styles)
|
||||
.unwrap_or_else(|| {
|
||||
Some(
|
||||
TextElem::packed(Packed::<OutlineElem>::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::<dyn Outlinable>() 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<OutlineElem> {
|
||||
fn show_set(&self, styles: StyleChain) -> Styles {
|
||||
let mut out = Styles::new();
|
||||
|
22
tests/ref/html/outline-figure-html.html
Normal file
22
tests/ref/html/outline-figure-html.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<h2>Contents</h2>
|
||||
<ol class="outline" style="list-style: none">
|
||||
<li><a href="#loc-1"><span class="prefix">Figure 1</span>: A</a></li>
|
||||
<li><a href="#loc-2"><span class="prefix">Figure 2</span>: B</a></li>
|
||||
</ol>
|
||||
<figure id="loc-1">
|
||||
<p>An A</p>
|
||||
<figcaption>Figure 1: A</figcaption>
|
||||
</figure>
|
||||
<figure id="loc-2">
|
||||
<p>An B</p>
|
||||
<figcaption>Figure 2: B</figcaption>
|
||||
</figure>
|
||||
</body>
|
||||
</html>
|
24
tests/ref/html/outline-html.html
Normal file
24
tests/ref/html/outline-html.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<h2>Contents</h2>
|
||||
<ol class="outline" style="list-style: none">
|
||||
<li><a href="#a"><span class="prefix">1</span> A</a></li>
|
||||
<li><a href="#b"><span class="prefix">2</span> B</a></li>
|
||||
<li><a href="#c"><span class="prefix">3</span> C</a></li>
|
||||
<li><a href="#d"><span class="prefix">4</span> D</a></li>
|
||||
<li><a href="#e"><span class="prefix">4.1</span> E</a></li>
|
||||
<li><a href="#f"><span class="prefix">4.2</span> F</a></li>
|
||||
</ol>
|
||||
<h2 id="a">1 A</h2>
|
||||
<h2 id="b">2 B</h2>
|
||||
<h2 id="c">3 C</h2>
|
||||
<h2 id="d">4 D</h2>
|
||||
<h3 id="e">4.1 E</h3>
|
||||
<h3 id="f">4.2 F</h3>
|
||||
</body>
|
||||
</html>
|
@ -252,6 +252,23 @@ A
|
||||
= B
|
||||
= C
|
||||
|
||||
--- outline-html html ---
|
||||
#set heading(numbering: "1.1")
|
||||
|
||||
#outline()
|
||||
|
||||
= A <a>
|
||||
= B <b>
|
||||
= C <c>
|
||||
= D <d>
|
||||
== E <e>
|
||||
== F <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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user