mirror of
https://github.com/typst/typst
synced 2025-08-17 08:28: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.
|
/// Adds a new property in builder-style.
|
||||||
#[expect(unused)]
|
|
||||||
pub fn with(mut self, property: &str, value: impl Display) -> Self {
|
pub fn with(mut self, property: &str, value: impl Display) -> Self {
|
||||||
self.push(property, value);
|
self.push(property, value);
|
||||||
self
|
self
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use comemo::Track;
|
||||||
use ecow::{eco_format, EcoVec};
|
use ecow::{eco_format, EcoVec};
|
||||||
use typst_library::diag::{warning, At};
|
use typst_library::diag::{warning, At};
|
||||||
use typst_library::foundations::{
|
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::introspection::{Counter, Locator};
|
||||||
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
||||||
use typst_library::layout::{OuterVAlignment, Sizing};
|
use typst_library::layout::{OuterVAlignment, Sizing};
|
||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
||||||
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, OutlineElem, OutlineEntry,
|
||||||
RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
||||||
};
|
};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
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_RULE);
|
||||||
rules.register(Html, FIGURE_CAPTION_RULE);
|
rules.register(Html, FIGURE_CAPTION_RULE);
|
||||||
rules.register(Html, QUOTE_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, REF_RULE);
|
||||||
rules.register(Html, CITE_GROUP_RULE);
|
rules.register(Html, CITE_GROUP_RULE);
|
||||||
rules.register(Html, TABLE_RULE);
|
rules.register(Html, TABLE_RULE);
|
||||||
@ -274,6 +277,56 @@ const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
|||||||
Ok(realized)
|
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 REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
|
||||||
|
|
||||||
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
|
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
|
||||||
|
@ -20,8 +20,8 @@ use typst_library::math::EquationElem;
|
|||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
|
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
|
||||||
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
|
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
|
||||||
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
|
LinkElem, ListElem, OutlineElem, OutlineEntry, ParElem, ParbreakElem, QuoteElem,
|
||||||
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
||||||
};
|
};
|
||||||
use typst_library::pdf::EmbedElem;
|
use typst_library::pdf::EmbedElem;
|
||||||
use typst_library::text::{
|
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| {
|
const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
|
||||||
let span = elem.span();
|
let (title, entries) = elem.realize(engine, styles)?;
|
||||||
|
Ok(Content::sequence(title.into_iter().chain(entries)))
|
||||||
// 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))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {
|
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 prefix = elem.prefix(engine, context, span)?;
|
||||||
let inner = elem.inner(engine, context, span)?;
|
let inner = elem.inner(engine, context, span)?;
|
||||||
let block = if elem.element.is::<EquationElem>() {
|
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;
|
let body = prefix.unwrap_or_default() + inner;
|
||||||
BlockElem::new()
|
BlockElem::new()
|
||||||
.with_body(Some(BlockBody::Content(body)))
|
.with_body(Some(BlockBody::Content(body)))
|
||||||
|
@ -19,7 +19,7 @@ use crate::layout::{
|
|||||||
VAlignment,
|
VAlignment,
|
||||||
};
|
};
|
||||||
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
|
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
|
||||||
use crate::text::{Lang, Region, TextElem};
|
use crate::text::{Lang, TextElem};
|
||||||
use crate::visualize::ImageElem;
|
use crate::visualize::ImageElem;
|
||||||
|
|
||||||
/// A figure with an optional caption.
|
/// A figure with an optional caption.
|
||||||
@ -526,23 +526,23 @@ impl FigureCaption {
|
|||||||
Ok(realized)
|
Ok(realized)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the default separator in the given language and (optionally)
|
/// Retrieves the locale separator.
|
||||||
/// region.
|
pub fn get_separator(&self, styles: StyleChain) -> Content {
|
||||||
fn local_separator(lang: Lang, _: Option<Region>) -> &'static str {
|
self.separator
|
||||||
match lang {
|
.get_cloned(styles)
|
||||||
Lang::CHINESE => "\u{2003}",
|
.unwrap_or_else(|| Self::local_separator_in(styles))
|
||||||
Lang::FRENCH => ".\u{a0}– ",
|
|
||||||
Lang::RUSSIAN => ". ",
|
|
||||||
Lang::ENGLISH | _ => ": ",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_separator(&self, styles: StyleChain) -> Content {
|
/// Gets the default separator in the given language and (optionally)
|
||||||
self.separator.get_cloned(styles).unwrap_or_else(|| {
|
/// region.
|
||||||
TextElem::packed(Self::local_separator(
|
pub fn local_separator_in(styles: StyleChain) -> Content {
|
||||||
styles.get(TextElem::lang),
|
styles.get_cloned(Self::separator).unwrap_or_else(|| {
|
||||||
styles.get(TextElem::region),
|
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;
|
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> {
|
impl ShowSet for Packed<OutlineElem> {
|
||||||
fn show_set(&self, styles: StyleChain) -> Styles {
|
fn show_set(&self, styles: StyleChain) -> Styles {
|
||||||
let mut out = Styles::new();
|
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
|
= B
|
||||||
= C
|
= 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 ---
|
--- issue-2048-outline-multiline ---
|
||||||
// Without the word joiner between the dots and the page number,
|
// Without the word joiner between the dots and the page number,
|
||||||
// the page number would be alone in its line.
|
// the page number would be alone in its line.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user