summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/meta
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/meta
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/meta')
-rw-r--r--crates/typst-library/src/meta/bibliography.rs724
-rw-r--r--crates/typst-library/src/meta/context.rs220
-rw-r--r--crates/typst-library/src/meta/counter.rs683
-rw-r--r--crates/typst-library/src/meta/document.rs86
-rw-r--r--crates/typst-library/src/meta/figure.rs351
-rw-r--r--crates/typst-library/src/meta/footnote.rs299
-rw-r--r--crates/typst-library/src/meta/heading.rs239
-rw-r--r--crates/typst-library/src/meta/link.rs137
-rw-r--r--crates/typst-library/src/meta/mod.rs64
-rw-r--r--crates/typst-library/src/meta/numbering.rs525
-rw-r--r--crates/typst-library/src/meta/outline.rs528
-rw-r--r--crates/typst-library/src/meta/query.rs145
-rw-r--r--crates/typst-library/src/meta/reference.rs276
-rw-r--r--crates/typst-library/src/meta/state.rs440
14 files changed, 4717 insertions, 0 deletions
diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs
new file mode 100644
index 00000000..0531997d
--- /dev/null
+++ b/crates/typst-library/src/meta/bibliography.rs
@@ -0,0 +1,724 @@
+use std::collections::HashMap;
+use std::ffi::OsStr;
+use std::path::Path;
+use std::sync::Arc;
+
+use ecow::{eco_vec, EcoVec};
+use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
+use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting};
+use hayagriva::Entry;
+use typst::diag::FileError;
+use typst::util::{option_eq, Bytes};
+
+use super::{LinkElem, LocalName, RefElem};
+use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
+use crate::meta::{FootnoteElem, HeadingElem};
+use crate::prelude::*;
+use crate::text::TextElem;
+
+/// A bibliography / reference listing.
+///
+/// You can create a new bibliography by calling this function with a path
+/// to a bibliography file in either one of two formats:
+///
+/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format
+/// designed for use with Typst. Visit its
+/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)
+/// for more details.
+/// - A BibLaTeX `.bib` file.
+///
+/// As soon as you add a bibliography somewhere in your document, you can start
+/// citing things with reference syntax (`[@key]`) or explicit calls to the
+/// [citation]($func/cite) function (`[#cite("key")]`). The bibliography will
+/// only show entries for works that were referenced in the document.
+///
+/// # Example
+/// ```example
+/// This was already noted by
+/// pirates long ago. @arrgh
+///
+/// Multiple sources say ...
+/// #cite("arrgh", "netwok").
+///
+/// #bibliography("works.bib")
+/// ```
+///
+/// Display: Bibliography
+/// Category: meta
+#[element(Locatable, Synthesize, Show, Finalize, LocalName)]
+pub struct BibliographyElem {
+ /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file.
+ #[required]
+ #[parse(
+ let Spanned { v: paths, span } =
+ args.expect::<Spanned<BibPaths>>("path to bibliography file")?;
+
+ // Load bibliography files.
+ let data = paths.0
+ .iter()
+ .map(|path| {
+ let id = vm.location().join(path).at(span)?;
+ vm.world().file(id).at(span)
+ })
+ .collect::<SourceResult<Vec<Bytes>>>()?;
+
+ // Check that parsing works.
+ let _ = load(&paths, &data).at(span)?;
+
+ paths
+ )]
+ pub path: BibPaths,
+
+ /// The raw file buffers.
+ #[internal]
+ #[required]
+ #[parse(data)]
+ pub data: Vec<Bytes>,
+
+ /// The title of the bibliography.
+ ///
+ /// - When set to `{auto}`, an appropriate title for the [text
+ /// language]($func/text.lang) will be used. This is the default.
+ /// - When set to `{none}`, the bibliography will not have a title.
+ /// - A custom title can be set by passing content.
+ ///
+ /// The bibliography's heading will not be numbered by default, but you can
+ /// force it to be with a show-set rule:
+ /// `{show bibliography: set heading(numbering: "1.")}`
+ /// ```
+ #[default(Some(Smart::Auto))]
+ pub title: Option<Smart<Content>>,
+
+ /// The bibliography style.
+ #[default(BibliographyStyle::Ieee)]
+ pub style: BibliographyStyle,
+}
+
+/// A list of bibliography file paths.
+#[derive(Debug, Default, Clone, Hash)]
+pub struct BibPaths(Vec<EcoString>);
+
+cast! {
+ BibPaths,
+ self => self.0.into_value(),
+ v: EcoString => Self(vec![v]),
+ v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
+}
+
+impl BibliographyElem {
+ /// Find the document's bibliography.
+ pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
+ let mut iter = introspector.query(&Self::func().select()).into_iter();
+ let Some(elem) = iter.next() else {
+ bail!("the document does not contain a bibliography");
+ };
+
+ if iter.next().is_some() {
+ bail!("multiple bibliographies are not supported");
+ }
+
+ Ok(elem.to::<Self>().unwrap().clone())
+ }
+
+ /// Whether the bibliography contains the given key.
+ pub fn has(vt: &Vt, key: &str) -> bool {
+ vt.introspector
+ .query(&Self::func().select())
+ .into_iter()
+ .flat_map(|elem| {
+ let elem = elem.to::<Self>().unwrap();
+ load(&elem.path(), &elem.data())
+ })
+ .flatten()
+ .any(|entry| entry.key() == key)
+ }
+
+ /// Find all bibliography keys.
+ pub fn keys(
+ introspector: Tracked<Introspector>,
+ ) -> Vec<(EcoString, Option<EcoString>)> {
+ Self::find(introspector)
+ .and_then(|elem| load(&elem.path(), &elem.data()))
+ .into_iter()
+ .flatten()
+ .map(|entry| {
+ let key = entry.key().into();
+ let detail =
+ entry.title().map(|title| title.canonical.value.as_str().into());
+ (key, detail)
+ })
+ .collect()
+ }
+}
+
+impl Synthesize for BibliographyElem {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_style(self.style(styles));
+ Ok(())
+ }
+}
+
+impl Show for BibliographyElem {
+ #[tracing::instrument(name = "BibliographyElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ const COLUMN_GUTTER: Em = Em::new(0.65);
+ const INDENT: Em = Em::new(1.5);
+
+ let mut seq = vec![];
+ if let Some(title) = self.title(styles) {
+ let title =
+ title.unwrap_or_else(|| {
+ TextElem::packed(self.local_name(
+ TextElem::lang_in(styles),
+ TextElem::region_in(styles),
+ ))
+ .spanned(self.span())
+ });
+
+ seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack());
+ }
+
+ Ok(vt.delayed(|vt| {
+ let works = Works::new(vt).at(self.span())?;
+
+ let row_gutter = BlockElem::below_in(styles).amount();
+ if works.references.iter().any(|(prefix, _)| prefix.is_some()) {
+ let mut cells = vec![];
+ for (prefix, reference) in &works.references {
+ cells.push(prefix.clone().unwrap_or_default());
+ cells.push(reference.clone());
+ }
+
+ seq.push(VElem::new(row_gutter).with_weakness(3).pack());
+ seq.push(
+ GridElem::new(cells)
+ .with_columns(TrackSizings(vec![Sizing::Auto; 2]))
+ .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()]))
+ .with_row_gutter(TrackSizings(vec![row_gutter.into()]))
+ .pack(),
+ );
+ } else {
+ let mut entries = vec![];
+ for (_, reference) in &works.references {
+ entries.push(VElem::new(row_gutter).with_weakness(3).pack());
+ entries.push(reference.clone());
+ }
+
+ seq.push(
+ Content::sequence(entries)
+ .styled(ParElem::set_hanging_indent(INDENT.into())),
+ );
+ }
+
+ Ok(Content::sequence(seq))
+ }))
+ }
+}
+
+impl Finalize for BibliographyElem {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ realized.styled(HeadingElem::set_numbering(None))
+ }
+}
+
+impl LocalName for BibliographyElem {
+ fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
+ match lang {
+ Lang::ALBANIAN => "Bibliografi",
+ Lang::ARABIC => "المراجع",
+ Lang::BOKMÅL => "Bibliografi",
+ Lang::CHINESE if option_eq(region, "TW") => "書目",
+ Lang::CHINESE => "参考文献",
+ Lang::CZECH => "Bibliografie",
+ Lang::DANISH => "Bibliografi",
+ Lang::DUTCH => "Bibliografie",
+ Lang::FILIPINO => "Bibliograpiya",
+ Lang::FRENCH => "Bibliographie",
+ Lang::GERMAN => "Bibliographie",
+ Lang::ITALIAN => "Bibliografia",
+ Lang::NYNORSK => "Bibliografi",
+ Lang::POLISH => "Bibliografia",
+ Lang::PORTUGUESE => "Bibliografia",
+ Lang::RUSSIAN => "Библиография",
+ Lang::SLOVENIAN => "Literatura",
+ Lang::SPANISH => "Bibliografía",
+ Lang::SWEDISH => "Bibliografi",
+ Lang::TURKISH => "Kaynakça",
+ Lang::UKRAINIAN => "Бібліографія",
+ Lang::VIETNAMESE => "Tài liệu tham khảo",
+ Lang::ENGLISH | _ => "Bibliography",
+ }
+ }
+}
+
+/// A bibliography style.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum BibliographyStyle {
+ /// Follows guidance of the American Psychological Association. Based on the
+ /// 7th edition of the APA Publication Manual.
+ Apa,
+ /// The Chicago Author Date style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 15.
+ ChicagoAuthorDate,
+ /// The Chicago Notes style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 14.
+ ChicagoNotes,
+ /// The style of the Institute of Electrical and Electronics Engineers.
+ /// Based on the 2018 IEEE Reference Guide.
+ Ieee,
+ /// Follows guidance of the Modern Language Association. Based on the 8th
+ /// edition of the MLA Handbook.
+ Mla,
+}
+
+impl BibliographyStyle {
+ /// The default citation style for this bibliography style.
+ pub fn default_citation_style(self) -> CitationStyle {
+ match self {
+ Self::Apa => CitationStyle::ChicagoAuthorDate,
+ Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate,
+ Self::ChicagoNotes => CitationStyle::ChicagoNotes,
+ Self::Ieee => CitationStyle::Numerical,
+ Self::Mla => CitationStyle::ChicagoAuthorDate,
+ }
+ }
+}
+
+/// Cite a work from the bibliography.
+///
+/// Before you starting citing, you need to add a
+/// [bibliography]($func/bibliography) somewhere in your document.
+///
+/// # Example
+/// ```example
+/// This was already noted by
+/// pirates long ago. @arrgh
+///
+/// Multiple sources say ...
+/// #cite("arrgh", "netwok").
+///
+/// #bibliography("works.bib")
+/// ```
+///
+/// # Syntax
+/// This function indirectly has dedicated syntax. [References]($func/ref)
+/// can be used to cite works from the bibliography. The label then
+/// corresponds to the citation key.
+///
+/// Display: Citation
+/// Category: meta
+#[element(Locatable, Synthesize, Show)]
+pub struct CiteElem {
+ /// The citation keys that identify the elements that shall be cited in
+ /// the bibliography.
+ ///
+ /// Reference syntax supports only a single key.
+ #[variadic]
+ pub keys: Vec<EcoString>,
+
+ /// A supplement for the citation such as page or chapter number.
+ ///
+ /// In reference syntax, the supplement can be added in square brackets:
+ ///
+ /// ```example
+ /// This has been proven over and
+ /// over again. @distress[p.~7]
+ ///
+ /// #bibliography("works.bib")
+ /// ```
+ #[positional]
+ pub supplement: Option<Content>,
+
+ /// Whether the citation should include brackets.
+ ///
+ /// ```example
+ /// #set cite(brackets: false)
+ ///
+ /// @netwok follow these methods
+ /// in their work ...
+ ///
+ /// #bibliography(
+ /// "works.bib",
+ /// style: "chicago-author-date",
+ /// )
+ /// ```
+ #[default(true)]
+ pub brackets: bool,
+
+ /// The citation style.
+ ///
+ /// When set to `{auto}`, automatically picks the preferred citation style
+ /// for the bibliography's style.
+ ///
+ /// ```example
+ /// #set cite(style: "alphanumerical")
+ /// Alphanumerical references.
+ /// @netwok
+ ///
+ /// #bibliography("works.bib")
+ /// ```
+ pub style: Smart<CitationStyle>,
+}
+
+impl Synthesize for CiteElem {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_supplement(self.supplement(styles));
+ self.push_brackets(self.brackets(styles));
+ self.push_style(self.style(styles));
+ Ok(())
+ }
+}
+
+impl Show for CiteElem {
+ #[tracing::instrument(name = "CiteElem::show", skip(self, vt))]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let works = Works::new(vt).at(self.span())?;
+ let location = self.0.location().unwrap();
+ works
+ .citations
+ .get(&location)
+ .cloned()
+ .flatten()
+ .ok_or("bibliography does not contain this key")
+ .at(self.span())
+ }))
+ }
+}
+
+cast! {
+ CiteElem,
+ v: Content => v.to::<Self>().cloned().ok_or("expected citation")?,
+}
+
+/// A citation style.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum CitationStyle {
+ /// IEEE-style numerical reference markers.
+ Numerical,
+ /// A simple alphanumerical style. For example, the output could be Rass97
+ /// or MKG+21.
+ Alphanumerical,
+ /// Citations that just consist of the entry keys.
+ Keys,
+ /// The Chicago Author Date style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 15.
+ ChicagoAuthorDate,
+ /// The Chicago Notes style. Based on the 17th edition of the Chicago
+ /// Manual of Style, Chapter 14.
+ ChicagoNotes,
+ /// A Chicago-like author-title format. Results could look like this:
+ /// Prokopov, “It Is Fast or It Is Wrong”.
+ ChicagoAuthorTitle,
+}
+
+impl CitationStyle {
+ fn is_short(self) -> bool {
+ matches!(self, Self::Numerical | Self::Alphanumerical | Self::Keys)
+ }
+}
+
+/// Fully formatted citations and references.
+#[derive(Default)]
+struct Works {
+ citations: HashMap<Location, Option<Content>>,
+ references: Vec<(Option<Content>, Content)>,
+}
+
+impl Works {
+ /// Prepare all things need to cite a work or format a bibliography.
+ fn new(vt: &Vt) -> StrResult<Arc<Self>> {
+ let bibliography = BibliographyElem::find(vt.introspector)?;
+ let citations = vt
+ .introspector
+ .query(&Selector::Or(eco_vec![
+ RefElem::func().select(),
+ CiteElem::func().select(),
+ ]))
+ .into_iter()
+ .map(|elem| match elem.to::<RefElem>() {
+ Some(reference) => reference.citation().unwrap(),
+ _ => elem.to::<CiteElem>().unwrap().clone(),
+ })
+ .collect();
+ Ok(create(bibliography, citations))
+ }
+}
+
+/// Generate all citations and the whole bibliography.
+#[comemo::memoize]
+fn create(bibliography: BibliographyElem, citations: Vec<CiteElem>) -> Arc<Works> {
+ let span = bibliography.span();
+ let entries = load(&bibliography.path(), &bibliography.data()).unwrap();
+ let style = bibliography.style(StyleChain::default());
+ let bib_location = bibliography.0.location().unwrap();
+ let ref_location = |target: &Entry| {
+ let i = entries
+ .iter()
+ .position(|entry| entry.key() == target.key())
+ .unwrap_or_default();
+ bib_location.variant(i)
+ };
+
+ let mut db = Database::new();
+ let mut ids = HashMap::new();
+ let mut preliminary = vec![];
+
+ for citation in citations {
+ let cite_id = citation.0.location().unwrap();
+ let entries = citation
+ .keys()
+ .into_iter()
+ .map(|key| {
+ let entry = entries.iter().find(|entry| entry.key() == key)?;
+ ids.entry(entry.key()).or_insert(cite_id);
+ db.push(entry);
+ Some(entry)
+ })
+ .collect::<Option<Vec<_>>>();
+ preliminary.push((citation, entries));
+ }
+
+ let mut current = CitationStyle::Numerical;
+ let mut citation_style: Box<dyn style::CitationStyle> =
+ Box::new(style::Numerical::new());
+
+ let citations = preliminary
+ .into_iter()
+ .map(|(citation, cited)| {
+ let location = citation.0.location().unwrap();
+ let Some(cited) = cited else { return (location, None) };
+
+ let mut supplement = citation.supplement(StyleChain::default());
+ let brackets = citation.brackets(StyleChain::default());
+ let style = citation
+ .style(StyleChain::default())
+ .unwrap_or(style.default_citation_style());
+
+ if style != current {
+ current = style;
+ citation_style = match style {
+ CitationStyle::Numerical => Box::new(style::Numerical::new()),
+ CitationStyle::Alphanumerical => {
+ Box::new(style::Alphanumerical::new())
+ }
+ CitationStyle::ChicagoAuthorDate => {
+ Box::new(style::ChicagoAuthorDate::new())
+ }
+ CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
+ CitationStyle::ChicagoAuthorTitle => {
+ Box::new(style::AuthorTitle::new())
+ }
+ CitationStyle::Keys => Box::new(style::Keys::new()),
+ };
+ }
+
+ let len = cited.len();
+ let mut content = Content::empty();
+ for (i, entry) in cited.into_iter().enumerate() {
+ let supplement = if i + 1 == len { supplement.take() } else { None };
+ let mut display = db
+ .citation(
+ &mut *citation_style,
+ &[Citation {
+ entry,
+ supplement: supplement.is_some().then_some(SUPPLEMENT),
+ }],
+ )
+ .display;
+
+ if style.is_short() {
+ display.value = display.value.replace(' ', "\u{a0}");
+ }
+
+ if brackets && len == 1 {
+ display = display.with_default_brackets(&*citation_style);
+ }
+
+ if i > 0 {
+ content += TextElem::packed(",\u{a0}");
+ }
+
+ // Format and link to the reference entry.
+ content += format_display_string(&display, supplement, citation.span())
+ .linked(Destination::Location(ref_location(entry)));
+ }
+
+ if brackets && len > 1 {
+ content = match citation_style.brackets() {
+ Brackets::None => content,
+ Brackets::Round => {
+ TextElem::packed('(') + content + TextElem::packed(')')
+ }
+ Brackets::Square => {
+ TextElem::packed('[') + content + TextElem::packed(']')
+ }
+ };
+ }
+
+ if style == CitationStyle::ChicagoNotes {
+ content = FootnoteElem::with_content(content).pack();
+ }
+
+ (location, Some(content))
+ })
+ .collect();
+
+ let bibliography_style: Box<dyn style::BibliographyStyle> = match style {
+ BibliographyStyle::Apa => Box::new(style::Apa::new()),
+ BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()),
+ BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()),
+ BibliographyStyle::Ieee => Box::new(style::Ieee::new()),
+ BibliographyStyle::Mla => Box::new(style::Mla::new()),
+ };
+
+ let references = db
+ .bibliography(&*bibliography_style, None)
+ .into_iter()
+ .map(|reference| {
+ let backlink = ref_location(reference.entry);
+ let prefix = reference.prefix.map(|prefix| {
+ // Format and link to first citation.
+ let bracketed = prefix.with_default_brackets(&*citation_style);
+ format_display_string(&bracketed, None, span)
+ .linked(Destination::Location(ids[reference.entry.key()]))
+ .backlinked(backlink)
+ });
+
+ let mut reference = format_display_string(&reference.display, None, span);
+ if prefix.is_none() {
+ reference = reference.backlinked(backlink);
+ }
+
+ (prefix, reference)
+ })
+ .collect();
+
+ Arc::new(Works { citations, references })
+}
+
+/// Load bibliography entries from a path.
+#[comemo::memoize]
+fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<EcoVec<hayagriva::Entry>> {
+ let mut result = EcoVec::new();
+
+ // We might have multiple bib/yaml files
+ for (path, bytes) in paths.0.iter().zip(data) {
+ let src = std::str::from_utf8(bytes).map_err(|_| FileError::InvalidUtf8)?;
+ let entries = parse_bib(path, src)?;
+ result.extend(entries);
+ }
+
+ // Biblatex only checks for duplicate keys within files
+ // -> We have to do this between files again
+ let mut keys = result.iter().map(|r| r.key()).collect::<Vec<_>>();
+ keys.sort_unstable();
+ // Waiting for `slice_partition_dedup` #54279
+ let mut duplicates = Vec::new();
+ for pair in keys.windows(2) {
+ if pair[0] == pair[1] {
+ duplicates.push(pair[0]);
+ }
+ }
+
+ if !duplicates.is_empty() {
+ Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", ")))
+ } else {
+ Ok(result)
+ }
+}
+
+/// Parse a bibliography file (bib/yml/yaml)
+fn parse_bib(path_str: &str, src: &str) -> StrResult<Vec<hayagriva::Entry>> {
+ let path = Path::new(path_str);
+ let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
+ match ext.to_lowercase().as_str() {
+ "yml" | "yaml" => {
+ hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error)
+ }
+ "bib" => hayagriva::io::from_biblatex_str(src).map_err(|err| {
+ err.into_iter()
+ .next()
+ .map(|error| format_biblatex_error(path_str, src, error))
+ .unwrap_or_else(|| eco_format!("failed to parse {path_str}"))
+ }),
+ _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
+ }
+}
+
+/// Format a Hayagriva loading error.
+fn format_hayagriva_error(error: YamlBibliographyError) -> EcoString {
+ eco_format!("{error}")
+}
+
+/// Format a BibLaTeX loading error.
+fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString {
+ let (span, msg) = match error {
+ BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()),
+ BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
+ };
+ let line = src.get(..span.start).unwrap_or_default().lines().count();
+ eco_format!("parsing failed at {path}:{line}: {msg}")
+}
+
+/// Hayagriva only supports strings, but we have a content supplement. To deal
+/// with this, we pass this string to hayagriva instead of our content, find it
+/// in the output and replace it with the content.
+const SUPPLEMENT: &str = "cdc579c45cf3d648905c142c7082683f";
+
+/// Format a display string into content.
+fn format_display_string(
+ string: &DisplayString,
+ mut supplement: Option<Content>,
+ span: Span,
+) -> Content {
+ let mut stops: Vec<_> = string
+ .formatting
+ .iter()
+ .flat_map(|(range, _)| [range.start, range.end])
+ .collect();
+
+ if let Some(i) = string.value.find(SUPPLEMENT) {
+ stops.push(i);
+ stops.push(i + SUPPLEMENT.len());
+ }
+
+ stops.sort();
+ stops.dedup();
+ stops.push(string.value.len());
+
+ let mut start = 0;
+ let mut seq = vec![];
+ for stop in stops {
+ let segment = string.value.get(start..stop).unwrap_or_default();
+ if segment.is_empty() {
+ continue;
+ }
+
+ let mut content = if segment == SUPPLEMENT && supplement.is_some() {
+ supplement.take().unwrap_or_default()
+ } else {
+ TextElem::packed(segment).spanned(span)
+ };
+
+ for (range, fmt) in &string.formatting {
+ if !range.contains(&start) {
+ continue;
+ }
+
+ content = match fmt {
+ Formatting::Bold => content.strong(),
+ Formatting::Italic => content.emph(),
+ Formatting::Link(link) => {
+ LinkElem::new(Destination::Url(link.as_str().into()).into(), content)
+ .pack()
+ }
+ };
+ }
+
+ seq.push(content);
+ start = stop;
+ }
+
+ Content::sequence(seq)
+}
diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs
new file mode 100644
index 00000000..a42c6980
--- /dev/null
+++ b/crates/typst-library/src/meta/context.rs
@@ -0,0 +1,220 @@
+use crate::prelude::*;
+
+/// Provides access to the location of content.
+///
+/// This is useful in combination with [queries]($func/query),
+/// [counters]($func/counter), [state]($func/state), and [links]($func/link).
+/// See their documentation for more details.
+///
+/// ```example
+/// #locate(loc => [
+/// My location: \
+/// #loc.position()!
+/// ])
+/// ```
+///
+/// ## Methods
+/// ### page()
+/// Returns the page number for this location.
+///
+/// Note that this does not return the value of the [page counter]($func/counter)
+/// at this location, but the true page number (starting from one).
+///
+/// If you want to know the value of the page counter, use
+/// `{counter(page).at(loc)}` instead.
+///
+/// - returns: integer
+///
+/// ### position()
+/// Returns a dictionary with the page number and the x, y position for this
+/// location. The page number starts at one and the coordinates are measured
+/// from the top-left of the page.
+///
+/// If you only need the page number, use `page()` instead as it allows Typst
+/// to skip unnecessary work.
+///
+/// - returns: dictionary
+///
+/// ### page-numbering()
+/// Returns the page numbering pattern of the page at this location. This can be
+/// used when displaying the page counter in order to obtain the local numbering.
+/// This is useful if you are building custom indices or outlines.
+///
+/// If the page numbering is set to `none` at that location, this function returns `none`.
+///
+/// - returns: string or function or none
+///
+/// Display: Locate
+/// Category: meta
+#[func]
+pub fn locate(
+ /// A function that receives a `location`. Its return value is displayed
+ /// in the document.
+ ///
+ /// This function is called once for each time the content returned by
+ /// `locate` appears in the document. That makes it possible to generate
+ /// content that depends on its own location in the document.
+ func: Func,
+) -> Content {
+ LocateElem::new(func).pack()
+}
+
+/// Executes a `locate` call.
+///
+/// Display: Locate
+/// Category: special
+#[element(Locatable, Show)]
+struct LocateElem {
+ /// The function to call with the location.
+ #[required]
+ func: Func,
+}
+
+impl Show for LocateElem {
+ #[tracing::instrument(name = "LocateElem::show", skip(self, vt))]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let location = self.0.location().unwrap();
+ Ok(self.func().call_vt(vt, [location])?.display())
+ }))
+ }
+}
+
+/// Provides access to active styles.
+///
+/// The styles are currently opaque and only useful in combination with the
+/// [`measure`]($func/measure) function. See its documentation for more details.
+/// In the future, the provided styles might also be directly accessed to look
+/// up styles defined by [set rules]($styling/#set-rules).
+///
+/// ```example
+/// #let thing(body) = style(styles => {
+/// let size = measure(body, styles)
+/// [Width of "#body" is #size.width]
+/// })
+///
+/// #thing[Hey] \
+/// #thing[Welcome]
+/// ```
+///
+/// Display: Style
+/// Category: meta
+#[func]
+pub fn style(
+ /// A function to call with the styles. Its return value is displayed
+ /// in the document.
+ ///
+ /// This function is called once for each time the content returned by
+ /// `style` appears in the document. That makes it possible to generate
+ /// content that depends on the style context it appears in.
+ func: Func,
+) -> Content {
+ StyleElem::new(func).pack()
+}
+
+/// Executes a style access.
+///
+/// Display: Style
+/// Category: special
+#[element(Show)]
+struct StyleElem {
+ /// The function to call with the styles.
+ #[required]
+ func: Func,
+}
+
+impl Show for StyleElem {
+ #[tracing::instrument(name = "StyleElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ Ok(self.func().call_vt(vt, [styles.to_map()])?.display())
+ }
+}
+
+/// Provides access to the current outer container's (or page's, if none) size
+/// (width and height).
+///
+/// The given function must accept a single parameter, `size`, which is a
+/// dictionary with keys `width` and `height`, both of type
+/// [`length`]($type/length).
+///
+
+/// ```example
+/// #let text = lorem(30)
+/// #layout(size => style(styles => [
+/// #let (height,) = measure(
+/// block(width: size.width, text),
+/// styles,
+/// )
+/// This text is #height high with
+/// the current page width: \
+/// #text
+/// ]))
+/// ```
+///
+/// If the `layout` call is placed inside of a box width a width of `{800pt}`
+/// and a height of `{400pt}`, then the specified function will be given the
+/// parameter `{(width: 800pt, height: 400pt)}`. If it placed directly into the
+/// page it receives the page's dimensions minus its margins. This is mostly
+/// useful in combination with [measurement]($func/measure).
+///
+/// You can also use this function to resolve [`ratio`]($type/ratio) to fixed
+/// lengths. This might come in handy if you're building your own layout
+/// abstractions.
+///
+/// ```example
+/// #layout(size => {
+/// let half = 50% * size.width
+/// [Half a page is #half wide.]
+/// })
+/// ```
+///
+/// Note that this function will provide an infinite width or height if one of
+/// the page width or height is `auto`, respectively.
+///
+/// Display: Layout
+/// Category: meta
+#[func]
+pub fn layout(
+ /// A function to call with the outer container's size. Its return value is
+ /// displayed in the document.
+ ///
+ /// The container's size is given as a [dictionary]($type/dictionary) with
+ /// the keys `width` and `height`.
+ ///
+ /// This function is called once for each time the content returned by
+ /// `layout` appears in the document. That makes it possible to generate
+ /// content that depends on the size of the container it is inside of.
+ func: Func,
+) -> Content {
+ LayoutElem::new(func).pack()
+}
+
+/// Executes a `layout` call.
+///
+/// Display: Layout
+/// Category: special
+#[element(Layout)]
+struct LayoutElem {
+ /// The function to call with the outer container's (or page's) size.
+ #[required]
+ func: Func,
+}
+
+impl Layout for LayoutElem {
+ #[tracing::instrument(name = "LayoutElem::layout", skip_all)]
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ // Gets the current region's base size, which will be the size of the
+ // outer container, or of the page if there is no such container.
+ let Size { x, y } = regions.base();
+ let result = self
+ .func()
+ .call_vt(vt, [dict! { "width" => x, "height" => y }])?
+ .display();
+ result.layout(vt, styles, regions)
+ }
+}
diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs
new file mode 100644
index 00000000..9a223b32
--- /dev/null
+++ b/crates/typst-library/src/meta/counter.rs
@@ -0,0 +1,683 @@
+use std::fmt::{self, Debug, Formatter, Write};
+use std::str::FromStr;
+
+use ecow::{eco_vec, EcoVec};
+use smallvec::{smallvec, SmallVec};
+use typst::eval::Tracer;
+use typst::model::DelayedErrors;
+
+use super::{FigureElem, HeadingElem, Numbering, NumberingPattern};
+use crate::layout::PageElem;
+use crate::math::EquationElem;
+use crate::prelude::*;
+
+/// Counts through pages, elements, and more.
+///
+/// With the counter function, you can access and modify counters for pages,
+/// headings, figures, and more. Moreover, you can define custom counters for
+/// other things you want to count.
+///
+/// ## Displaying a counter { #displaying }
+/// To display the current value of the heading counter, you call the `counter`
+/// function with the `key` set to `heading` and then call the `display` method
+/// on the counter. To see any output, you also have to enable heading
+/// [numbering]($func/heading.numbering).
+///
+/// The `display` method optionally takes an argument telling it how to format
+/// the counter. This can be a [numbering pattern or a
+/// function]($func/numbering).
+///
+/// ```example
+/// #set heading(numbering: "1.")
+///
+/// = Introduction
+/// Some text here.
+///
+/// = Background
+/// The current value is:
+/// #counter(heading).display()
+///
+/// Or in roman numerals:
+/// #counter(heading).display("I")
+/// ```
+///
+/// ## Modifying a counter { #modifying }
+/// To modify a counter, you can use the `step` and `update` methods:
+///
+/// - The `step` method increases the value of the counter by one. Because
+/// counters can have multiple levels (in the case of headings for sections,
+/// subsections, and so on), the `step` method optionally takes a `level`
+/// argument. If given, the counter steps at the given depth.
+///
+/// - The `update` method allows you to arbitrarily modify the counter. In its
+/// basic form, you give it an integer (or multiple for multiple levels). For
+/// more flexibility, you can instead also give it a function that gets the
+/// current value and returns a new value.
+///
+/// The heading counter is stepped before the heading is displayed, so
+/// `Analysis` gets the number seven even though the counter is at six after the
+/// second update.
+///
+/// ```example
+/// #set heading(numbering: "1.")
+///
+/// = Introduction
+/// #counter(heading).step()
+///
+/// = Background
+/// #counter(heading).update(3)
+/// #counter(heading).update(n => n * 2)
+///
+/// = Analysis
+/// Let's skip 7.1.
+/// #counter(heading).step(level: 2)
+///
+/// == Analysis
+/// Still at #counter(heading).display().
+/// ```
+///
+/// ## Custom counters { #custom-counters }
+/// To define your own counter, call the `counter` function with a string as a
+/// key. This key identifies the counter globally.
+///
+/// ```example
+/// #let mine = counter("mycounter")
+/// #mine.display() \
+/// #mine.step()
+/// #mine.display() \
+/// #mine.update(c => c * 3)
+/// #mine.display() \
+/// ```
+///
+/// ## How to step { #how-to-step }
+/// When you define and use a custom counter, in general, you should first step
+/// the counter and then display it. This way, the stepping behaviour of a
+/// counter can depend on the element it is stepped for. If you were writing a
+/// counter for, let's say, theorems, your theorem's definition would thus first
+/// include the counter step and only then display the counter and the theorem's
+/// contents.
+///
+/// ```example
+/// #let c = counter("theorem")
+/// #let theorem(it) = block[
+/// #c.step()
+/// *Theorem #c.display():* #it
+/// ]
+///
+/// #theorem[$1 = 1$]
+/// #theorem[$2 < 3$]
+/// ```
+///
+/// The rationale behind this is best explained on the example of the heading
+/// counter: An update to the heading counter depends on the heading's level.
+/// By stepping directly before the heading, we can correctly step from `1` to
+/// `1.1` when encountering a level 2 heading. If we were to step after the
+/// heading, we wouldn't know what to step to.
+///
+/// Because counters should always be stepped before the elements they count,
+/// they always start at zero. This way, they are at one for the first display
+/// (which happens after the first step).
+///
+/// ## Page counter { #page-counter }
+/// The page counter is special. It is automatically stepped at each pagebreak.
+/// But like other counters, you can also step it manually. For example, you
+/// could have Roman page numbers for your preface, then switch to Arabic page
+/// numbers for your main content and reset the page counter to one.
+///
+/// ```example
+/// >>> #set page(
+/// >>> height: 100pt,
+/// >>> margin: (bottom: 24pt, rest: 16pt),
+/// >>> )
+/// #set page(numbering: "(i)")
+///
+/// = Preface
+/// The preface is numbered with
+/// roman numerals.
+///
+/// #set page(numbering: "1 / 1")
+/// #counter(page).update(1)
+///
+/// = Main text
+/// Here, the counter is reset to one.
+/// We also display both the current
+/// page and total number of pages in
+/// Arabic numbers.
+/// ```
+///
+/// ## Time travel { #time-travel }
+/// Counters can travel through time! You can find out the final value of the
+/// counter before it is reached and even determine what the value was at any
+/// particular location in the document.
+///
+/// ```example
+/// #let mine = counter("mycounter")
+///
+/// = Values
+/// #locate(loc => {
+/// let start-val = mine.at(loc)
+/// let elements = query(<intro>, loc)
+/// let intro-val = mine.at(
+/// elements.first().location()
+/// )
+/// let final-val = mine.final(loc)
+/// [Starts as: #start-val \
+/// Value at intro is: #intro-val \
+/// Final value is: #final-val \ ]
+/// })
+///
+/// #mine.update(n => n + 3)
+///
+/// = Introduction <intro>
+/// #lorem(10)
+///
+/// #mine.step()
+/// #mine.step()
+/// ```
+///
+/// Let's dissect what happens in the example above:
+///
+/// - We call [`locate`]($func/locate) to get access to the current location in
+/// the document. We then pass this location to our counter's `at` method to
+/// get its value at the current location. The `at` method always returns an
+/// array because counters can have multiple levels. As the counter starts at
+/// one, the first value is thus `{(1,)}`.
+///
+/// - We now [`query`]($func/query) the document for all elements with the
+/// `{<intro>}` label. The result is an array from which we extract the first
+/// (and only) element's [location]($type/content.location). We then look up
+/// the value of the counter at that location. The first update to the counter
+/// sets it to `{1 + 3 = 4}`. At the introduction heading, the value is thus
+/// `{(4,)}`.
+///
+/// - Last but not least, we call the `final` method on the counter. It tells us
+/// what the counter's value will be at the end of the document. We also need
+/// to give it a location to prove that we are inside of a `locate` call, but
+/// which one doesn't matter. After the heading follow two calls to `step()`,
+/// so the final value is `{(6,)}`.
+///
+/// ## Other kinds of state { #other-state }
+/// The `counter` function is closely related to [state]($func/state) function.
+/// Read its documentation for more details on state management in Typst and
+/// why it doesn't just use normal variables for counters.
+///
+/// ## Methods
+/// ### display()
+/// Displays the value of the counter.
+///
+/// - numbering: string or function (positional)
+/// A [numbering pattern or a function]($func/numbering), which specifies how
+/// to display the counter. If given a function, that function receives each
+/// number of the counter as a separate argument. If the amount of numbers
+/// varies, e.g. for the heading argument, you can use an
+/// [argument sink]($type/arguments).
+///
+/// If this is omitted, displays the counter with the numbering style for the
+/// counted element or with the pattern `{"1.1"}` if no such style exists.
+///
+/// - both: boolean (named)
+/// If enabled, displays the current and final top-level count together. Both
+/// can be styled through a single numbering pattern. This is used by the page
+/// numbering property to display the current and total number of pages when a
+/// pattern like `{"1 / 1"}` is given.
+///
+/// - returns: content
+///
+/// ### step()
+/// Increases the value of the counter by one.
+///
+/// The update will be in effect at the position where the returned content is
+/// inserted into the document. If you don't put the output into the document,
+/// nothing happens! This would be the case, for example, if you write
+/// `{let _ = counter(page).step()}`. Counter updates are always applied in
+/// layout order and in that case, Typst wouldn't know when to step the counter.
+///
+/// - level: integer (named)
+/// The depth at which to step the counter. Defaults to `{1}`.
+///
+/// - returns: content
+///
+/// ### update()
+/// Updates the value of the counter.
+///
+/// Just like with `step`, the update only occurs if you put the resulting
+/// content into the document.
+///
+/// - value: integer or array or function (positional, required)
+/// If given an integer or array of integers, sets the counter to that value.
+/// If given a function, that function receives the previous counter value
+/// (with each number as a separate argument) and has to return the new
+/// value (integer or array).
+///
+/// - returns: content
+///
+/// ### at()
+/// Gets the value of the counter at the given location. Always returns an
+/// array of integers, even if the counter has just one number.
+///
+/// - location: location (positional, required)
+/// The location at which the counter value should be retrieved. A suitable
+/// location can be retrieved from [`locate`]($func/locate) or
+/// [`query`]($func/query).
+///
+/// - returns: array
+///
+/// ### final()
+/// Gets the value of the counter at the end of the document. Always returns an
+/// array of integers, even if the counter has just one number.
+///
+/// - location: location (positional, required)
+/// Can be any location. Why is it required then? Typst has to evaluate parts
+/// of your code multiple times to determine all counter values. By only
+/// allowing this method within [`locate`]($func/locate) calls, the amount of
+/// code that can depend on the method's result is reduced. If you could call
+/// `final` directly at the top level of a module, the evaluation of the whole
+/// module and its exports could depend on the counter's value.
+///
+/// - returns: array
+///
+/// Display: Counter
+/// Category: meta
+#[func]
+pub fn counter(
+ /// The key that identifies this counter.
+ ///
+ /// - If it is a string, creates a custom counter that is only affected by
+ /// manual updates,
+ /// - If this is a `{<label>}`, counts through all elements with that label,
+ /// - If this is an element function or selector, counts through its elements,
+ /// - If this is the [`page`]($func/page) function, counts through pages.
+ key: CounterKey,
+) -> Counter {
+ Counter::new(key)
+}
+
+/// Counts through pages, elements, and more.
+#[derive(Clone, PartialEq, Hash)]
+pub struct Counter(CounterKey);
+
+impl Counter {
+ /// Create a new counter from a key.
+ pub fn new(key: CounterKey) -> Self {
+ Self(key)
+ }
+
+ /// The counter for the given element.
+ pub fn of(func: ElemFunc) -> Self {
+ Self::new(CounterKey::Selector(Selector::Elem(func, None)))
+ }
+
+ /// Call a method on counter.
+ #[tracing::instrument(skip(vm))]
+ pub fn call_method(
+ self,
+ vm: &mut Vm,
+ method: &str,
+ mut args: Args,
+ span: Span,
+ ) -> SourceResult<Value> {
+ let value = match method {
+ "display" => self
+ .display(args.eat()?, args.named("both")?.unwrap_or(false))
+ .into_value(),
+ "step" => self
+ .update(CounterUpdate::Step(
+ args.named("level")?.unwrap_or(NonZeroUsize::ONE),
+ ))
+ .into_value(),
+ "update" => self.update(args.expect("value or function")?).into_value(),
+ "at" => self.at(&mut vm.vt, args.expect("location")?)?.into_value(),
+ "final" => self.final_(&mut vm.vt, args.expect("location")?)?.into_value(),
+ _ => bail!(span, "type counter has no method `{}`", method),
+ };
+ args.finish()?;
+ Ok(value)
+ }
+
+ /// Display the current value of the counter.
+ pub fn display(self, numbering: Option<Numbering>, both: bool) -> Content {
+ DisplayElem::new(self, numbering, both).pack()
+ }
+
+ /// Get the value of the state at the given location.
+ pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
+ let sequence = self.sequence(vt)?;
+ let offset = vt.introspector.query(&self.selector().before(location, true)).len();
+ let (mut state, page) = sequence[offset].clone();
+ if self.is_page() {
+ let delta = vt.introspector.page(location).get().saturating_sub(page.get());
+ state.step(NonZeroUsize::ONE, delta);
+ }
+
+ Ok(state)
+ }
+
+ /// Get the value of the state at the final location.
+ pub fn final_(&self, vt: &mut Vt, _: Location) -> SourceResult<CounterState> {
+ let sequence = self.sequence(vt)?;
+ let (mut state, page) = sequence.last().unwrap().clone();
+ if self.is_page() {
+ let delta = vt.introspector.pages().get().saturating_sub(page.get());
+ state.step(NonZeroUsize::ONE, delta);
+ }
+ Ok(state)
+ }
+
+ /// Get the current and final value of the state combined in one state.
+ pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
+ let sequence = self.sequence(vt)?;
+ let offset = vt
+ .introspector
+ .query(&Selector::before(self.selector(), location, true))
+ .len();
+ let (mut at_state, at_page) = sequence[offset].clone();
+ let (mut final_state, final_page) = sequence.last().unwrap().clone();
+ if self.is_page() {
+ let at_delta =
+ vt.introspector.page(location).get().saturating_sub(at_page.get());
+ at_state.step(NonZeroUsize::ONE, at_delta);
+ let final_delta =
+ vt.introspector.pages().get().saturating_sub(final_page.get());
+ final_state.step(NonZeroUsize::ONE, final_delta);
+ }
+ Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
+ }
+
+ /// Produce content that performs a state update.
+ pub fn update(self, update: CounterUpdate) -> Content {
+ UpdateElem::new(self, update).pack()
+ }
+
+ /// Produce the whole sequence of counter states.
+ ///
+ /// This has to happen just once for all counters, cutting down the number
+ /// of counter updates from quadratic to linear.
+ fn sequence(
+ &self,
+ vt: &mut Vt,
+ ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
+ self.sequence_impl(
+ vt.world,
+ vt.introspector,
+ vt.locator.track(),
+ TrackedMut::reborrow_mut(&mut vt.delayed),
+ TrackedMut::reborrow_mut(&mut vt.tracer),
+ )
+ }
+
+ /// Memoized implementation of `sequence`.
+ #[comemo::memoize]
+ fn sequence_impl(
+ &self,
+ world: Tracked<dyn World + '_>,
+ introspector: Tracked<Introspector>,
+ locator: Tracked<Locator>,
+ delayed: TrackedMut<DelayedErrors>,
+ tracer: TrackedMut<Tracer>,
+ ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
+ let mut locator = Locator::chained(locator);
+ let mut vt = Vt {
+ world,
+ introspector,
+ locator: &mut locator,
+ delayed,
+ tracer,
+ };
+ let mut state = CounterState(match &self.0 {
+ // special case, because pages always start at one.
+ CounterKey::Page => smallvec![1],
+ _ => smallvec![0],
+ });
+ let mut page = NonZeroUsize::ONE;
+ let mut stops = eco_vec![(state.clone(), page)];
+
+ for elem in introspector.query(&self.selector()) {
+ if self.is_page() {
+ let prev = page;
+ page = introspector.page(elem.location().unwrap());
+
+ let delta = page.get() - prev.get();
+ if delta > 0 {
+ state.step(NonZeroUsize::ONE, delta);
+ }
+ }
+
+ if let Some(update) = match elem.to::<UpdateElem>() {
+ Some(elem) => Some(elem.update()),
+ None => match elem.with::<dyn Count>() {
+ Some(countable) => countable.update(),
+ None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
+ },
+ } {
+ state.update(&mut vt, update)?;
+ }
+
+ stops.push((state.clone(), page));
+ }
+
+ Ok(stops)
+ }
+
+ /// The selector relevant for this counter's updates.
+ fn selector(&self) -> Selector {
+ let mut selector =
+ Selector::Elem(UpdateElem::func(), Some(dict! { "counter" => self.clone() }));
+
+ if let CounterKey::Selector(key) = &self.0 {
+ selector = Selector::Or(eco_vec![selector, key.clone()]);
+ }
+
+ selector
+ }
+
+ /// Whether this is the page counter.
+ fn is_page(&self) -> bool {
+ self.0 == CounterKey::Page
+ }
+}
+
+impl Debug for Counter {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("counter(")?;
+ self.0.fmt(f)?;
+ f.write_char(')')
+ }
+}
+
+cast! {
+ type Counter: "counter",
+}
+
+/// Identifies a counter.
+#[derive(Clone, PartialEq, Hash)]
+pub enum CounterKey {
+ /// The page counter.
+ Page,
+ /// Counts elements matching the given selectors. Only works for locatable
+ /// elements or labels.
+ Selector(Selector),
+ /// Counts through manual counters with the same key.
+ Str(Str),
+}
+
+cast! {
+ CounterKey,
+ v: Str => Self::Str(v),
+ label: Label => Self::Selector(Selector::Label(label)),
+ v: ElemFunc => {
+ if v == PageElem::func() {
+ Self::Page
+ } else {
+ Self::Selector(LocatableSelector::from_value(v.into_value())?.0)
+ }
+ },
+ selector: LocatableSelector => Self::Selector(selector.0),
+}
+
+impl Debug for CounterKey {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Page => f.pad("page"),
+ Self::Selector(selector) => selector.fmt(f),
+ Self::Str(str) => str.fmt(f),
+ }
+ }
+}
+
+/// An update to perform on a counter.
+#[derive(Clone, PartialEq, Hash)]
+pub enum CounterUpdate {
+ /// Set the counter to the specified state.
+ Set(CounterState),
+ /// Increase the number for the given level by one.
+ Step(NonZeroUsize),
+ /// Apply the given function to the counter's state.
+ Func(Func),
+}
+
+impl Debug for CounterUpdate {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad("..")
+ }
+}
+
+cast! {
+ type CounterUpdate: "counter update",
+ v: CounterState => Self::Set(v),
+ v: Func => Self::Func(v),
+}
+
+/// Elements that have special counting behaviour.
+pub trait Count {
+ /// Get the counter update for this element.
+ fn update(&self) -> Option<CounterUpdate>;
+}
+
+/// Counts through elements with different levels.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub struct CounterState(pub SmallVec<[usize; 3]>);
+
+impl CounterState {
+ /// Advance the counter and return the numbers for the given heading.
+ pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> {
+ match update {
+ CounterUpdate::Set(state) => *self = state,
+ CounterUpdate::Step(level) => self.step(level, 1),
+ CounterUpdate::Func(func) => {
+ *self = func.call_vt(vt, self.0.iter().copied())?.cast().at(func.span())?
+ }
+ }
+ Ok(())
+ }
+
+ /// Advance the number of the given level by the specified amount.
+ pub fn step(&mut self, level: NonZeroUsize, by: usize) {
+ let level = level.get();
+
+ if self.0.len() >= level {
+ self.0[level - 1] = self.0[level - 1].saturating_add(by);
+ self.0.truncate(level);
+ }
+
+ while self.0.len() < level {
+ self.0.push(1);
+ }
+ }
+
+ /// Get the first number of the state.
+ pub fn first(&self) -> usize {
+ self.0.first().copied().unwrap_or(1)
+ }
+
+ /// Display the counter state with a numbering.
+ pub fn display(&self, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> {
+ Ok(numbering.apply_vt(vt, &self.0)?.display())
+ }
+}
+
+cast! {
+ CounterState,
+ self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
+ num: usize => Self(smallvec![num]),
+ array: Array => Self(array
+ .into_iter()
+ .map(Value::cast)
+ .collect::<StrResult<_>>()?),
+}
+
+/// Executes a display of a state.
+///
+/// Display: State
+/// Category: special
+#[element(Locatable, Show)]
+struct DisplayElem {
+ /// The counter.
+ #[required]
+ counter: Counter,
+
+ /// The numbering to display the counter with.
+ #[required]
+ numbering: Option<Numbering>,
+
+ /// Whether to display both the current and final value.
+ #[required]
+ both: bool,
+}
+
+impl Show for DisplayElem {
+ #[tracing::instrument(name = "DisplayElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let location = self.0.location().unwrap();
+ let counter = self.counter();
+ let numbering = self
+ .numbering()
+ .or_else(|| {
+ let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else {
+ return None;
+ };
+
+ if func == HeadingElem::func() {
+ HeadingElem::numbering_in(styles)
+ } else if func == FigureElem::func() {
+ FigureElem::numbering_in(styles)
+ } else if func == EquationElem::func() {
+ EquationElem::numbering_in(styles)
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
+
+ let state = if self.both() {
+ counter.both(vt, location)?
+ } else {
+ counter.at(vt, location)?
+ };
+
+ state.display(vt, &numbering)
+ }))
+ }
+}
+
+/// Executes a display of a state.
+///
+/// Display: State
+/// Category: special
+#[element(Locatable, Show)]
+struct UpdateElem {
+ /// The counter.
+ #[required]
+ counter: Counter,
+
+ /// The update to perform on the counter.
+ #[required]
+ update: CounterUpdate,
+}
+
+impl Show for UpdateElem {
+ #[tracing::instrument(name = "UpdateElem::show", skip(self))]
+ fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(Content::empty())
+ }
+}
diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs
new file mode 100644
index 00000000..db036e0a
--- /dev/null
+++ b/crates/typst-library/src/meta/document.rs
@@ -0,0 +1,86 @@
+use crate::layout::{LayoutRoot, PageElem};
+use crate::prelude::*;
+
+/// The root element of a document and its metadata.
+///
+/// All documents are automatically wrapped in a `document` element. You cannot
+/// create a document element yourself. This function is only used with
+/// [set rules]($styling/#set-rules) to specify document metadata. Such a set
+/// rule must appear before any of the document's contents.
+///
+/// ```example
+/// #set document(title: "Hello")
+///
+/// This has no visible output, but
+/// embeds metadata into the PDF!
+/// ```
+///
+/// Note that metadata set with this function is not rendered within the
+/// document. Instead, it is embedded in the compiled PDF file.
+///
+/// Display: Document
+/// Category: meta
+#[element(Construct, LayoutRoot)]
+pub struct DocumentElem {
+ /// The document's title. This is often rendered as the title of the
+ /// PDF viewer window.
+ pub title: Option<EcoString>,
+
+ /// The document's authors.
+ pub author: Author,
+
+ /// The page runs.
+ #[internal]
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+impl Construct for DocumentElem {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ bail!(args.span, "can only be used in set rules")
+ }
+}
+
+impl LayoutRoot for DocumentElem {
+ /// Layout the document into a sequence of frames, one per page.
+ #[tracing::instrument(name = "DocumentElem::layout_root", skip_all)]
+ fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
+ tracing::info!("Document layout");
+
+ let mut pages = vec![];
+
+ for mut child in &self.children() {
+ let outer = styles;
+ let mut styles = styles;
+ if let Some((elem, local)) = child.to_styled() {
+ styles = outer.chain(local);
+ child = elem;
+ }
+
+ if let Some(page) = child.to::<PageElem>() {
+ let number = NonZeroUsize::ONE.saturating_add(pages.len());
+ let fragment = page.layout(vt, styles, number)?;
+ pages.extend(fragment);
+ } else {
+ bail!(child.span(), "unexpected document child");
+ }
+ }
+
+ Ok(Document {
+ pages,
+ title: self.title(styles),
+ author: self.author(styles).0,
+ })
+ }
+}
+
+/// A list of authors.
+#[derive(Debug, Default, Clone, Hash)]
+pub struct Author(Vec<EcoString>);
+
+cast! {
+ Author,
+ self => self.0.into_value(),
+ v: EcoString => Self(vec![v]),
+ v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
+}
diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs
new file mode 100644
index 00000000..0d218770
--- /dev/null
+++ b/crates/typst-library/src/meta/figure.rs
@@ -0,0 +1,351 @@
+use std::str::FromStr;
+
+use super::{
+ Count, Counter, CounterKey, CounterUpdate, LocalName, Numbering, NumberingPattern,
+};
+use crate::layout::{BlockElem, VElem};
+use crate::meta::{Outlinable, Refable, Supplement};
+use crate::prelude::*;
+use crate::text::TextElem;
+use crate::visualize::ImageElem;
+
+/// A figure with an optional caption.
+///
+/// Automatically detects its contents to select the correct counting track.
+/// For example, figures containing images will be numbered separately from
+/// figures containing tables.
+///
+/// ## Examples { #examples }
+/// The example below shows a basic figure with an image:
+/// ```example
+/// @glacier shows a glacier. Glaciers
+/// are complex systems.
+///
+/// #figure(
+/// image("glacier.jpg", width: 80%),
+/// caption: [A curious figure.],
+/// ) <glacier>
+/// ```
+///
+/// You can also insert [tables]($func/table) into figures to give them a
+/// caption. The figure will detect this and automatically use a separate
+/// counter.
+///
+/// ```example
+/// #figure(
+/// table(
+/// columns: 4,
+/// [t], [1], [2], [3],
+/// [y], [0.3s], [0.4s], [0.8s],
+/// ),
+/// caption: [Timing results],
+/// )
+/// ```
+///
+/// This behaviour can be overridden by explicitly specifying the figure's
+/// `kind`. All figures of the same kind share a common counter.
+///
+/// ## Modifying the appearance { #modifying-appearance }
+/// You can completely customize the look of your figures with a [show
+/// rule]($styling/#show-rules). In the example below, we show the figure's
+/// caption above its body and display its supplement and counter after the
+/// caption.
+///
+/// ```example
+/// #show figure: it => align(center)[
+/// #it.caption |
+/// #emph[
+/// #it.supplement
+/// #it.counter.display(it.numbering)
+/// ]
+/// #v(10pt, weak: true)
+/// #it.body
+/// ]
+///
+/// #figure(
+/// image("molecular.jpg", width: 80%),
+/// caption: [
+/// The molecular testing pipeline.
+/// ],
+/// )
+/// ```
+///
+/// If your figure is too large and its contents are breakable across pages
+/// (e.g. if it contains a large table), then you can make the figure breakable
+/// across pages as well by using `#show figure: set block(breakable: true)`
+/// (see the [block]($func/block) documentation for more information).
+///
+/// Display: Figure
+/// Category: meta
+#[element(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
+pub struct FigureElem {
+ /// The content of the figure. Often, an [image]($func/image).
+ #[required]
+ pub body: Content,
+
+ /// The figure's caption.
+ pub caption: Option<Content>,
+
+ /// The kind of the figure this is.
+ ///
+ /// If set to `{auto}`, the figure will try to automatically determine its
+ /// kind. All figures of the same kind share a common counter.
+ ///
+ /// Setting this to something other than `{auto}` will override the
+ /// automatic detection. This can be useful if
+ /// - you wish to create a custom figure type that is not an
+ /// [image]($func/image), a [table]($func/table) or [code]($func/raw),
+ /// - you want to force the figure to use a specific counter regardless of
+ /// its content.
+ ///
+ /// You can set the kind to be an element function or a string. If you set
+ /// it to an element function that is not supported by the figure, you will
+ /// need to manually specify the figure's supplement.
+ ///
+ /// ```example
+ /// #figure(
+ /// circle(radius: 10pt),
+ /// caption: [A curious atom.],
+ /// kind: "atom",
+ /// supplement: [Atom],
+ /// )
+ /// ```
+ #[default(Smart::Auto)]
+ pub kind: Smart<FigureKind>,
+
+ /// The figure's supplement.
+ ///
+ /// If set to `{auto}`, the figure will try to automatically determine the
+ /// correct supplement based on the `kind` and the active [text
+ /// language]($func/text.lang). If you are using a custom figure type, you
+ /// will need to manually specify the supplement.
+ ///
+ /// If a function is specified, it is passed the first descendant of the
+ /// specified `kind` (typically, the figure's body) and should return
+ /// content.
+ ///
+ /// ```example
+ /// #figure(
+ /// [The contents of my figure!],
+ /// caption: [My custom figure],
+ /// supplement: [Bar],
+ /// kind: "foo",
+ /// )
+ /// ```
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// How to number the figure. Accepts a
+ /// [numbering pattern or function]($func/numbering).
+ #[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
+ pub numbering: Option<Numbering>,
+
+ /// The vertical gap between the body and caption.
+ #[default(Em::new(0.65).into())]
+ pub gap: Length,
+
+ /// Whether the figure should appear in an [`outline`]($func/outline)
+ /// of figures.
+ #[default(true)]
+ pub outlined: bool,
+
+ /// Convenience field to get access to the counter for this figure.
+ ///
+ /// The counter only depends on the `kind`:
+ /// - For (tables)[$func/table]: `{counter(figure.where(kind: table))}`
+ /// - For (images)[$func/image]: `{counter(figure.where(kind: image))}`
+ /// - For a custom kind: `{counter(figure.where(kind: kind))}`
+ ///
+ /// These are the counters you'll need to modify if you want to skip a
+ /// number or reset the counter.
+ #[synthesized]
+ pub counter: Option<Counter>,
+}
+
+impl Synthesize for FigureElem {
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ let numbering = self.numbering(styles);
+
+ // Determine the figure's kind.
+ let kind = self.kind(styles).unwrap_or_else(|| {
+ self.body()
+ .query_first(Selector::can::<dyn Figurable>())
+ .cloned()
+ .map(|elem| FigureKind::Elem(elem.func()))
+ .unwrap_or_else(|| FigureKind::Elem(ImageElem::func()))
+ });
+
+ // Resolve the supplement.
+ let supplement = match self.supplement(styles) {
+ Smart::Auto => {
+ // Default to the local name for the kind, if available.
+ let name = match &kind {
+ FigureKind::Elem(func) => {
+ let empty = Content::new(*func);
+ empty.with::<dyn LocalName>().map(|c| {
+ TextElem::packed(c.local_name(
+ TextElem::lang_in(styles),
+ TextElem::region_in(styles),
+ ))
+ })
+ }
+ FigureKind::Name(_) => None,
+ };
+
+ if numbering.is_some() && name.is_none() {
+ bail!(self.span(), "please specify the figure's supplement")
+ }
+
+ name.unwrap_or_default()
+ }
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => {
+ // Resolve the supplement with the first descendant of the kind or
+ // just the body, if none was found.
+ let descendant = match kind {
+ FigureKind::Elem(func) => {
+ self.body().query_first(Selector::Elem(func, None)).cloned()
+ }
+ FigureKind::Name(_) => None,
+ };
+
+ let target = descendant.unwrap_or_else(|| self.body());
+ supplement.resolve(vt, [target])?
+ }
+ };
+
+ // Construct the figure's counter.
+ let counter = Counter::new(CounterKey::Selector(Selector::Elem(
+ Self::func(),
+ Some(dict! {
+ "kind" => kind.clone(),
+ }),
+ )));
+
+ self.push_caption(self.caption(styles));
+ self.push_kind(Smart::Custom(kind));
+ self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
+ self.push_numbering(numbering);
+ self.push_outlined(self.outlined(styles));
+ self.push_counter(Some(counter));
+
+ Ok(())
+ }
+}
+
+impl Show for FigureElem {
+ #[tracing::instrument(name = "FigureElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let mut realized = self.body();
+
+ // Build the caption, if any.
+ if let Some(caption) = self.full_caption(vt)? {
+ realized += VElem::weak(self.gap(styles).into()).pack();
+ realized += caption;
+ }
+
+ // Wrap the contents in a block.
+ Ok(BlockElem::new()
+ .with_body(Some(realized))
+ .pack()
+ .aligned(Axes::with_x(Some(Align::Center.into()))))
+ }
+}
+
+impl Finalize for FigureElem {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ // Allow breakable figures with `show figure: set block(breakable: true)`.
+ realized.styled(BlockElem::set_breakable(false))
+ }
+}
+
+impl Count for FigureElem {
+ fn update(&self) -> Option<CounterUpdate> {
+ // If the figure is numbered, step the counter by one.
+ // This steps the `counter(figure)` which is global to all numbered figures.
+ self.numbering(StyleChain::default())
+ .is_some()
+ .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
+ }
+}
+
+impl Refable for FigureElem {
+ fn supplement(&self) -> Content {
+ // After synthesis, this should always be custom content.
+ match self.supplement(StyleChain::default()) {
+ Smart::Custom(Some(Supplement::Content(content))) => content,
+ _ => Content::empty(),
+ }
+ }
+
+ fn counter(&self) -> Counter {
+ self.counter().unwrap_or_else(|| Counter::of(Self::func()))
+ }
+
+ fn numbering(&self) -> Option<Numbering> {
+ self.numbering(StyleChain::default())
+ }
+}
+
+impl Outlinable for FigureElem {
+ fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
+ if !self.outlined(StyleChain::default()) {
+ return Ok(None);
+ }
+
+ self.full_caption(vt)
+ }
+}
+
+impl FigureElem {
+ /// Builds the full caption for the figure (with supplement and numbering).
+ pub fn full_caption(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
+ let Some(mut caption) = self.caption(StyleChain::default()) else {
+ return Ok(None);
+ };
+
+ if let (
+ Smart::Custom(Some(Supplement::Content(mut supplement))),
+ Some(counter),
+ Some(numbering),
+ ) = (
+ self.supplement(StyleChain::default()),
+ self.counter(),
+ self.numbering(StyleChain::default()),
+ ) {
+ let loc = self.0.location().unwrap();
+ let numbers = counter.at(vt, loc)?.display(vt, &numbering)?;
+
+ if !supplement.is_empty() {
+ supplement += TextElem::packed("\u{a0}");
+ }
+
+ caption = supplement + numbers + TextElem::packed(": ") + caption;
+ }
+
+ Ok(Some(caption))
+ }
+}
+
+/// The `kind` parameter of a [`FigureElem`].
+#[derive(Debug, Clone)]
+pub enum FigureKind {
+ /// The kind is an element function.
+ Elem(ElemFunc),
+ /// The kind is a name.
+ Name(EcoString),
+}
+
+cast! {
+ FigureKind,
+ self => match self {
+ Self::Elem(v) => v.into_value(),
+ Self::Name(v) => v.into_value(),
+ },
+ v: ElemFunc => Self::Elem(v),
+ v: EcoString => Self::Name(v),
+}
+
+/// An element that can be auto-detected in a figure.
+///
+/// This trait is used to determine the type of a figure.
+pub trait Figurable: LocalName {}
diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs
new file mode 100644
index 00000000..31ec9fe9
--- /dev/null
+++ b/crates/typst-library/src/meta/footnote.rs
@@ -0,0 +1,299 @@
+use comemo::Prehashed;
+use std::str::FromStr;
+
+use super::{Counter, Numbering, NumberingPattern};
+use crate::layout::{HElem, ParElem};
+use crate::meta::{Count, CounterUpdate};
+use crate::prelude::*;
+use crate::text::{SuperElem, TextElem, TextSize};
+use crate::visualize::LineElem;
+
+/// The body of a footnote can be either some content or a label referencing
+/// another footnote.
+#[derive(Debug)]
+pub enum FootnoteBody {
+ Content(Content),
+ Reference(Label),
+}
+
+cast! {
+ FootnoteBody,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Reference(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Label => Self::Reference(v),
+}
+
+/// A footnote.
+///
+/// Includes additional remarks and references on the same page with footnotes.
+/// A footnote will insert a superscript number that links to the note at the
+/// bottom of the page. Notes are numbered sequentially throughout your document
+/// and can break across multiple pages.
+///
+/// To customize the appearance of the entry in the footnote listing, see
+/// [`footnote.entry`]($func/footnote.entry). The footnote itself is realized as
+/// a normal superscript, so you can use a set rule on the
+/// [`super`]($func/super) function to customize it.
+///
+/// ## Example { #example }
+/// ```example
+/// Check the docs for more details.
+/// #footnote[https://typst.app/docs]
+/// ```
+///
+/// The footnote automatically attaches itself to the preceding word, even if
+/// there is a space before it in the markup. To force space, you can use the
+/// string `[#" "]` or explicit [horizontal spacing]($func/h).
+///
+/// By giving a label to a footnote, you can have multiple references to it.
+///
+/// ```example
+/// You can edit Typst documents online.
+/// #footnote[https://typst.app/app] <fn>
+/// Checkout Typst's website. @fn
+/// And the online app. #footnote(<fn>)
+/// ```
+///
+/// _Note:_ Set and show rules in the scope where `footnote` is called may not
+/// apply to the footnote's content. See [here][issue] more information.
+///
+/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
+///
+/// Display: Footnote
+/// Category: meta
+#[element(Locatable, Synthesize, Show, Count)]
+#[scope(
+ scope.define("entry", FootnoteEntry::func());
+ scope
+)]
+pub struct FootnoteElem {
+ /// How to number footnotes.
+ ///
+ /// By default, the footnote numbering continues throughout your document.
+ /// If you prefer per-page footnote numbering, you can reset the footnote
+ /// [counter]($func/counter) in the page [header]($func/page.header). In the
+ /// future, there might be a simpler way to achieve this.
+ ///
+ /// ```example
+ /// #set footnote(numbering: "*")
+ ///
+ /// Footnotes:
+ /// #footnote[Star],
+ /// #footnote[Dagger]
+ /// ```
+ #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
+ pub numbering: Numbering,
+
+ /// The content to put into the footnote. Can also be the label of another
+ /// footnote this one should point to.
+ #[required]
+ pub body: FootnoteBody,
+}
+
+impl FootnoteElem {
+ /// Creates a new footnote that the passed content as its body.
+ pub fn with_content(content: Content) -> Self {
+ Self::new(FootnoteBody::Content(content))
+ }
+
+ /// Creates a new footnote referencing the footnote with the specified label.
+ pub fn with_label(label: Label) -> Self {
+ Self::new(FootnoteBody::Reference(label))
+ }
+
+ /// Tests if this footnote is a reference to another footnote.
+ pub fn is_ref(&self) -> bool {
+ matches!(self.body(), FootnoteBody::Reference(_))
+ }
+
+ /// Returns the content of the body of this footnote if it is not a ref.
+ pub fn body_content(&self) -> Option<Content> {
+ match self.body() {
+ FootnoteBody::Content(content) => Some(content),
+ _ => None,
+ }
+ }
+
+ /// Returns the location of the definition of this footnote.
+ pub fn declaration_location(&self, vt: &Vt) -> StrResult<Location> {
+ match self.body() {
+ FootnoteBody::Reference(label) => {
+ let element: Prehashed<Content> = vt.introspector.query_label(&label)?;
+ let footnote = element
+ .to::<FootnoteElem>()
+ .ok_or("referenced element should be a footnote")?;
+ footnote.declaration_location(vt)
+ }
+ _ => Ok(self.0.location().unwrap()),
+ }
+ }
+}
+
+impl Synthesize for FootnoteElem {
+ fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_numbering(self.numbering(styles));
+ Ok(())
+ }
+}
+
+impl Show for FootnoteElem {
+ #[tracing::instrument(name = "FootnoteElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let loc = self.declaration_location(vt).at(self.span())?;
+ let numbering = self.numbering(styles);
+ let counter = Counter::of(Self::func());
+ let num = counter.at(vt, loc)?.display(vt, &numbering)?;
+ let sup = SuperElem::new(num).pack();
+ let hole = HElem::new(Abs::zero().into()).with_weak(true).pack();
+ let loc = loc.variant(1);
+ Ok(hole + sup.linked(Destination::Location(loc)))
+ }))
+ }
+}
+
+impl Count for FootnoteElem {
+ fn update(&self) -> Option<CounterUpdate> {
+ (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
+ }
+}
+
+/// An entry in a footnote list.
+///
+/// This function is not intended to be called directly. Instead, it is used
+/// in set and show rules to customize footnote listings.
+///
+/// ## Example { #example }
+/// ```example
+/// #show footnote.entry: set text(red)
+///
+/// My footnote listing
+/// #footnote[It's down here]
+/// has red text!
+/// ```
+///
+/// Display: Footnote Entry
+/// Category: meta
+#[element(Show, Finalize)]
+pub struct FootnoteEntry {
+ /// The footnote for this entry. It's location can be used to determine
+ /// the footnote counter state.
+ ///
+ /// ```example
+ /// #show footnote.entry: it => {
+ /// let loc = it.note.location()
+ /// numbering(
+ /// "1: ",
+ /// ..counter(footnote).at(loc),
+ /// )
+ /// it.note.body
+ /// }
+ ///
+ /// Customized #footnote[Hello]
+ /// listing #footnote[World! 🌏]
+ /// ```
+ #[required]
+ pub note: FootnoteElem,
+
+ /// The separator between the document body and the footnote listing.
+ ///
+ /// ```example
+ /// #set footnote.entry(
+ /// separator: repeat[.]
+ /// )
+ ///
+ /// Testing a different separator.
+ /// #footnote[
+ /// Unconventional, but maybe
+ /// not that bad?
+ /// ]
+ /// ```
+ #[default(
+ LineElem::new()
+ .with_length(Ratio::new(0.3).into())
+ .with_stroke(PartialStroke {
+ thickness: Smart::Custom(Abs::pt(0.5).into()),
+ ..Default::default()
+ })
+ .pack()
+ )]
+ pub separator: Content,
+
+ /// The amount of clearance between the document body and the separator.
+ ///
+ /// ```example
+ /// #set footnote.entry(clearance: 3em)
+ ///
+ /// Footnotes also need ...
+ /// #footnote[
+ /// ... some space to breathe.
+ /// ]
+ /// ```
+ #[default(Em::new(1.0).into())]
+ #[resolve]
+ pub clearance: Length,
+
+ /// The gap between footnote entries.
+ ///
+ /// ```example
+ /// #set footnote.entry(gap: 0.8em)
+ ///
+ /// Footnotes:
+ /// #footnote[Spaced],
+ /// #footnote[Apart]
+ /// ```
+ #[default(Em::new(0.5).into())]
+ #[resolve]
+ pub gap: Length,
+
+ /// The indent of each footnote entry.
+ ///
+ /// ```example
+ /// #set footnote.entry(indent: 0em)
+ ///
+ /// Footnotes:
+ /// #footnote[No],
+ /// #footnote[Indent]
+ /// ```
+ #[default(Em::new(1.0).into())]
+ pub indent: Length,
+}
+
+impl Show for FootnoteEntry {
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let note = self.note();
+ let number_gap = Em::new(0.05);
+ let numbering = note.numbering(StyleChain::default());
+ let counter = Counter::of(FootnoteElem::func());
+ let loc = note.0.location().unwrap();
+ let num = counter.at(vt, loc)?.display(vt, &numbering)?;
+ let sup = SuperElem::new(num)
+ .pack()
+ .linked(Destination::Location(loc))
+ .backlinked(loc.variant(1));
+ Ok(Content::sequence([
+ HElem::new(self.indent(styles).into()).pack(),
+ sup,
+ HElem::new(number_gap.into()).with_weak(true).pack(),
+ note.body_content().unwrap(),
+ ]))
+ }
+}
+
+impl Finalize for FootnoteEntry {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ let text_size = Em::new(0.85);
+ let leading = Em::new(0.5);
+ realized
+ .styled(ParElem::set_leading(leading.into()))
+ .styled(TextElem::set_size(TextSize(text_size.into())))
+ }
+}
+
+cast! {
+ FootnoteElem,
+ v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::with_content(v.clone())),
+}
diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs
new file mode 100644
index 00000000..d70bc0a9
--- /dev/null
+++ b/crates/typst-library/src/meta/heading.rs
@@ -0,0 +1,239 @@
+use typst::font::FontWeight;
+use typst::util::option_eq;
+
+use super::{Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable};
+use crate::layout::{BlockElem, HElem, VElem};
+use crate::meta::{Count, Supplement};
+use crate::prelude::*;
+use crate::text::{SpaceElem, TextElem, TextSize};
+
+/// A section heading.
+///
+/// With headings, you can structure your document into sections. Each heading
+/// has a _level,_ which starts at one and is unbounded upwards. This level
+/// indicates the logical role of the following content (section, subsection,
+/// etc.) A top-level heading indicates a top-level section of the document
+/// (not the document's title).
+///
+/// Typst can automatically number your headings for you. To enable numbering,
+/// specify how you want your headings to be numbered with a
+/// [numbering pattern or function]($func/numbering).
+///
+/// Independently from the numbering, Typst can also automatically generate an
+/// [outline]($func/outline) of all headings for you. To exclude one or more
+/// headings from this outline, you can set the `outlined` parameter to
+/// `{false}`.
+///
+/// ## Example { #example }
+/// ```example
+/// #set heading(numbering: "1.a)")
+///
+/// = Introduction
+/// In recent years, ...
+///
+/// == Preliminaries
+/// To start, ...
+/// ```
+///
+/// ## Syntax { #syntax }
+/// Headings have dedicated syntax: They can be created by starting a line with
+/// one or multiple equals signs, followed by a space. The number of equals
+/// signs determines the heading's logical nesting depth.
+///
+/// Display: Heading
+/// Category: meta
+#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)]
+pub struct HeadingElem {
+ /// The logical nesting depth of the heading, starting from one.
+ #[default(NonZeroUsize::ONE)]
+ pub level: NonZeroUsize,
+
+ /// How to number the heading. Accepts a
+ /// [numbering pattern or function]($func/numbering).
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.a.")
+ ///
+ /// = A section
+ /// == A subsection
+ /// === A sub-subsection
+ /// ```
+ pub numbering: Option<Numbering>,
+
+ /// A supplement for the heading.
+ ///
+ /// For references to headings, this is added before the referenced number.
+ ///
+ /// If a function is specified, it is passed the referenced heading and
+ /// should return content.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.", supplement: [Chapter])
+ ///
+ /// = Introduction <intro>
+ /// In @intro, we see how to turn
+ /// Sections into Chapters. And
+ /// in @intro[Part], it is done
+ /// manually.
+ /// ```
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// Whether the heading should appear in the outline.
+ ///
+ /// ```example
+ /// #outline()
+ ///
+ /// #heading[Normal]
+ /// This is a normal heading.
+ ///
+ /// #heading(outlined: false)[Hidden]
+ /// This heading does not appear
+ /// in the outline.
+ /// ```
+ #[default(true)]
+ pub outlined: bool,
+
+ /// The heading's title.
+ #[required]
+ pub body: Content,
+}
+
+impl Synthesize for HeadingElem {
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ // Resolve the supplement.
+ let supplement = match self.supplement(styles) {
+ Smart::Auto => TextElem::packed(self.local_name_in(styles)),
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?,
+ };
+
+ self.push_level(self.level(styles));
+ self.push_numbering(self.numbering(styles));
+ self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
+ self.push_outlined(self.outlined(styles));
+
+ Ok(())
+ }
+}
+
+impl Show for HeadingElem {
+ #[tracing::instrument(name = "HeadingElem::show", skip_all)]
+ fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let mut realized = self.body();
+ if let Some(numbering) = self.numbering(styles) {
+ realized = Counter::of(Self::func())
+ .display(Some(numbering), false)
+ .spanned(self.span())
+ + HElem::new(Em::new(0.3).into()).with_weak(true).pack()
+ + realized;
+ }
+ Ok(BlockElem::new().with_body(Some(realized)).pack())
+ }
+}
+
+impl Finalize for HeadingElem {
+ fn finalize(&self, realized: Content, styles: StyleChain) -> Content {
+ let level = self.level(styles).get();
+ let scale = match level {
+ 1 => 1.4,
+ 2 => 1.2,
+ _ => 1.0,
+ };
+
+ let size = Em::new(scale);
+ let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
+ let below = Em::new(0.75) / scale;
+
+ let mut styles = Styles::new();
+ styles.set(TextElem::set_size(TextSize(size.into())));
+ styles.set(TextElem::set_weight(FontWeight::BOLD));
+ styles.set(BlockElem::set_above(VElem::block_around(above.into())));
+ styles.set(BlockElem::set_below(VElem::block_around(below.into())));
+ styles.set(BlockElem::set_sticky(true));
+ realized.styled_with_map(styles)
+ }
+}
+
+impl Count for HeadingElem {
+ fn update(&self) -> Option<CounterUpdate> {
+ self.numbering(StyleChain::default())
+ .is_some()
+ .then(|| CounterUpdate::Step(self.level(StyleChain::default())))
+ }
+}
+
+cast! {
+ HeadingElem,
+ v: Content => v.to::<Self>().ok_or("expected heading")?.clone(),
+}
+
+impl Refable for HeadingElem {
+ fn supplement(&self) -> Content {
+ // After synthesis, this should always be custom content.
+ match self.supplement(StyleChain::default()) {
+ Smart::Custom(Some(Supplement::Content(content))) => content,
+ _ => Content::empty(),
+ }
+ }
+
+ fn counter(&self) -> Counter {
+ Counter::of(Self::func())
+ }
+
+ fn numbering(&self) -> Option<Numbering> {
+ self.numbering(StyleChain::default())
+ }
+}
+
+impl Outlinable for HeadingElem {
+ fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
+ if !self.outlined(StyleChain::default()) {
+ return Ok(None);
+ }
+
+ let mut content = self.body();
+ if let Some(numbering) = self.numbering(StyleChain::default()) {
+ let numbers = Counter::of(Self::func())
+ .at(vt, self.0.location().unwrap())?
+ .display(vt, &numbering)?;
+ content = numbers + SpaceElem::new().pack() + content;
+ };
+
+ Ok(Some(content))
+ }
+
+ fn level(&self) -> NonZeroUsize {
+ self.level(StyleChain::default())
+ }
+}
+
+impl LocalName for HeadingElem {
+ fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
+ match lang {
+ Lang::ALBANIAN => "Kapitull",
+ Lang::ARABIC => "الفصل",
+ Lang::BOKMÅL => "Kapittel",
+ Lang::CHINESE if option_eq(region, "TW") => "小節",
+ Lang::CHINESE => "小节",
+ Lang::CZECH => "Kapitola",
+ Lang::DANISH => "Afsnit",
+ Lang::DUTCH => "Hoofdstuk",
+ Lang::FILIPINO => "Seksyon",
+ Lang::FRENCH => "Chapitre",
+ Lang::GERMAN => "Abschnitt",
+ Lang::ITALIAN => "Sezione",
+ Lang::NYNORSK => "Kapittel",
+ Lang::POLISH => "Sekcja",
+ Lang::PORTUGUESE if option_eq(region, "PT") => "Secção",
+ Lang::PORTUGUESE => "Seção",
+ Lang::RUSSIAN => "Раздел",
+ Lang::SLOVENIAN => "Poglavje",
+ Lang::SPANISH => "Sección",
+ Lang::SWEDISH => "Kapitel",
+ Lang::TURKISH => "Bölüm",
+ Lang::UKRAINIAN => "Розділ",
+ Lang::VIETNAMESE => "Phần", // TODO: This may be wrong.
+ Lang::ENGLISH | _ => "Section",
+ }
+ }
+}
diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs
new file mode 100644
index 00000000..2a53b84f
--- /dev/null
+++ b/crates/typst-library/src/meta/link.rs
@@ -0,0 +1,137 @@
+use crate::prelude::*;
+use crate::text::{Hyphenate, TextElem};
+
+/// Links to a URL or a location in the document.
+///
+/// By default, links are not styled any different from normal text. However,
+/// you can easily apply a style of your choice with a show rule.
+///
+/// ## Example { #example }
+/// ```example
+/// #show link: underline
+///
+/// https://example.com \
+///
+/// #link("https://example.com") \
+/// #link("https://example.com")[
+/// See example.com
+/// ]
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax: Text that starts with `http://` or
+/// `https://` is automatically turned into a link.
+///
+/// Display: Link
+/// Category: meta
+#[element(Show)]
+pub struct LinkElem {
+ /// The destination the link points to.
+ ///
+ /// - To link to web pages, `dest` should be a valid URL string. If the URL
+ /// is in the `mailto:` or `tel:` scheme and the `body` parameter is
+ /// omitted, the email address or phone number will be the link's body,
+ /// without the scheme.
+ ///
+ /// - To link to another part of the document, `dest` can take one of three
+ /// forms:
+ /// - A [label]($func/label) attached to an element. If you also want
+ /// automatic text for the link based on the element, consider using
+ /// a [reference]($func/ref) instead.
+ ///
+ /// - A [location]($func/locate) resulting from a [`locate`]($func/locate)
+ /// call or [`query`]($func/query).
+ ///
+ /// - A dictionary with a `page` key of type [integer]($type/integer) and
+ /// `x` and `y` coordinates of type [length]($type/length). Pages are
+ /// counted from one, and the coordinates are relative to the page's top
+ /// left corner.
+ ///
+ /// ```example
+ /// = Introduction <intro>
+ /// #link("mailto:hello@typst.app") \
+ /// #link(<intro>)[Go to intro] \
+ /// #link((page: 1, x: 0pt, y: 0pt))[
+ /// Go to top
+ /// ]
+ /// ```
+ #[required]
+ #[parse(
+ let dest = args.expect::<LinkTarget>("destination")?;
+ dest.clone()
+ )]
+ pub dest: LinkTarget,
+
+ /// The content that should become a link.
+ ///
+ /// If `dest` is an URL string, the parameter can be omitted. In this case,
+ /// the URL will be shown as the link.
+ #[required]
+ #[parse(match &dest {
+ LinkTarget::Dest(Destination::Url(url)) => match args.eat()? {
+ Some(body) => body,
+ None => body_from_url(url),
+ },
+ _ => args.expect("body")?,
+ })]
+ pub body: Content,
+}
+
+impl LinkElem {
+ /// Create a link element from a URL with its bare text.
+ pub fn from_url(url: EcoString) -> Self {
+ let body = body_from_url(&url);
+ Self::new(LinkTarget::Dest(Destination::Url(url)), body)
+ }
+}
+
+impl Show for LinkElem {
+ #[tracing::instrument(name = "LinkElem::show", skip(self, vt))]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ let body = self.body();
+ let linked = match self.dest() {
+ LinkTarget::Dest(dest) => body.linked(dest),
+ LinkTarget::Label(label) => vt
+ .delayed(|vt| {
+ let elem = vt.introspector.query_label(&label).at(self.span())?;
+ let dest = Destination::Location(elem.location().unwrap());
+ Ok(Some(body.clone().linked(dest)))
+ })
+ .unwrap_or(body),
+ };
+
+ Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))))
+ }
+}
+
+fn body_from_url(url: &EcoString) -> Content {
+ let mut text = url.as_str();
+ for prefix in ["mailto:", "tel:"] {
+ text = text.trim_start_matches(prefix);
+ }
+ let shorter = text.len() < url.len();
+ TextElem::packed(if shorter { text.into() } else { url.clone() })
+}
+
+/// A target where a link can go.
+#[derive(Debug, Clone)]
+pub enum LinkTarget {
+ Dest(Destination),
+ Label(Label),
+}
+
+cast! {
+ LinkTarget,
+ self => match self {
+ Self::Dest(v) => v.into_value(),
+ Self::Label(v) => v.into_value(),
+ },
+ v: Destination => Self::Dest(v),
+ v: Label => Self::Label(v),
+}
+
+impl From<Destination> for LinkTarget {
+ fn from(dest: Destination) -> Self {
+ Self::Dest(dest)
+ }
+}
diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs
new file mode 100644
index 00000000..dcac6379
--- /dev/null
+++ b/crates/typst-library/src/meta/mod.rs
@@ -0,0 +1,64 @@
+//! Interaction between document parts.
+
+mod bibliography;
+mod context;
+mod counter;
+mod document;
+mod figure;
+mod footnote;
+mod heading;
+mod link;
+mod numbering;
+mod outline;
+mod query;
+mod reference;
+mod state;
+
+pub use self::bibliography::*;
+pub use self::context::*;
+pub use self::counter::*;
+pub use self::document::*;
+pub use self::figure::*;
+pub use self::footnote::*;
+pub use self::heading::*;
+pub use self::link::*;
+pub use self::numbering::*;
+pub use self::outline::*;
+pub use self::query::*;
+pub use self::reference::*;
+pub use self::state::*;
+
+use crate::prelude::*;
+use crate::text::TextElem;
+
+/// Hook up all meta definitions.
+pub(super) fn define(global: &mut Scope) {
+ global.define("document", DocumentElem::func());
+ global.define("ref", RefElem::func());
+ global.define("link", LinkElem::func());
+ global.define("outline", OutlineElem::func());
+ global.define("heading", HeadingElem::func());
+ global.define("figure", FigureElem::func());
+ global.define("footnote", FootnoteElem::func());
+ global.define("cite", CiteElem::func());
+ global.define("bibliography", BibliographyElem::func());
+ global.define("locate", locate_func());
+ global.define("style", style_func());
+ global.define("layout", layout_func());
+ global.define("counter", counter_func());
+ global.define("numbering", numbering_func());
+ global.define("state", state_func());
+ global.define("query", query_func());
+ global.define("selector", selector_func());
+}
+
+/// The named with which an element is referenced.
+pub trait LocalName {
+ /// Get the name in the given language and (optionally) region.
+ fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str;
+
+ /// Resolve the local name with a style chain.
+ fn local_name_in(&self, styles: StyleChain) -> &'static str {
+ self.local_name(TextElem::lang_in(styles), TextElem::region_in(styles))
+ }
+}
diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs
new file mode 100644
index 00000000..8698f7b9
--- /dev/null
+++ b/crates/typst-library/src/meta/numbering.rs
@@ -0,0 +1,525 @@
+use std::str::FromStr;
+
+use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
+use ecow::EcoVec;
+
+use crate::prelude::*;
+use crate::text::Case;
+
+/// Applies a numbering to a sequence of numbers.
+///
+/// A numbering defines how a sequence of numbers should be displayed as
+/// content. It is defined either through a pattern string or an arbitrary
+/// function.
+///
+/// A numbering pattern consists of counting symbols, for which the actual
+/// number is substituted, their prefixes, and one suffix. The prefixes and the
+/// suffix are repeated as-is.
+///
+/// ## Example { #example }
+/// ```example
+/// #numbering("1.1)", 1, 2, 3) \
+/// #numbering("1.a.i", 1, 2) \
+/// #numbering("I – 1", 12, 2) \
+/// #numbering(
+/// (..nums) => nums
+/// .pos()
+/// .map(str)
+/// .join(".") + ")",
+/// 1, 2, 3,
+/// )
+/// ```
+///
+/// Display: Numbering
+/// Category: meta
+#[func]
+pub fn numbering(
+ /// Defines how the numbering works.
+ ///
+ /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`,
+ /// `ㄱ`, and `*`. They are replaced by the number in the sequence, in the
+ /// given case.
+ ///
+ /// The `*` character means that symbols should be used to count, in the
+ /// order of `*`, `†`, `‡`, `§`, `¶`, and `‖`. If there are more than six
+ /// items, the number is represented using multiple symbols.
+ ///
+ /// **Suffixes** are all characters after the last counting symbol. They are
+ /// repeated as-is at the end of any rendered number.
+ ///
+ /// **Prefixes** are all characters that are neither counting symbols nor
+ /// suffixes. They are repeated as-is at in front of their rendered
+ /// equivalent of their counting symbol.
+ ///
+ /// This parameter can also be an arbitrary function that gets each number
+ /// as an individual argument. When given a function, the `numbering`
+ /// function just forwards the arguments to that function. While this is not
+ /// particularly useful in itself, it means that you can just give arbitrary
+ /// numberings to the `numbering` function without caring whether they are
+ /// defined as a pattern or function.
+ numbering: Numbering,
+ /// The numbers to apply the numbering to. Must be positive.
+ ///
+ /// If `numbering` is a pattern and more numbers than counting symbols are
+ /// given, the last counting symbol with its prefix is repeated.
+ #[variadic]
+ numbers: Vec<usize>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ numbering.apply_vm(vm, &numbers)
+}
+
+/// How to number a sequence of things.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Numbering {
+ /// A pattern with prefix, numbering, lower / upper case and suffix.
+ Pattern(NumberingPattern),
+ /// A closure mapping from an item's number to content.
+ Func(Func),
+}
+
+impl Numbering {
+ /// Apply the pattern to the given numbers.
+ pub fn apply_vm(&self, vm: &mut Vm, numbers: &[usize]) -> SourceResult<Value> {
+ Ok(match self {
+ Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
+ Self::Func(func) => {
+ let args = Args::new(func.span(), numbers.iter().copied());
+ func.call_vm(vm, args)?
+ }
+ })
+ }
+
+ /// Apply the pattern to the given numbers.
+ pub fn apply_vt(&self, vt: &mut Vt, numbers: &[usize]) -> SourceResult<Value> {
+ Ok(match self {
+ Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
+ Self::Func(func) => func.call_vt(vt, numbers.iter().copied())?,
+ })
+ }
+
+ /// Trim the prefix suffix if this is a pattern.
+ pub fn trimmed(mut self) -> Self {
+ if let Self::Pattern(pattern) = &mut self {
+ pattern.trimmed = true;
+ }
+ self
+ }
+}
+
+impl From<NumberingPattern> for Numbering {
+ fn from(pattern: NumberingPattern) -> Self {
+ Self::Pattern(pattern)
+ }
+}
+
+cast! {
+ Numbering,
+ self => match self {
+ Self::Pattern(pattern) => pattern.into_value(),
+ Self::Func(func) => func.into_value(),
+ },
+ v: NumberingPattern => Self::Pattern(v),
+ v: Func => Self::Func(v),
+}
+
+/// How to turn a number into text.
+///
+/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`,
+/// `I`, `い`, `イ`, `א`, `가`, `ㄱ`, or `*`, and then a suffix.
+///
+/// Examples of valid patterns:
+/// - `1)`
+/// - `a.`
+/// - `(I)`
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct NumberingPattern {
+ pieces: EcoVec<(EcoString, NumberingKind, Case)>,
+ suffix: EcoString,
+ trimmed: bool,
+}
+
+impl NumberingPattern {
+ /// Apply the pattern to the given number.
+ pub fn apply(&self, numbers: &[usize]) -> EcoString {
+ let mut fmt = EcoString::new();
+ let mut numbers = numbers.iter();
+
+ for (i, ((prefix, kind, case), &n)) in
+ self.pieces.iter().zip(&mut numbers).enumerate()
+ {
+ if i > 0 || !self.trimmed {
+ fmt.push_str(prefix);
+ }
+ fmt.push_str(&kind.apply(n, *case));
+ }
+
+ for ((prefix, kind, case), &n) in
+ self.pieces.last().into_iter().cycle().zip(numbers)
+ {
+ if prefix.is_empty() {
+ fmt.push_str(&self.suffix);
+ } else {
+ fmt.push_str(prefix);
+ }
+ fmt.push_str(&kind.apply(n, *case));
+ }
+
+ if !self.trimmed {
+ fmt.push_str(&self.suffix);
+ }
+
+ fmt
+ }
+
+ /// Apply only the k-th segment of the pattern to a number.
+ pub fn apply_kth(&self, k: usize, number: usize) -> EcoString {
+ let mut fmt = EcoString::new();
+ if let Some((prefix, _, _)) = self.pieces.first() {
+ fmt.push_str(prefix);
+ }
+ if let Some((_, kind, case)) = self
+ .pieces
+ .iter()
+ .chain(self.pieces.last().into_iter().cycle())
+ .nth(k)
+ {
+ fmt.push_str(&kind.apply(number, *case));
+ }
+ fmt.push_str(&self.suffix);
+ fmt
+ }
+
+ /// How many counting symbols this pattern has.
+ pub fn pieces(&self) -> usize {
+ self.pieces.len()
+ }
+}
+
+impl FromStr for NumberingPattern {
+ type Err = &'static str;
+
+ fn from_str(pattern: &str) -> Result<Self, Self::Err> {
+ let mut pieces = EcoVec::new();
+ let mut handled = 0;
+
+ for (i, c) in pattern.char_indices() {
+ let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else {
+ continue;
+ };
+
+ let prefix = pattern[handled..i].into();
+ let case =
+ if c.is_uppercase() || c == '壹' { Case::Upper } else { Case::Lower };
+ pieces.push((prefix, kind, case));
+ handled = c.len_utf8() + i;
+ }
+
+ let suffix = pattern[handled..].into();
+ if pieces.is_empty() {
+ return Err("invalid numbering pattern");
+ }
+
+ Ok(Self { pieces, suffix, trimmed: false })
+ }
+}
+
+cast! {
+ NumberingPattern,
+ self => {
+ let mut pat = EcoString::new();
+ for (prefix, kind, case) in &self.pieces {
+ pat.push_str(prefix);
+ let mut c = kind.to_char();
+ if *case == Case::Upper {
+ c = c.to_ascii_uppercase();
+ }
+ pat.push(c);
+ }
+ pat.push_str(&self.suffix);
+ pat.into_value()
+ },
+ v: Str => v.parse()?,
+}
+
+/// Different kinds of numberings.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+enum NumberingKind {
+ Arabic,
+ Letter,
+ Roman,
+ Symbol,
+ Hebrew,
+ SimplifiedChinese,
+ // TODO: Pick the numbering pattern based on languages choice.
+ // As the `1st` numbering character of Chinese (Simplified) and
+ // Chinese (Traditional) is same, we are unable to determine
+ // if the context is Simplified or Traditional by only this
+ // character.
+ #[allow(unused)]
+ TraditionalChinese,
+ HiraganaIroha,
+ KatakanaIroha,
+ KoreanJamo,
+ KoreanSyllable,
+}
+
+impl NumberingKind {
+ /// Create a numbering kind from a lowercase character.
+ pub fn from_char(c: char) -> Option<Self> {
+ Some(match c {
+ '1' => NumberingKind::Arabic,
+ 'a' => NumberingKind::Letter,
+ 'i' => NumberingKind::Roman,
+ '*' => NumberingKind::Symbol,
+ 'א' => NumberingKind::Hebrew,
+ '一' | '壹' => NumberingKind::SimplifiedChinese,
+ 'い' => NumberingKind::HiraganaIroha,
+ 'イ' => NumberingKind::KatakanaIroha,
+ 'ㄱ' => NumberingKind::KoreanJamo,
+ '가' => NumberingKind::KoreanSyllable,
+ _ => return None,
+ })
+ }
+
+ /// The lowercase character for this numbering kind.
+ pub fn to_char(self) -> char {
+ match self {
+ Self::Arabic => '1',
+ Self::Letter => 'a',
+ Self::Roman => 'i',
+ Self::Symbol => '*',
+ Self::Hebrew => 'א',
+ Self::SimplifiedChinese => '一',
+ Self::TraditionalChinese => '一',
+ Self::HiraganaIroha => 'い',
+ Self::KatakanaIroha => 'イ',
+ Self::KoreanJamo => 'ㄱ',
+ Self::KoreanSyllable => '가',
+ }
+ }
+
+ /// Apply the numbering to the given number.
+ pub fn apply(self, mut n: usize, case: Case) -> EcoString {
+ match self {
+ Self::Arabic => {
+ eco_format!("{n}")
+ }
+ Self::Letter => zeroless::<26>(
+ |x| match case {
+ Case::Lower => char::from(b'a' + x as u8),
+ Case::Upper => char::from(b'A' + x as u8),
+ },
+ n,
+ ),
+ Self::HiraganaIroha => zeroless::<47>(
+ |x| {
+ [
+ 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
+ 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
+ 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
+ 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ',
+ 'も', 'せ', 'す',
+ ][x]
+ },
+ n,
+ ),
+ Self::KatakanaIroha => zeroless::<47>(
+ |x| {
+ [
+ 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
+ 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
+ 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
+ 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ',
+ 'モ', 'セ', 'ス',
+ ][x]
+ },
+ n,
+ ),
+ Self::Roman => {
+ if n == 0 {
+ return 'N'.into();
+ }
+
+ // Adapted from Yann Villessuzanne's roman.rs under the
+ // Unlicense, at https://github.com/linfir/roman.rs/
+ let mut fmt = EcoString::new();
+ for &(name, value) in &[
+ ("M̅", 1000000),
+ ("D̅", 500000),
+ ("C̅", 100000),
+ ("L̅", 50000),
+ ("X̅", 10000),
+ ("V̅", 5000),
+ ("I̅V̅", 4000),
+ ("M", 1000),
+ ("CM", 900),
+ ("D", 500),
+ ("CD", 400),
+ ("C", 100),
+ ("XC", 90),
+ ("L", 50),
+ ("XL", 40),
+ ("X", 10),
+ ("IX", 9),
+ ("V", 5),
+ ("IV", 4),
+ ("I", 1),
+ ] {
+ while n >= value {
+ n -= value;
+ for c in name.chars() {
+ match case {
+ Case::Lower => fmt.extend(c.to_lowercase()),
+ Case::Upper => fmt.push(c),
+ }
+ }
+ }
+ }
+
+ fmt
+ }
+ Self::Symbol => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
+ let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
+ let amount = ((n - 1) / SYMBOLS.len()) + 1;
+ std::iter::repeat(symbol).take(amount).collect()
+ }
+ Self::Hebrew => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ let mut fmt = EcoString::new();
+ 'outer: for &(name, value) in &[
+ ('ת', 400),
+ ('ש', 300),
+ ('ר', 200),
+ ('ק', 100),
+ ('צ', 90),
+ ('פ', 80),
+ ('ע', 70),
+ ('ס', 60),
+ ('נ', 50),
+ ('מ', 40),
+ ('ל', 30),
+ ('כ', 20),
+ ('י', 10),
+ ('ט', 9),
+ ('ח', 8),
+ ('ז', 7),
+ ('ו', 6),
+ ('ה', 5),
+ ('ד', 4),
+ ('ג', 3),
+ ('ב', 2),
+ ('א', 1),
+ ] {
+ while n >= value {
+ match n {
+ 15 => fmt.push_str("ט״ו"),
+ 16 => fmt.push_str("ט״ז"),
+ _ => {
+ let append_geresh = n == value && fmt.is_empty();
+ if n == value && !fmt.is_empty() {
+ fmt.push('״');
+ }
+ fmt.push(name);
+ if append_geresh {
+ fmt.push('׳');
+ }
+
+ n -= value;
+ continue;
+ }
+ }
+ break 'outer;
+ }
+ }
+ fmt
+ }
+ l @ (Self::SimplifiedChinese | Self::TraditionalChinese) => {
+ let chinese_case = match case {
+ Case::Lower => ChineseCase::Lower,
+ Case::Upper => ChineseCase::Upper,
+ };
+
+ match (n as u8).to_chinese(
+ match l {
+ Self::SimplifiedChinese => ChineseVariant::Simple,
+ Self::TraditionalChinese => ChineseVariant::Traditional,
+ _ => unreachable!(),
+ },
+ chinese_case,
+ ChineseCountMethod::TenThousand,
+ ) {
+ Ok(num_str) => EcoString::from(num_str),
+ Err(_) => '-'.into(),
+ }
+ }
+ Self::KoreanJamo => zeroless::<14>(
+ |x| {
+ [
+ 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
+ 'ㅌ', 'ㅍ', 'ㅎ',
+ ][x]
+ },
+ n,
+ ),
+ Self::KoreanSyllable => zeroless::<14>(
+ |x| {
+ [
+ '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
+ '타', '파', '하',
+ ][x]
+ },
+ n,
+ ),
+ }
+ }
+}
+
+/// Stringify a number using a base-N counting system with no zero digit.
+///
+/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'.
+/// we would get the following:
+///
+/// ```text
+/// 1 => "A"
+/// 2 => "B"
+/// 3 => "C"
+/// 4 => "AA"
+/// 5 => "AB"
+/// 6 => "AC"
+/// 7 => "BA"
+/// 8 => "BB"
+/// 9 => "BC"
+/// 10 => "CA"
+/// 11 => "CB"
+/// 12 => "CC"
+/// 13 => "AAA"
+/// etc.
+/// ```
+///
+/// You might be familiar with this scheme from the way spreadsheet software
+/// tends to label its columns.
+fn zeroless<const N_DIGITS: usize>(
+ mk_digit: impl Fn(usize) -> char,
+ mut n: usize,
+) -> EcoString {
+ if n == 0 {
+ return '-'.into();
+ }
+ let mut cs = vec![];
+ while n > 0 {
+ n -= 1;
+ cs.push(mk_digit(n % N_DIGITS));
+ n /= N_DIGITS;
+ }
+ cs.into_iter().rev().collect()
+}
diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs
new file mode 100644
index 00000000..ba858a69
--- /dev/null
+++ b/crates/typst-library/src/meta/outline.rs
@@ -0,0 +1,528 @@
+use std::str::FromStr;
+
+use typst::util::option_eq;
+
+use super::{
+ Counter, CounterKey, HeadingElem, LocalName, Numbering, NumberingPattern, Refable,
+};
+use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing};
+use crate::prelude::*;
+use crate::text::{LinebreakElem, SpaceElem, TextElem};
+
+/// A table of contents, figures, or other elements.
+///
+/// This function generates a list of all occurrences of an element in the
+/// document, up to a given depth. The element's numbering and page number will
+/// be displayed in the outline alongside its title or caption. By default this
+/// generates a table of contents.
+///
+/// ## Example { #example }
+/// ```example
+/// #outline()
+///
+/// = Introduction
+/// #lorem(5)
+///
+/// = Prior work
+/// #lorem(10)
+/// ```
+///
+/// ## Alternative outlines { #alternative-outlines }
+/// By setting the `target` parameter, the outline can be used to generate a
+/// list of other kinds of elements than headings. In the example below, we list
+/// all figures containing images by setting `target` to `{figure.where(kind:
+/// image)}`. We could have also set it to just `figure`, but then the list
+/// would also include figures containing tables or other material. For more
+/// details on the `where` selector, [see here]($type/content.where).
+///
+/// ```example
+/// #outline(
+/// title: [List of Figures],
+/// target: figure.where(kind: image),
+/// )
+///
+/// #figure(
+/// image("tiger.jpg"),
+/// caption: [A nice figure!],
+/// )
+/// ```
+///
+/// ## Styling the outline { #styling-the-outline }
+/// The outline element has several options for customization, such as its
+/// `title` and `indent` parameters. If desired, however, it is possible to
+/// have more control over the outline's look and style through the
+/// [`outline.entry`]($func/outline.entry) element.
+///
+/// Display: Outline
+/// Category: meta
+/// Keywords: Table of Contents
+#[element(Show, Finalize, LocalName)]
+#[scope(
+ scope.define("entry", OutlineEntry::func());
+ scope
+)]
+pub struct OutlineElem {
+ /// The title of the outline.
+ ///
+ /// - When set to `{auto}`, an appropriate title for the
+ /// [text language]($func/text.lang) will be used. This is the default.
+ /// - When set to `{none}`, the outline will not have a title.
+ /// - A custom title can be set by passing content.
+ ///
+ /// The outline's heading will not be numbered by default, but you can
+ /// force it to be with a show-set rule:
+ /// `{show outline: set heading(numbering: "1.")}`
+ /// ```
+ #[default(Some(Smart::Auto))]
+ pub title: Option<Smart<Content>>,
+
+ /// The type of element to include in the outline.
+ ///
+ /// To list figures containing a specific kind of element, like a table, you
+ /// can write `{figure.where(kind: table)}`.
+ ///
+ /// ```example
+ /// #outline(
+ /// title: [List of Tables],
+ /// target: figure.where(kind: table),
+ /// )
+ ///
+ /// #figure(
+ /// table(
+ /// columns: 4,
+ /// [t], [1], [2], [3],
+ /// [y], [0.3], [0.7], [0.5],
+ /// ),
+ /// caption: [Experiment results],
+ /// )
+ /// ```
+ #[default(LocatableSelector(Selector::Elem(
+ HeadingElem::func(),
+ Some(dict! { "outlined" => true })
+ )))]
+ pub target: LocatableSelector,
+
+ /// The maximum level up to which elements are included in the outline. When
+ /// this argument is `{none}`, all elements are included.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.")
+ /// #outline(depth: 2)
+ ///
+ /// = Yes
+ /// Top-level section.
+ ///
+ /// == Still
+ /// Subsection.
+ ///
+ /// === Nope
+ /// Not included.
+ /// ```
+ pub depth: Option<NonZeroUsize>,
+
+ /// How to indent the outline's entries.
+ ///
+ /// - `{none}`: No indent
+ /// - `{auto}`: Indents the numbering of the nested entry with the title of
+ /// its parent entry. This only has an effect if the entries are numbered
+ /// (e.g., via [heading numbering]($func/heading.numbering)).
+ /// - [Relative length]($type/relative-length): Indents the item by this length
+ /// multiplied by its nesting level. Specifying `{2em}`, for instance,
+ /// would indent top-level headings (not nested) by `{0em}`, second level
+ /// headings by `{2em}` (nested once), third-level headings by `{4em}`
+ /// (nested twice) and so on.
+ /// - [Function]($type/function): You can completely customize this setting
+ /// with a function. That function receives the nesting level as a
+ /// parameter (starting at 0 for top-level headings/elements) and can
+ /// return a relative length or content making up the indent. For example,
+ /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`,
+ /// while `{n => [→ ] * n}` would indent with one arrow per nesting
+ /// level.
+ ///
+ /// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or
+ /// `{false}` (equivalent to `{none}`) for this option is deprecated and
+ /// will be removed in a future release.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.a.")
+ ///
+ /// #outline(
+ /// title: [Contents (Automatic)],
+ /// indent: auto,
+ /// )
+ ///
+ /// #outline(
+ /// title: [Contents (Length)],
+ /// indent: 2em,
+ /// )
+ ///
+ /// #outline(
+ /// title: [Contents (Function)],
+ /// indent: n => [→ ] * n,
+ /// )
+ ///
+ /// = About ACME Corp.
+ /// == History
+ /// === Origins
+ /// #lorem(10)
+ ///
+ /// == Products
+ /// #lorem(10)
+ /// ```
+ #[default(None)]
+ pub indent: Option<Smart<OutlineIndent>>,
+
+ /// Content to fill the space between the title and the page number. Can be
+ /// set to `none` to disable filling.
+ ///
+ /// ```example
+ /// #outline(fill: line(length: 100%))
+ ///
+ /// = A New Beginning
+ /// ```
+ #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
+ pub fill: Option<Content>,
+}
+
+impl Show for OutlineElem {
+ #[tracing::instrument(name = "OutlineElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let mut seq = vec![ParbreakElem::new().pack()];
+ // Build the outline title.
+ if let Some(title) = self.title(styles) {
+ let title =
+ title.unwrap_or_else(|| {
+ TextElem::packed(self.local_name(
+ TextElem::lang_in(styles),
+ TextElem::region_in(styles),
+ ))
+ .spanned(self.span())
+ });
+
+ seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack());
+ }
+
+ let indent = self.indent(styles);
+ let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
+
+ let mut ancestors: Vec<&Content> = vec![];
+ let elems = vt.introspector.query(&self.target(styles).0);
+
+ for elem in &elems {
+ let Some(entry) = OutlineEntry::from_outlinable(
+ vt,
+ self.span(),
+ elem.clone().into_inner(),
+ self.fill(styles),
+ )? else {
+ continue;
+ };
+
+ let level = entry.level();
+ if depth < level {
+ continue;
+ }
+
+ // Deals with the ancestors of the current element.
+ // This is only applicable for elements with a hierarchy/level.
+ while ancestors
+ .last()
+ .and_then(|ancestor| ancestor.with::<dyn Outlinable>())
+ .map_or(false, |last| last.level() >= level)
+ {
+ ancestors.pop();
+ }
+
+ OutlineIndent::apply(&indent, vt, &ancestors, &mut seq, self.span())?;
+
+ // Add the overridable outline entry, followed by a line break.
+ seq.push(entry.pack());
+ seq.push(LinebreakElem::new().pack());
+
+ ancestors.push(elem);
+ }
+
+ seq.push(ParbreakElem::new().pack());
+
+ Ok(Content::sequence(seq))
+ }
+}
+
+impl Finalize for OutlineElem {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ realized
+ .styled(HeadingElem::set_outlined(false))
+ .styled(HeadingElem::set_numbering(None))
+ }
+}
+
+impl LocalName for OutlineElem {
+ fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
+ match lang {
+ Lang::ALBANIAN => "Përmbajtja",
+ Lang::ARABIC => "المحتويات",
+ Lang::BOKMÅL => "Innhold",
+ Lang::CHINESE if option_eq(region, "TW") => "目錄",
+ Lang::CHINESE => "目录",
+ Lang::CZECH => "Obsah",
+ Lang::DANISH => "Indhold",
+ Lang::DUTCH => "Inhoudsopgave",
+ Lang::FILIPINO => "Talaan ng mga Nilalaman",
+ Lang::FRENCH => "Table des matières",
+ Lang::GERMAN => "Inhaltsverzeichnis",
+ Lang::ITALIAN => "Indice",
+ Lang::NYNORSK => "Innhald",
+ Lang::POLISH => "Spis treści",
+ Lang::PORTUGUESE if option_eq(region, "PT") => "Índice",
+ Lang::PORTUGUESE => "Sumário",
+ Lang::RUSSIAN => "Содержание",
+ Lang::SLOVENIAN => "Kazalo",
+ Lang::SPANISH => "Índice",
+ Lang::SWEDISH => "Innehåll",
+ Lang::TURKISH => "İçindekiler",
+ Lang::UKRAINIAN => "Зміст",
+ Lang::VIETNAMESE => "Mục lục",
+ Lang::ENGLISH | _ => "Contents",
+ }
+ }
+}
+
+/// Marks an element as being able to be outlined. This is used to implement the
+/// `#outline()` element.
+pub trait Outlinable: Refable {
+ /// Produce an outline item for this element.
+ fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>>;
+
+ /// Returns the nesting level of this element.
+ fn level(&self) -> NonZeroUsize {
+ NonZeroUsize::ONE
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum OutlineIndent {
+ Bool(bool),
+ Rel(Rel<Length>),
+ Func(Func),
+}
+
+impl OutlineIndent {
+ fn apply(
+ indent: &Option<Smart<Self>>,
+ vt: &mut Vt,
+ ancestors: &Vec<&Content>,
+ seq: &mut Vec<Content>,
+ span: Span,
+ ) -> SourceResult<()> {
+ match indent {
+ // 'none' | 'false' => no indenting
+ None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {}
+
+ // 'auto' | 'true' => use numbering alignment for indenting
+ Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => {
+ // Add hidden ancestors numberings to realize the indent.
+ let mut hidden = Content::empty();
+ for ancestor in ancestors {
+ let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
+
+ if let Some(numbering) = ancestor_outlinable.numbering() {
+ let numbers = ancestor_outlinable
+ .counter()
+ .at(vt, ancestor.location().unwrap())?
+ .display(vt, &numbering)?;
+
+ hidden += numbers + SpaceElem::new().pack();
+ };
+ }
+
+ if !ancestors.is_empty() {
+ seq.push(HideElem::new(hidden).pack());
+ seq.push(SpaceElem::new().pack());
+ }
+ }
+
+ // Length => indent with some fixed spacing per level
+ Some(Smart::Custom(OutlineIndent::Rel(length))) => {
+ seq.push(
+ HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()),
+ );
+ }
+
+ // Function => call function with the current depth and take
+ // the returned content
+ Some(Smart::Custom(OutlineIndent::Func(func))) => {
+ let depth = ancestors.len();
+ let LengthOrContent(content) =
+ func.call_vt(vt, [depth])?.cast().at(span)?;
+ if !content.is_empty() {
+ seq.push(content);
+ }
+ }
+ };
+
+ Ok(())
+ }
+}
+
+cast! {
+ OutlineIndent,
+ self => match self {
+ Self::Bool(v) => v.into_value(),
+ Self::Rel(v) => v.into_value(),
+ Self::Func(v) => v.into_value()
+ },
+ v: bool => OutlineIndent::Bool(v),
+ v: Rel<Length> => OutlineIndent::Rel(v),
+ v: Func => OutlineIndent::Func(v),
+}
+
+struct LengthOrContent(Content);
+
+cast! {
+ LengthOrContent,
+ v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
+ v: Content => Self(v),
+}
+
+/// Represents each entry line in an outline, including the reference to the
+/// outlined element, its page number, and the filler content between both.
+///
+/// This element is intended for use with show rules to control the appearance
+/// of outlines.
+///
+/// ## Example { #example }
+/// The example below shows how to style entries for top-level sections to make
+/// them stand out.
+///
+/// ```example
+/// #set heading(numbering: "1.")
+///
+/// #show outline.entry.where(
+/// level: 1
+/// ): it => {
+/// v(12pt, weak: true)
+/// strong(it)
+/// }
+///
+/// #outline(indent: auto)
+///
+/// = Introduction
+/// = Background
+/// == History
+/// == State of the Art
+/// = Analysis
+/// == Setup
+/// ```
+///
+/// To completely customize an entry's line, you can also build it from scratch
+/// by accessing the `level`, `element`, `body`, `fill` and `page` fields on the entry.
+///
+/// Display: Outline Entry
+/// Category: meta
+#[element(Show)]
+pub struct OutlineEntry {
+ /// The nesting level of this outline entry. Starts at `{1}` for top-level
+ /// entries.
+ #[required]
+ pub level: NonZeroUsize,
+
+ /// The element this entry refers to. Its location will be available
+ /// through the [`location`]($type/content.location) method on content
+ /// and can be [linked]($func/link) to.
+ #[required]
+ pub element: Content,
+
+ /// The content which is displayed in place of the referred element at its
+ /// entry in the outline. For a heading, this would be its number followed
+ /// by the heading's title, for example.
+ #[required]
+ pub body: Content,
+
+ /// The content used to fill the space between the element's outline and
+ /// its page number, as defined by the outline element this entry is
+ /// located in. When `{none}`, empty space is inserted in that gap instead.
+ ///
+ /// Note that, when using show rules to override outline entries, it is
+ /// recommended to wrap the filling content in a [`box`]($func/box) with
+ /// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show
+ /// precisely as many `-` characters as necessary to fill a particular gap.
+ #[required]
+ pub fill: Option<Content>,
+
+ /// The page number of the element this entry links to, formatted with the
+ /// numbering set for the referenced page.
+ #[required]
+ pub page: Content,
+}
+
+impl OutlineEntry {
+ /// Generates an OutlineEntry from the given element, if possible (errors if
+ /// the element does not implement `Outlinable`). If the element should not
+ /// be outlined (e.g. heading with 'outlined: false'), does not generate an
+ /// entry instance (returns `Ok(None)`).
+ fn from_outlinable(
+ vt: &mut Vt,
+ span: Span,
+ elem: Content,
+ fill: Option<Content>,
+ ) -> SourceResult<Option<Self>> {
+ let Some(outlinable) = elem.with::<dyn Outlinable>() else {
+ bail!(span, "cannot outline {}", elem.func().name());
+ };
+
+ let Some(body) = outlinable.outline(vt)? else {
+ return Ok(None);
+ };
+
+ let location = elem.location().unwrap();
+ let page_numbering = vt
+ .introspector
+ .page_numbering(location)
+ .cast::<Option<Numbering>>()
+ .unwrap()
+ .unwrap_or_else(|| {
+ Numbering::Pattern(NumberingPattern::from_str("1").unwrap())
+ });
+
+ let page = Counter::new(CounterKey::Page)
+ .at(vt, location)?
+ .display(vt, &page_numbering)?;
+
+ Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
+ }
+}
+
+impl Show for OutlineEntry {
+ fn show(&self, _vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ let mut seq = vec![];
+ let elem = self.element();
+
+ // In case a user constructs an outline entry with an arbitrary element.
+ let Some(location) = elem.location() else {
+ bail!(self.span(), "cannot outline {}", elem.func().name())
+ };
+
+ // The body text remains overridable.
+ seq.push(self.body().linked(Destination::Location(location)));
+
+ // Add filler symbols between the section name and page number.
+ if let Some(filler) = self.fill() {
+ seq.push(SpaceElem::new().pack());
+ seq.push(
+ BoxElem::new()
+ .with_body(Some(filler))
+ .with_width(Fr::one().into())
+ .pack(),
+ );
+ seq.push(SpaceElem::new().pack());
+ } else {
+ seq.push(HElem::new(Fr::one().into()).pack());
+ }
+
+ // Add the page number.
+ let page = self.page().linked(Destination::Location(location));
+ seq.push(page);
+
+ Ok(Content::sequence(seq))
+ }
+}
diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs
new file mode 100644
index 00000000..826b812e
--- /dev/null
+++ b/crates/typst-library/src/meta/query.rs
@@ -0,0 +1,145 @@
+use crate::prelude::*;
+
+/// Finds elements in the document.
+///
+/// The `query` functions lets you search your document for elements of a
+/// particular type or with a particular label.
+///
+/// To use it, you first need to retrieve the current document location with the
+/// [`locate`]($func/locate) function. You can then decide whether you want to
+/// find all elements, just the ones before that location, or just the ones
+/// after it.
+///
+/// ## Finding elements { #finding-elements }
+/// In the example below, we create a custom page header that displays the text
+/// "Typst Academy" in small capitals and the current section title. On the
+/// first page, the section title is omitted because the header is before the
+/// first section heading.
+///
+/// To realize this layout, we call `locate` and then query for all headings
+/// after the current location. The function we pass to locate is called twice
+/// in this case: Once per page.
+///
+/// - On the first page the query for all headings before the current location
+/// yields an empty array: There are no previous headings. We check for this
+/// case and and just display "Typst Academy".
+///
+/// - For the second page, we retrieve the last element from the query's result.
+/// This is the latest heading before the current position and as such, it is
+/// the heading of the section we are currently in. We access its content
+/// through the `body` field and display it alongside "Typst Academy".
+///
+/// ```example
+/// >>> #set page(
+/// >>> width: 240pt,
+/// >>> height: 180pt,
+/// >>> margin: (top: 35pt, rest: 15pt),
+/// >>> header-ascent: 12pt,
+/// >>> )
+/// #set page(header: locate(loc => {
+/// let elems = query(
+/// selector(heading).before(loc),
+/// loc,
+/// )
+/// let academy = smallcaps[
+/// Typst Academy
+/// ]
+/// if elems == () {
+/// align(right, academy)
+/// } else {
+/// let body = elems.last().body
+/// academy + h(1fr) + emph(body)
+/// }
+/// }))
+///
+/// = Introduction
+/// #lorem(23)
+///
+/// = Background
+/// #lorem(30)
+///
+/// = Analysis
+/// #lorem(15)
+/// ```
+///
+/// ## A word of caution { #caution }
+/// To resolve all your queries, Typst evaluates and layouts parts of the
+/// document multiple times. However, there is no guarantee that your queries
+/// can actually be completely resolved. If you aren't careful a query can
+/// affect itself—leading to a result that never stabilizes.
+///
+/// In the example below, we query for all headings in the document. We then
+/// generate as many headings. In the beginning, there's just one heading,
+/// titled `Real`. Thus, `count` is `1` and one `Fake` heading is generated.
+/// Typst sees that the query's result has changed and processes it again. This
+/// time, `count` is `2` and two `Fake` headings are generated. This goes on and
+/// on. As we can see, the output has five headings. This is because Typst
+/// simply gives up after five attempts.
+///
+/// In general, you should try not to write queries that affect themselves.
+/// The same words of caution also apply to other introspection features like
+/// [counters]($func/counter) and [state]($func/state).
+///
+/// ```example
+/// = Real
+/// #locate(loc => {
+/// let elems = query(heading, loc)
+/// let count = elems.len()
+/// count * [= Fake]
+/// })
+/// ```
+///
+/// ## Migration Hints { #migration-hints }
+/// The `before` and `after` arguments have been removed in version 0.3.0. You
+/// can now use flexible selector combinator methods instead. For example,
+/// `query(heading, before: loc)` becomes `query(heading.before(loc), loc)`.
+/// Please refer to the [selector documentation]($type/selector) for more
+/// details.
+///
+/// Display: Query
+/// Category: meta
+#[func]
+pub fn query(
+ /// Can be an element function like a `heading` or `figure`, a `{<label>}`
+ /// or a more complex selector like `{heading.where(level: 1)}`.
+ ///
+ /// Currently, only a subset of element functions is supported. Aside from
+ /// headings and figures, this includes equations, references and all
+ /// elements with an explicit label. As a result, you _can_ query for e.g.
+ /// [`strong`]($func/strong) elements, but you will find only those that
+ /// have an explicit label attached to them. This limitation will be
+ /// resolved in the future.
+ target: LocatableSelector,
+ /// Can be any location. Why is it required then? As noted before, Typst has
+ /// to evaluate parts of your code multiple times to determine the values of
+ /// all state. By only allowing this function within
+ /// [`locate`]($func/locate) calls, the amount of code that can depend on
+ /// the query's result is reduced. If you could call it directly at the top
+ /// level of a module, the evaluation of the whole module and its exports
+ /// could depend on the query's result.
+ location: Location,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> Array {
+ let _ = location;
+ let vec = vm.vt.introspector.query(&target.0);
+ vec.into_iter()
+ .map(|elem| Value::Content(elem.into_inner()))
+ .collect()
+}
+
+/// Turns a value into a selector. The following values are accepted:
+/// - An element function like a `heading` or `figure`.
+/// - A `{<label>}`.
+/// - A more complex selector like `{heading.where(level: 1)}`.
+///
+/// Display: Selector
+/// Category: meta
+#[func]
+pub fn selector(
+ /// Can be an element function like a `heading` or `figure`, a `{<label>}`
+ /// or a more complex selector like `{heading.where(level: 1)}`.
+ target: Selector,
+) -> Selector {
+ target
+}
diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs
new file mode 100644
index 00000000..5bd04431
--- /dev/null
+++ b/crates/typst-library/src/meta/reference.rs
@@ -0,0 +1,276 @@
+use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering};
+use crate::meta::FootnoteElem;
+use crate::prelude::*;
+use crate::text::TextElem;
+
+/// A reference to a label or bibliography.
+///
+/// Produces a textual reference to a label. For example, a reference to a
+/// heading will yield an appropriate string such as "Section 1" for a reference
+/// to the first heading. The references are also links to the respective
+/// element. Reference syntax can also be used to [cite]($func/cite) from a
+/// bibliography.
+///
+/// Referenceable elements include [headings]($func/heading),
+/// [figures]($func/figure), [equations]($func/math.equation), and
+/// [footnotes]($func/footnote). To create a custom referenceable element like a
+/// theorem, you can create a figure of a custom [`kind`]($func/figure.kind) and
+/// write a show rule for it. In the future, there might be a more direct way to
+/// define a custom referenceable element.
+///
+/// If you just want to link to a labelled element and not get an automatic
+/// textual reference, consider using the [`link`]($func/link) function instead.
+///
+/// ## Example { #example }
+/// ```example
+/// #set heading(numbering: "1.")
+/// #set math.equation(numbering: "(1)")
+///
+/// = Introduction <intro>
+/// Recent developments in
+/// typesetting software have
+/// rekindled hope in previously
+/// frustrated researchers. @distress
+/// As shown in @results, we ...
+///
+/// = Results <results>
+/// We discuss our approach in
+/// comparison with others.
+///
+/// == Performance <perf>
+/// @slow demonstrates what slow
+/// software looks like.
+/// $ O(n) = 2^n $ <slow>
+///
+/// #bibliography("works.bib")
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax: A reference to a label can be
+/// created by typing an `@` followed by the name of the label (e.g.
+/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`).
+///
+/// To customize the supplement, add content in square brackets after the
+/// reference: `[@intro[Chapter]]`.
+///
+/// ## Customization { #customization }
+/// If you write a show rule for references, you can access the referenced
+/// element through the `element` field of the reference. The `element` may
+/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
+/// always need to handle that case in your code.
+///
+/// ```example
+/// #set heading(numbering: "1.")
+/// #set math.equation(numbering: "(1)")
+///
+/// #show ref: it => {
+/// let eq = math.equation
+/// let el = it.element
+/// if el != none and el.func() == eq {
+/// // Override equation references.
+/// numbering(
+/// el.numbering,
+/// ..counter(eq).at(el.location())
+/// )
+/// } else {
+/// // Other references as usual.
+/// it
+/// }
+/// }
+///
+/// = Beginnings <beginning>
+/// In @beginning we prove @pythagoras.
+/// $ a^2 + b^2 = c^2 $ <pythagoras>
+/// ```
+///
+/// Display: Reference
+/// Category: meta
+#[element(Synthesize, Locatable, Show)]
+pub struct RefElem {
+ /// The target label that should be referenced.
+ #[required]
+ pub target: Label,
+
+ /// A supplement for the reference.
+ ///
+ /// For references to headings or figures, this is added before the
+ /// referenced number. For citations, this can be used to add a page number.
+ ///
+ /// If a function is specified, it is passed the referenced element and
+ /// should return content.
+ ///
+ /// ```example
+ /// #set heading(numbering: "1.")
+ /// #set ref(supplement: it => {
+ /// if it.func() == heading {
+ /// "Chapter"
+ /// } else {
+ /// "Thing"
+ /// }
+ /// })
+ ///
+ /// = Introduction <intro>
+ /// In @intro, we see how to turn
+ /// Sections into Chapters. And
+ /// in @intro[Part], it is done
+ /// manually.
+ /// ```
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// A synthesized citation.
+ #[synthesized]
+ pub citation: Option<CiteElem>,
+
+ /// The referenced element.
+ #[synthesized]
+ pub element: Option<Content>,
+}
+
+impl Synthesize for RefElem {
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ let citation = self.to_citation(vt, styles)?;
+ self.push_citation(Some(citation));
+ self.push_element(None);
+
+ let target = self.target();
+ if !BibliographyElem::has(vt, &target.0) {
+ if let Ok(elem) = vt.introspector.query_label(&target) {
+ self.push_element(Some(elem.into_inner()));
+ return Ok(());
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl Show for RefElem {
+ #[tracing::instrument(name = "RefElem::show", skip_all)]
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let target = self.target();
+ let elem = vt.introspector.query_label(&self.target());
+ let span = self.span();
+
+ if BibliographyElem::has(vt, &target.0) {
+ if elem.is_ok() {
+ bail!(span, "label occurs in the document and its bibliography");
+ }
+
+ return Ok(self.to_citation(vt, styles)?.pack().spanned(span));
+ }
+
+ let elem = elem.at(span)?;
+
+ if elem.func() == FootnoteElem::func() {
+ return Ok(FootnoteElem::with_label(target).pack().spanned(span));
+ }
+
+ let refable = elem
+ .with::<dyn Refable>()
+ .ok_or_else(|| {
+ if elem.can::<dyn Figurable>() {
+ eco_format!(
+ "cannot reference {} directly, try putting it into a figure",
+ elem.func().name()
+ )
+ } else {
+ eco_format!("cannot reference {}", elem.func().name())
+ }
+ })
+ .at(span)?;
+
+ let numbering = refable
+ .numbering()
+ .ok_or_else(|| {
+ eco_format!(
+ "cannot reference {} without numbering",
+ elem.func().name()
+ )
+ })
+ .hint(eco_format!(
+ "you can enable heading numbering with `#set {}(numbering: \"1.\")`",
+ elem.func().name()
+ ))
+ .at(span)?;
+
+ let numbers = refable
+ .counter()
+ .at(vt, elem.location().unwrap())?
+ .display(vt, &numbering.trimmed())?;
+
+ let supplement = match self.supplement(styles) {
+ Smart::Auto => refable.supplement(),
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => {
+ supplement.resolve(vt, [(*elem).clone()])?
+ }
+ };
+
+ let mut content = numbers;
+ if !supplement.is_empty() {
+ content = supplement + TextElem::packed("\u{a0}") + content;
+ }
+
+ Ok(content.linked(Destination::Location(elem.location().unwrap())))
+ }))
+ }
+}
+
+impl RefElem {
+ /// Turn the reference into a citation.
+ pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> {
+ let mut elem = CiteElem::new(vec![self.target().0]);
+ elem.0.set_location(self.0.location().unwrap());
+ elem.synthesize(vt, styles)?;
+ elem.push_supplement(match self.supplement(styles) {
+ Smart::Custom(Some(Supplement::Content(content))) => Some(content),
+ _ => None,
+ });
+
+ Ok(elem)
+ }
+}
+
+/// Additional content for a reference.
+pub enum Supplement {
+ Content(Content),
+ Func(Func),
+}
+
+impl Supplement {
+ /// Tries to resolve the supplement into its content.
+ pub fn resolve<T: IntoValue>(
+ &self,
+ vt: &mut Vt,
+ args: impl IntoIterator<Item = T>,
+ ) -> SourceResult<Content> {
+ Ok(match self {
+ Supplement::Content(content) => content.clone(),
+ Supplement::Func(func) => func.call_vt(vt, args)?.display(),
+ })
+ }
+}
+
+cast! {
+ Supplement,
+ self => match self {
+ Self::Content(v) => v.into_value(),
+ Self::Func(v) => v.into_value(),
+ },
+ v: Content => Self::Content(v),
+ v: Func => Self::Func(v),
+}
+
+/// Marks an element as being able to be referenced. This is used to implement
+/// the `@ref` element.
+pub trait Refable {
+ /// The supplement, if not overridden by the reference.
+ fn supplement(&self) -> Content;
+
+ /// Returns the counter of this element.
+ fn counter(&self) -> Counter;
+
+ /// Returns the numbering of this element.
+ fn numbering(&self) -> Option<Numbering>;
+}
diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs
new file mode 100644
index 00000000..aee53a29
--- /dev/null
+++ b/crates/typst-library/src/meta/state.rs
@@ -0,0 +1,440 @@
+use std::fmt::{self, Debug, Formatter, Write};
+
+use ecow::{eco_vec, EcoVec};
+use typst::eval::Tracer;
+use typst::model::DelayedErrors;
+
+use crate::prelude::*;
+
+/// Manages stateful parts of your document.
+///
+/// Let's say you have some computations in your document and want to remember
+/// the result of your last computation to use it in the next one. You might try
+/// something similar to the code below and expect it to output 10, 13, 26, and
+/// 21. However this **does not work** in Typst. If you test this code, you will
+/// see that Typst complains with the following error message: _Variables from
+/// outside the function are read-only and cannot be modified._
+///
+/// ```typ
+/// #let x = 0
+/// #let compute(expr) = {
+/// x = eval(
+/// expr.replace("x", str(x))
+/// )
+/// [New value is #x. ]
+/// }
+///
+/// #compute("10") \
+/// #compute("x + 3") \
+/// #compute("x * 2") \
+/// #compute("x - 5")
+/// ```
+///
+/// ## State and document markup { #state-and-markup }
+/// Why does it do that? Because, in general, this kind of computation with side
+/// effects is problematic in document markup and Typst is upfront about that.
+/// For the results to make sense, the computation must proceed in the same
+/// order in which the results will be laid out in the document. In our simple
+/// example, that's the case, but in general it might not be.
+///
+/// Let's look at a slightly different, but similar kind of state: The heading
+/// numbering. We want to increase the heading counter at each heading. Easy
+/// enough, right? Just add one. Well, it's not that simple. Consider the
+/// following example:
+///
+/// ```example
+/// #set heading(numbering: "1.")
+/// #let template(body) = [
+/// = Outline
+/// ...
+/// #body
+/// ]
+///
+/// #show: template
+///
+/// = Introduction
+/// ...
+/// ```
+///
+/// Here, Typst first processes the body of the document after the show rule,
+/// sees the `Introduction` heading, then passes the resulting content to the
+/// `template` function and only then sees the `Outline`. Just counting up would
+/// number the `Introduction` with `1` and the `Outline` with `2`.
+///
+/// ## Managing state in Typst { #state-in-typst }
+/// So what do we do instead? We use Typst's state management system. Calling
+/// the `state` function with an identifying string key and an optional initial
+/// value gives you a state value which exposes a few methods. The two most
+/// important ones are `display` and `update`:
+///
+/// - The `display` method shows the current value of the state. You can
+/// optionally give it a function that receives the value and formats it in
+/// some way.
+///
+/// - The `update` method modifies the state. You can give it any value. If
+/// given a non-function value, it sets the state to that value. If given a
+/// function, that function receives the previous state and has to return the
+/// new state.
+///
+/// Our initial example would now look like this:
+///
+/// ```example
+/// #let s = state("x", 0)
+/// #let compute(expr) = [
+/// #s.update(x =>
+/// eval(expr.replace("x", str(x)))
+/// )
+/// New value is #s.display().
+/// ]
+///
+/// #compute("10") \
+/// #compute("x + 3") \
+/// #compute("x * 2") \
+/// #compute("x - 5")
+/// ```
+///
+/// State managed by Typst is always updated in layout order, not in evaluation
+/// order. The `update` method returns content and its effect occurs at the
+/// position where the returned content is inserted into the document.
+///
+/// As a result, we can now also store some of the computations in
+/// variables, but they still show the correct results:
+///
+/// ```example
+/// >>> #let s = state("x", 0)
+/// >>> #let compute(expr) = [
+/// >>> #s.update(x =>
+/// >>> eval(expr.replace("x", str(x)))
+/// >>> )
+/// >>> New value is #s.display().
+/// >>> ]
+/// <<< ...
+///
+/// #let more = [
+/// #compute("x * 2") \
+/// #compute("x - 5")
+/// ]
+///
+/// #compute("10") \
+/// #compute("x + 3") \
+/// #more
+/// ```
+///
+/// This example is of course a bit silly, but in practice this is often exactly
+/// what you want! A good example are heading counters, which is why Typst's
+/// [counting system]($func/counter) is very similar to its state system.
+///
+/// ## Time Travel { #time-travel }
+/// By using Typst's state management system you also get time travel
+/// capabilities! By combining the state system with [`locate`]($func/locate)
+/// and [`query`]($func/query), we can find out what the value of the state will
+/// be at any position in the document from anywhere else. In particular, the
+/// `at` method gives us the value of the state at any location and the `final`
+/// methods gives us the value of the state at the end of the document.
+///
+/// ```example
+/// >>> #let s = state("x", 0)
+/// >>> #let compute(expr) = [
+/// >>> #s.update(x => {
+/// >>> eval(expr.replace("x", str(x)))
+/// >>> })
+/// >>> New value is #s.display().
+/// >>> ]
+/// <<< ...
+///
+/// Value at `<here>` is
+/// #locate(loc => s.at(
+/// query(<here>, loc)
+/// .first()
+/// .location()
+/// ))
+///
+/// #compute("10") \
+/// #compute("x + 3") \
+/// *Here.* <here> \
+/// #compute("x * 2") \
+/// #compute("x - 5")
+/// ```
+///
+/// ## A word of caution { #caution }
+/// To resolve the values of all states, Typst evaluates parts of your code
+/// multiple times. However, there is no guarantee that your state manipulation
+/// can actually be completely resolved.
+///
+/// For instance, if you generate state updates depending on the final value of
+/// a state, the results might never converge. The example below illustrates
+/// this. We initialize our state with `1` and then update it to its own final
+/// value plus 1. So it should be `2`, but then its final value is `2`, so it
+/// should be `3`, and so on. This example display `4` because Typst simply
+/// gives up after a few attempts.
+///
+/// ```example
+/// #let s = state("x", 1)
+/// #locate(loc => {
+/// s.update(s.final(loc) + 1)
+/// })
+/// #s.display()
+/// ```
+///
+/// In general, you should _typically_ not generate state updates from within
+/// `locate` calls or `display` calls of state or counters. Instead, pass a
+/// function to `update` that determines the value of the state based on its
+/// previous value.
+///
+/// ## Methods
+/// ### display()
+/// Displays the value of the state.
+///
+/// - format: function (positional)
+/// A function which receives the value of the state and can return arbitrary
+/// content which is then displayed. If this is omitted, the value is directly
+/// displayed.
+///
+/// - returns: content
+///
+/// ### update()
+/// Updates the value of the state.
+///
+/// The update will be in effect at the position where the returned content is
+/// inserted into the document. If you don't put the output into the document,
+/// nothing happens! This would be the case, for example, if you write
+/// `{let _ = state("key").update(7)}`. State updates are always applied in
+/// layout order and in that case, Typst wouldn't know when to update the state.
+///
+/// - value: any or function (positional, required)
+/// If given a non function-value, sets the state to that value. If given a
+/// function, that function receives the previous state and has to return the
+/// new state.
+///
+/// - returns: content
+///
+/// ### at()
+/// Gets the value of the state at the given location.
+///
+/// - location: location (positional, required)
+/// The location at which the state's value should be retrieved. A suitable
+/// location can be retrieved from [`locate`]($func/locate) or
+/// [`query`]($func/query).
+///
+/// - returns: any
+///
+/// ### final()
+/// Gets the value of the state at the end of the document.
+///
+/// - location: location (positional, required)
+/// Can be any location. Why is it required then? As noted before, Typst has
+/// to evaluate parts of your code multiple times to determine the values of
+/// all state. By only allowing this method within [`locate`]($func/locate)
+/// calls, the amount of code that can depend on the method's result is
+/// reduced. If you could call `final` directly at the top level of a module,
+/// the evaluation of the whole module and its exports could depend on the
+/// state's value.
+///
+/// - returns: any
+///
+/// Display: State
+/// Category: meta
+#[func]
+pub fn state(
+ /// The key that identifies this state.
+ key: Str,
+ /// The initial value of the state.
+ #[default]
+ init: Value,
+) -> State {
+ State { key, init }
+}
+
+/// A state.
+#[derive(Clone, PartialEq, Hash)]
+pub struct State {
+ /// The key that identifies the state.
+ key: Str,
+ /// The initial value of the state.
+ init: Value,
+}
+
+impl State {
+ /// Call a method on a state.
+ #[tracing::instrument(skip(vm))]
+ pub fn call_method(
+ self,
+ vm: &mut Vm,
+ method: &str,
+ mut args: Args,
+ span: Span,
+ ) -> SourceResult<Value> {
+ let value = match method {
+ "display" => self.display(args.eat()?).into_value(),
+ "at" => self.at(&mut vm.vt, args.expect("location")?)?,
+ "final" => self.final_(&mut vm.vt, args.expect("location")?)?,
+ "update" => self.update(args.expect("value or function")?).into_value(),
+ _ => bail!(span, "type state has no method `{}`", method),
+ };
+ args.finish()?;
+ Ok(value)
+ }
+
+ /// Display the current value of the state.
+ pub fn display(self, func: Option<Func>) -> Content {
+ DisplayElem::new(self, func).pack()
+ }
+
+ /// Get the value of the state at the given location.
+ #[tracing::instrument(skip(self, vt))]
+ pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> {
+ let sequence = self.sequence(vt)?;
+ let offset = vt.introspector.query(&self.selector().before(location, true)).len();
+ Ok(sequence[offset].clone())
+ }
+
+ /// Get the value of the state at the final location.
+ #[tracing::instrument(skip(self, vt))]
+ pub fn final_(self, vt: &mut Vt, _: Location) -> SourceResult<Value> {
+ let sequence = self.sequence(vt)?;
+ Ok(sequence.last().unwrap().clone())
+ }
+
+ /// Produce content that performs a state update.
+ pub fn update(self, update: StateUpdate) -> Content {
+ UpdateElem::new(self, update).pack()
+ }
+
+ /// Produce the whole sequence of states.
+ ///
+ /// This has to happen just once for all states, cutting down the number
+ /// of state updates from quadratic to linear.
+ fn sequence(&self, vt: &mut Vt) -> SourceResult<EcoVec<Value>> {
+ self.sequence_impl(
+ vt.world,
+ vt.introspector,
+ vt.locator.track(),
+ TrackedMut::reborrow_mut(&mut vt.delayed),
+ TrackedMut::reborrow_mut(&mut vt.tracer),
+ )
+ }
+
+ /// Memoized implementation of `sequence`.
+ #[comemo::memoize]
+ fn sequence_impl(
+ &self,
+ world: Tracked<dyn World + '_>,
+ introspector: Tracked<Introspector>,
+ locator: Tracked<Locator>,
+ delayed: TrackedMut<DelayedErrors>,
+ tracer: TrackedMut<Tracer>,
+ ) -> SourceResult<EcoVec<Value>> {
+ let mut locator = Locator::chained(locator);
+ let mut vt = Vt {
+ world,
+ introspector,
+ locator: &mut locator,
+ delayed,
+ tracer,
+ };
+ let mut state = self.init.clone();
+ let mut stops = eco_vec![state.clone()];
+
+ for elem in introspector.query(&self.selector()) {
+ let elem = elem.to::<UpdateElem>().unwrap();
+ match elem.update() {
+ StateUpdate::Set(value) => state = value,
+ StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?,
+ }
+ stops.push(state.clone());
+ }
+
+ Ok(stops)
+ }
+
+ /// The selector for this state's updates.
+ fn selector(&self) -> Selector {
+ Selector::Elem(UpdateElem::func(), Some(dict! { "state" => self.clone() }))
+ }
+}
+
+impl Debug for State {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("state(")?;
+ self.key.fmt(f)?;
+ f.write_str(", ")?;
+ self.init.fmt(f)?;
+ f.write_char(')')
+ }
+}
+
+cast! {
+ type State: "state",
+}
+
+/// An update to perform on a state.
+#[derive(Clone, PartialEq, Hash)]
+pub enum StateUpdate {
+ /// Set the state to the specified value.
+ Set(Value),
+ /// Apply the given function to the state.
+ Func(Func),
+}
+
+impl Debug for StateUpdate {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad("..")
+ }
+}
+
+cast! {
+ type StateUpdate: "state update",
+ v: Func => Self::Func(v),
+ v: Value => Self::Set(v),
+}
+
+/// Executes a display of a state.
+///
+/// Display: State
+/// Category: special
+#[element(Locatable, Show)]
+struct DisplayElem {
+ /// The state.
+ #[required]
+ state: State,
+
+ /// The function to display the state with.
+ #[required]
+ func: Option<Func>,
+}
+
+impl Show for DisplayElem {
+ #[tracing::instrument(name = "DisplayElem::show", skip(self, vt))]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(vt.delayed(|vt| {
+ let location = self.0.location().unwrap();
+ let value = self.state().at(vt, location)?;
+ Ok(match self.func() {
+ Some(func) => func.call_vt(vt, [value])?.display(),
+ None => value.display(),
+ })
+ }))
+ }
+}
+
+/// Executes a display of a state.
+///
+/// Display: State
+/// Category: special
+#[element(Locatable, Show)]
+struct UpdateElem {
+ /// The state.
+ #[required]
+ state: State,
+
+ /// The update to perform on the state.
+ #[required]
+ update: StateUpdate,
+}
+
+impl Show for UpdateElem {
+ #[tracing::instrument(name = "UpdateElem::show")]
+ fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ Ok(Content::empty())
+ }
+}