diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/meta | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/meta')
| -rw-r--r-- | crates/typst-library/src/meta/bibliography.rs | 724 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/context.rs | 220 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/counter.rs | 683 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/document.rs | 86 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/figure.rs | 351 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/footnote.rs | 299 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/heading.rs | 239 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/link.rs | 137 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/mod.rs | 64 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/numbering.rs | 525 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/outline.rs | 528 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/query.rs | 145 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/reference.rs | 276 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/state.rs | 440 |
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()) + } +} |
