diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-03-17 11:32:15 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-03-17 11:45:57 +0100 |
| commit | 312197b276748e1a17258ad21837850f582a467c (patch) | |
| tree | 3fd0c078a2673a98b74bc12b4d654a4c143b4e1f | |
| parent | e8435df5ec718e8ecc8a2ad48e4eb3ddd1f92a72 (diff) | |
Counters
54 files changed, 1043 insertions, 605 deletions
@@ -172,7 +172,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "comemo" version = "0.1.0" -source = "git+https://github.com/typst/comemo#36fb31c76eb42d67244bd9c7a2630c29767912f2" +source = "git+https://github.com/typst/comemo#9b520e8f5284d1c39d0bb13eb426f923972775f8" dependencies = [ "comemo-macros", "siphasher", @@ -181,7 +181,7 @@ dependencies = [ [[package]] name = "comemo-macros" version = "0.1.0" -source = "git+https://github.com/typst/comemo#36fb31c76eb42d67244bd9c7a2630c29767912f2" +source = "git+https://github.com/typst/comemo#9b520e8f5284d1c39d0bb13eb426f923972775f8" dependencies = [ "proc-macro2", "quote", @@ -1415,6 +1415,7 @@ dependencies = [ "roxmltree", "rustybuzz", "serde_json", + "smallvec", "syntect", "ttf-parser 0.18.1", "typed-arena", diff --git a/docs/src/reference/styling.md b/docs/src/reference/styling.md index 43585b13..2a50019e 100644 --- a/docs/src/reference/styling.md +++ b/docs/src/reference/styling.md @@ -86,8 +86,9 @@ fantasy encyclopedia. #show heading: it => block[ #set align(center) #set text(font: "Inria Serif") - \~ _#it.body;_ - #it.numbers \~ + \~ #emph(it.body) + #(counter(heading) + .get(it.numbering)) \~ ] = Dragon diff --git a/docs/src/tutorial/3-advanced.md b/docs/src/tutorial/3-advanced.md index 3784bc8e..7d6bccf6 100644 --- a/docs/src/tutorial/3-advanced.md +++ b/docs/src/tutorial/3-advanced.md @@ -56,14 +56,11 @@ Let's start by writing some set rules for the document. #set page( >>> margin: auto, paper: "us-letter", - header: align(right + horizon)[ + header: align(right)[ A fluid dynamic model for glacier flow ], - footer: nr => align( - center + horizon, - [#nr], - ), + numbering: "1", ) #lorem(600) @@ -73,23 +70,17 @@ You are already familiar with most of what is going on here. We set the text size to `{11pt}` and the font to Linux Libertine. We also enable paragraph justification and set the page size to US letter. -The `header` and `footer` arguments are new: With these, we can provide content -to fill the top and bottom margins of every page. In the header, we specify our -paper's title as requested by the conference style guide. We use the `align` -function to align the text to the right and the `horizon` keyword to make sure -that it is vertically centered in the margin. - -Because we need a page number in the footer, we have to put different content -onto each page. To do that, we can pass a -[custom function]($type/function) to the footer argument that defines -how the footer should look for a given page number. Typst provides the page -number to this function. Once more, we use the `align` function to center the -page number horizontally and vertically. +The `header` argument is new: With it, we can provide content to fill the top +margin of every page. In the header, we specify our paper's title as requested +by the conference style guide. We use the `align` function to align the text to +the right. -We have to put the page variable into square brackets and prefix it with a -hashtag because the align function expects -[content,]($type/content) but the page number is an -[integer]($type/integer). +Last but not least is the `numbering` argument. Here, we can provide a +[numbering pattern]($func/numbering) that defines how to number the pages. By +setting into to `{"1"}`, Typst only displays the bare page number. Setting it to +`{"(1/1)"}` would have displayed the current page and total number of pages +surrounded by parentheses. And we can even have provided a completely custom +function here to format things to our liking. ## Creating a title and abstract Now, let's add a title and an abstract. We'll start with the title. We center @@ -157,10 +148,7 @@ be set ragged and centered. >>> A fluid dynamic model for >>> glacier flow >>> ], ->>> footer: page => align( ->>> center+horizon, ->>> [#page] ->>> ), +>>> numbering: "1", >>> ) >>> >>> #align(center, text(17pt)[ @@ -213,17 +201,14 @@ keyword: >>> #set text(font: "Linux Libertine", 11pt) >>> #set par(justify: true) #set page( ->>> "us-letter", ->>> margin: auto, +>>> "us-letter", +>>> margin: auto, header: align( right + horizon, title ), <<< ... ->>> footer: page => align( ->>> center+horizon, ->>> [#page] ->>> ), +>>> numbering: "1", ) #align(center, text(17pt)[ @@ -289,10 +274,7 @@ content. In our case, it passes it on to the `columns` function. >>> right + horizon, >>> title >>> ), ->>> footer: page => align( ->>> center+horizon, ->>> [#page] ->>> ), +>>> numbering: "1", >>> ) >>> >>> #align(center, text( @@ -351,10 +333,7 @@ a way to set any of that, we need to write our own heading show rule. >>> right + horizon, >>> title >>> ), ->>> footer: page => align( ->>> center + horizon, ->>> [#page] ->>> ), +>>> numbering: "1", >>> ) #show heading: it => block[ #set align(center) @@ -430,10 +409,7 @@ differentiate between section and subsection headings: >>> right + horizon, >>> title >>> ), ->>> footer: page => align( ->>> center + horizon, ->>> [#page] ->>> ), +>>> numbering: "1", >>> ) >>> #show heading.where( diff --git a/docs/src/tutorial/4-template.md b/docs/src/tutorial/4-template.md index 47f9dfc6..d49ee6a1 100644 --- a/docs/src/tutorial/4-template.md +++ b/docs/src/tutorial/4-template.md @@ -275,10 +275,7 @@ path of the file after the `{from}` keyword. >>> right + horizon, >>> title >>> ), ->>> footer: page => align( ->>> center + horizon, ->>> [#page] ->>> ), +>>> numbering: "1", >>> ) >>> >>> show heading.where( diff --git a/library/Cargo.toml b/library/Cargo.toml index 3c1f79d4..98da25bd 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -23,6 +23,7 @@ once_cell = "1" roxmltree = "0.14" rustybuzz = "0.5" serde_json = "1" +smallvec = "1.10" syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } ttf-parser = "0.18.1" typed-arena = "2" diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index a48933a7..0b7f7eb1 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -145,7 +145,7 @@ impl Layout for BoxNode { } // Apply metadata. - frame.meta(styles); + frame.meta(styles, false); Ok(Fragment::frame(frame)) } @@ -336,7 +336,7 @@ impl Layout for BlockNode { // Measure to ensure frames for all regions have the same width. if sizing.x == Smart::Auto { let pod = Regions::one(size, Axes::splat(false)); - let frame = body.layout(vt, styles, pod)?.into_frame(); + let frame = body.measure(vt, styles, pod)?.into_frame(); size.x = frame.width(); expand.x = true; } @@ -389,7 +389,7 @@ impl Layout for BlockNode { // Apply metadata. for frame in &mut frames { - frame.meta(styles); + frame.meta(styles, false); } Ok(Fragment::frames(frames)) diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 33b297e7..1c08cd5f 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -100,7 +100,7 @@ pub struct EnumNode { /// [Ahead], /// ) /// ``` - #[default(NonZeroUsize::new(1).unwrap())] + #[default(NonZeroUsize::ONE)] pub start: NonZeroUsize, /// Whether to display the full numbering, including the numbers of @@ -180,7 +180,7 @@ impl Layout for EnumNode { let resolved = if full { parents.push(number); - let content = numbering.apply(vt.world(), &parents)?.display(); + let content = numbering.apply(vt.world, &parents)?.display(); parents.pop(); content } else { @@ -188,7 +188,7 @@ impl Layout for EnumNode { Numbering::Pattern(pattern) => { TextNode::packed(pattern.apply_kth(parents.len(), number)) } - other => other.apply(vt.world(), &[number])?.display(), + other => other.apply(vt.world, &[number])?.display(), } }; diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index 2671b983..096c575e 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -47,7 +47,16 @@ impl Layout for FlowNode { || child.is::<CircleNode>() || child.is::<ImageNode>() { - layouter.layout_single(vt, &child, styles)?; + let layoutable = child.with::<dyn Layout>().unwrap(); + layouter.layout_single(vt, layoutable, styles)?; + } else if child.is::<MetaNode>() { + let mut frame = Frame::new(Size::zero()); + frame.meta(styles, true); + layouter.items.push(FlowItem::Frame( + frame, + Axes::new(Align::Top, Align::Left), + true, + )); } else if child.can::<dyn Layout>() { layouter.layout_multiple(vt, &child, styles)?; } else if child.is::<ColbreakNode>() { @@ -173,14 +182,13 @@ impl<'a> FlowLayouter<'a> { fn layout_single( &mut self, vt: &mut Vt, - content: &Content, + content: &dyn Layout, styles: StyleChain, ) -> SourceResult<()> { let aligns = AlignNode::alignment_in(styles).resolve(styles); let sticky = BlockNode::sticky_in(styles); let pod = Regions::one(self.regions.base(), Axes::splat(false)); - let layoutable = content.with::<dyn Layout>().unwrap(); - let frame = layoutable.layout(vt, styles, pod)?.into_frame(); + let frame = content.layout(vt, styles, pod)?.into_frame(); self.layout_item(FlowItem::Frame(frame, aligns, sticky)); self.last_was_par = false; Ok(()) diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index b6e86afd..47d3ab86 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -386,7 +386,7 @@ impl<'a, 'v> GridLayouter<'a, 'v> { let size = Size::new(available, height); let pod = Regions::one(size, Axes::splat(false)); - let frame = cell.layout(self.vt, self.styles, pod)?.into_frame(); + let frame = cell.measure(self.vt, self.styles, pod)?.into_frame(); resolved.set_max(frame.width()); } } @@ -457,7 +457,7 @@ impl<'a, 'v> GridLayouter<'a, 'v> { let mut pod = self.regions; pod.size.x = rcol; - let frames = cell.layout(self.vt, self.styles, pod)?.into_frames(); + let frames = cell.measure(self.vt, self.styles, pod)?.into_frames(); if let [first, rest @ ..] = frames.as_slice() { skip |= first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs index 6d605868..c954ab67 100644 --- a/library/src/layout/list.rs +++ b/library/src/layout/list.rs @@ -128,7 +128,7 @@ impl Layout for ListNode { }; let depth = self.depth(styles); - let marker = self.marker(styles).resolve(vt.world(), depth)?; + let marker = self.marker(styles).resolve(vt.world, depth)?; let mut cells = vec![]; for item in self.children() { diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index eb440b7f..fc0279eb 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -47,10 +47,7 @@ use std::mem; use typed_arena::Arena; use typst::diag::SourceResult; -use typst::model::{ - applicable, realize, Content, Node, SequenceNode, StyleChain, StyleVecBuilder, - StyledNode, -}; +use typst::model::{applicable, realize, SequenceNode, StyleVecBuilder, StyledNode}; use crate::math::{FormulaNode, LayoutMath}; use crate::meta::DocumentNode; @@ -103,6 +100,22 @@ pub trait Layout { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment>; + + /// Layout without side effects. + /// + /// This node must be layouted again in the same order for the results to be + /// valid. + fn measure( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + vt.provider.save(); + let result = self.layout(vt, styles, regions); + vt.provider.restore(); + result + } } impl Layout for Content { @@ -417,7 +430,10 @@ impl<'a> FlowBuilder<'a> { let last_was_parbreak = self.1; self.1 = false; - if content.is::<VNode>() || content.is::<ColbreakNode>() { + if content.is::<VNode>() + || content.is::<ColbreakNode>() + || content.is::<MetaNode>() + { self.0.push(content.clone(), styles); return true; } @@ -457,7 +473,12 @@ struct ParBuilder<'a>(BehavedBuilder<'a>); impl<'a> ParBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::<SpaceNode>() + if content.is::<MetaNode>() { + if !self.0.is_basically_empty() { + self.0.push(content.clone(), styles); + return true; + } + } else if content.is::<SpaceNode>() || content.is::<TextNode>() || content.is::<HNode>() || content.is::<LinebreakNode>() diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index e469bf10..8ad76387 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -1,6 +1,8 @@ +use std::ptr; use std::str::FromStr; -use super::ColumnsNode; +use super::{AlignNode, ColumnsNode}; +use crate::meta::{Counter, CounterAction, CounterNode, Numbering}; use crate::prelude::*; /// Layouts its child onto one or multiple pages. @@ -130,7 +132,7 @@ pub struct PageNode { /// emissions and mitigate the impacts /// of a rapidly changing climate. /// ``` - #[default(NonZeroUsize::new(1).unwrap())] + #[default(NonZeroUsize::ONE)] pub columns: NonZeroUsize, /// The page's background color. @@ -147,49 +149,84 @@ pub struct PageNode { /// ``` pub fill: Option<Paint>, - /// The page's header. + /// How to [number]($func/numbering) the pages. /// - /// The header is placed in the top margin of each page. + /// If an explicit `footer` is given, the numbering is ignored. /// - /// - Content: The content will be placed in the header. - /// - A function: The function will be called with the page number (starting - /// at one) as its only argument. The content it returns will be placed in - /// the header. - /// - `{none}`: The header will be empty. + /// ```example + /// #set page( + /// height: 100pt, + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1 / 1", + /// ) + /// + /// #lorem(48) + /// ``` + pub numbering: Option<Numbering>, + + /// The alignment of the page numbering. /// /// ```example - /// #set par(justify: true) /// #set page( - /// margin: (x: 24pt, y: 32pt), - /// header: align(horizon + right, text(8pt)[_Exercise Sheet 3_]), + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1", + /// number-align: right, /// ) /// - /// #lorem(18) + /// #lorem(30) /// ``` - pub header: Option<Marginal>, + #[default(Align::Center.into())] + pub number_align: Axes<Option<GenAlign>>, - /// The page's footer. + /// The page's header. Fills the top margin of each page. /// - /// The footer is placed in the bottom margin of each page. + /// ```example + /// #set par(justify: true) + /// #set page( + /// margin: (top: 32pt, bottom: 20pt), + /// header: [ + /// #set text(8pt) + /// #smallcaps[Typst Academcy] + /// #h(1fr) _Exercise Sheet 3_ + /// ], + /// ) /// - /// - Content: The content will be placed in the footer. - /// - A function: The function will be called with the page number (starting - /// at one) as its only argument. The content it returns will be placed in - /// the footer. - /// - `{none}`: The footer will be empty. + /// #lorem(19) + /// ``` + pub header: Option<Content>, + + /// The amount the header is raised into the top margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + pub header_ascent: Rel<Length>, + + /// The page's footer. Fills the bottom margin of each page. + /// + /// For just a page number, the `numbering` property, typically suffices. If + /// you want to create a custom footer, but still display the page number, + /// you can directly access the [page counter]($func/counter). /// /// ```example /// #set par(justify: true) /// #set page( - /// margin: (x: 24pt, y: 32pt), - /// footer: i => align(horizon + right, - /// text(8pt, numbering("I", i)) - /// ) + /// height: 100pt, + /// margin: 20pt, + /// footer: [ + /// #set align(right) + /// #set text(8pt) + /// #counter(page).get("1") of + /// #counter(page).final("I") + /// ] /// ) /// - /// #lorem(18) + /// #lorem(48) /// ``` - pub footer: Option<Marginal>, + pub footer: Option<Content>, + + /// The amount the footer is lowered into the bottom margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + pub footer_descent: Rel<Length>, /// Content in the page's background. /// @@ -197,35 +234,30 @@ pub struct PageNode { /// used to place a background image or a watermark. /// /// ```example - /// #set page(background: align( - /// center + horizon, - /// rotate(24deg, - /// text(18pt, fill: rgb("FFCBC4"))[*CONFIDENTIAL*] - /// ), + /// #set page(background: rotate(24deg, + /// text(18pt, fill: rgb("FFCBC4"))[ + /// *CONFIDENTIAL* + /// ] /// )) /// /// = Typst's secret plans - /// - /// In the year 2023, we plan to take over the world - /// (of typesetting). + /// In the year 2023, we plan to take + /// over the world (of typesetting). /// ``` - pub background: Option<Marginal>, + pub background: Option<Content>, /// Content in the page's foreground. /// /// This content will overlay the page's body. /// /// ```example - /// #set page(foreground: align( - /// center + horizon, - /// text(24pt)[🥸], - /// )) + /// #set page(foreground: text(24pt)[🥸]) /// /// Reviewer 2 has marked our paper /// "Weak Reject" because they did /// not understand our approach... /// ``` - pub foreground: Option<Marginal>, + pub foreground: Option<Content>, /// The contents of the page(s). /// @@ -238,12 +270,7 @@ pub struct PageNode { impl PageNode { /// Layout the page run into a sequence of frames, one per page. - pub fn layout( - &self, - vt: &mut Vt, - mut page: usize, - styles: StyleChain, - ) -> SourceResult<Fragment> { + pub fn layout(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Fragment> { // When one of the lengths is infinite the page fits its content along // that axis. let width = self.width(styles).unwrap_or(Abs::inf()); @@ -278,10 +305,18 @@ impl PageNode { let mut fragment = child.layout(vt, styles, regions)?; let fill = self.fill(styles); - let header = self.header(styles); - let footer = self.footer(styles); let foreground = self.foreground(styles); let background = self.background(styles); + let header = self.header(styles); + let header_ascent = self.header_ascent(styles); + let footer = self.footer(styles).or_else(|| { + self.numbering(styles).map(|numbering| { + CounterNode::new(Counter::Page, CounterAction::Both(numbering)) + .pack() + .aligned(self.number_align(styles)) + }) + }); + let footer_descent = self.footer_descent(styles); // Realize overlays. for frame in &mut fragment { @@ -292,26 +327,38 @@ impl PageNode { let size = frame.size(); let pad = padding.resolve(styles).relative_to(size); let pw = size.x - pad.left - pad.right; - let py = size.y - pad.bottom; - for (marginal, pos, area) in [ - (&header, Point::with_x(pad.left), Size::new(pw, pad.top)), - (&footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), - (&foreground, Point::zero(), size), - (&background, Point::zero(), size), - ] { - let in_background = std::ptr::eq(marginal, &background); - let Some(marginal) = marginal else { continue }; - let content = marginal.resolve(vt, page)?; + for marginal in [&header, &footer, &background, &foreground] { + let Some(content) = marginal else { continue }; + + let (pos, area, align); + if ptr::eq(marginal, &header) { + let ascent = header_ascent.relative_to(pad.top); + pos = Point::with_x(pad.left); + area = Size::new(pw, pad.top - ascent); + align = Align::Bottom.into(); + } else if ptr::eq(marginal, &footer) { + let descent = footer_descent.relative_to(pad.bottom); + pos = Point::new(pad.left, size.y - pad.bottom + descent); + area = Size::new(pw, pad.bottom - descent); + align = Align::Top.into(); + } else { + pos = Point::zero(); + area = size; + align = Align::CENTER_HORIZON.into(); + }; + let pod = Regions::one(area, Axes::splat(true)); - let sub = content.layout(vt, styles, pod)?.into_frame(); - if in_background { + let sub = content + .clone() + .styled(AlignNode::set_alignment(align)) + .layout(vt, styles, pod)? + .into_frame(); + if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) { frame.prepend_frame(pos, sub); } else { frame.push_frame(pos, sub); } } - - page += 1; } Ok(fragment) @@ -358,7 +405,7 @@ impl Marginal { Self::Content(content) => content.clone(), Self::Func(func) => { let args = Args::new(func.span(), [Value::Int(page as i64)]); - func.call_detached(vt.world(), args)?.display() + func.call_detached(vt.world, args)?.display() } }) } diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 244a61a9..1906dd7c 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -325,6 +325,8 @@ enum Segment<'a> { Formula(&'a FormulaNode), /// A box with arbitrary content. Box(&'a BoxNode, bool), + /// Metadata. + Meta, } impl Segment<'_> { @@ -334,7 +336,7 @@ impl Segment<'_> { Self::Text(len) => len, Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Box(_, true) => SPACING_REPLACE.len_utf8(), - Self::Formula(_) | Self::Box(_, _) => NODE_REPLACE.len_utf8(), + Self::Formula(_) | Self::Box(_, _) | Self::Meta => NODE_REPLACE.len_utf8(), } } } @@ -599,6 +601,9 @@ fn collect<'a>( let frac = node.width(styles).is_fractional(); full.push(if frac { SPACING_REPLACE } else { NODE_REPLACE }); Segment::Box(node, frac) + } else if child.is::<MetaNode>() { + full.push(NODE_REPLACE); + Segment::Meta } else { bail!(child.span(), "unexpected paragraph child"); }; @@ -679,6 +684,11 @@ fn prepare<'a>( items.push(Item::Frame(frame)); } } + Segment::Meta => { + let mut frame = Frame::new(Size::zero()); + frame.meta(styles, true); + items.push(Item::Frame(frame)); + } } cursor = end; diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index 6daafdbb..024bb0d6 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -233,7 +233,7 @@ impl<T: Cast + Clone> Celled<T> { Self::Func(func) => { let args = Args::new(func.span(), [Value::Int(x as i64), Value::Int(y as i64)]); - func.call_detached(vt.world(), args)?.cast().at(func.span())? + func.call_detached(vt.world, args)?.cast().at(func.span())? } }) } diff --git a/library/src/lib.rs b/library/src/lib.rs index c4a421d2..b397bfb4 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -91,6 +91,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("figure", meta::FigureNode::id()); global.define("cite", meta::CiteNode::id()); global.define("bibliography", meta::BibliographyNode::id()); + global.define("counter", meta::counter); global.define("numbering", meta::numbering); // Symbols. @@ -224,5 +225,6 @@ fn items() -> LangItems { math::AccentNode::new(base, math::Accent::new(accent)).pack() }, math_frac: |num, denom| math::FracNode::new(num, denom).pack(), + counter_method: meta::counter_method, } } diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 0cc7f43c..6df1d87a 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -174,7 +174,7 @@ impl Layout for FormulaNode { // Find a math font. let variant = variant(styles); - let world = vt.world(); + let world = vt.world; let Some(font) = families(styles) .find_map(|family| { let id = world.book().select(family.as_str(), variant)?; diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index e9990524..64ac1f8e 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -3,12 +3,12 @@ use std::ffi::OsStr; use std::path::Path; use std::sync::Arc; -use ecow::EcoVec; +use ecow::{eco_vec, EcoVec}; use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting}; use hayagriva::Entry; -use super::LocalName; +use super::{LocalName, RefNode}; use crate::layout::{BlockNode, GridNode, ParNode, Sizing, TrackSizings, VNode}; use crate::meta::HeadingNode; use crate::prelude::*; @@ -65,7 +65,7 @@ impl BibliographyNode { vt.introspector .query(Selector::node::<Self>()) .into_iter() - .flat_map(|node| load(vt.world(), &node.to::<Self>().unwrap().path())) + .flat_map(|node| load(vt.world, &node.to::<Self>().unwrap().path())) .flatten() .any(|entry| entry.key() == key) } @@ -100,12 +100,6 @@ impl Show for BibliographyNode { const COLUMN_GUTTER: Em = Em::new(0.65); const INDENT: Em = Em::new(1.5); - let works = match Works::new(vt) { - Ok(works) => works, - Err(error) if vt.locatable() => bail!(self.span(), error), - Err(_) => Arc::new(Works::default()), - }; - let mut seq = vec![]; if let Some(title) = self.title(styles) { let title = title.clone().unwrap_or_else(|| { @@ -115,12 +109,18 @@ impl Show for BibliographyNode { seq.push( HeadingNode::new(title) - .with_level(NonZeroUsize::new(1).unwrap()) + .with_level(NonZeroUsize::ONE) .with_numbering(None) .pack(), ); } + if !vt.introspector.init() { + return Ok(Content::sequence(seq)); + } + + let works = Works::new(vt).at(self.span())?; + let row_gutter = BlockNode::below_in(styles).amount(); if works.references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; @@ -227,18 +227,17 @@ impl Synthesize for CiteNode { impl Show for CiteNode { fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - let id = self.0.stable_id().unwrap(); - let works = match Works::new(vt) { - Ok(works) => works, - Err(error) if vt.locatable() => bail!(self.span(), error), - Err(_) => Arc::new(Works::default()), - }; - - let Some(citation) = works.citations.get(&id).cloned() else { - return Ok(TextNode::packed("[1]")); - }; + if !vt.introspector.init() { + return Ok(Content::empty()); + } - citation + let works = Works::new(vt).at(self.span())?; + let id = self.0.stable_id().unwrap(); + works + .citations + .get(&id) + .cloned() + .flatten() .ok_or("bibliography does not contain this key") .at(self.span()) } @@ -264,17 +263,28 @@ pub enum CitationStyle { /// Fully formatted citations and references. #[derive(Default)] -pub struct Works { +struct Works { citations: HashMap<StableId, Option<Content>>, references: Vec<(Option<Content>, Content)>, } impl Works { /// Prepare all things need to cite a work or format a bibliography. - pub fn new(vt: &Vt) -> StrResult<Arc<Self>> { + fn new(vt: &Vt) -> StrResult<Arc<Self>> { let bibliography = BibliographyNode::find(vt.introspector)?; - let citations = vt.query_node::<CiteNode>().collect(); - Ok(create(vt.world(), &bibliography, citations)) + let citations = vt + .introspector + .query(Selector::Any(eco_vec![ + Selector::node::<RefNode>(), + Selector::node::<CiteNode>(), + ])) + .into_iter() + .map(|node| match node.to::<RefNode>() { + Some(reference) => reference.to_citation(StyleChain::default()), + _ => node.to::<CiteNode>().unwrap().clone(), + }) + .collect(); + Ok(create(vt.world, bibliography, citations)) } } @@ -282,8 +292,8 @@ impl Works { #[comemo::memoize] fn create( world: Tracked<dyn World>, - bibliography: &BibliographyNode, - citations: Vec<&CiteNode>, + bibliography: BibliographyNode, + citations: Vec<CiteNode>, ) -> Arc<Works> { let span = bibliography.span(); let entries = load(world, &bibliography.path()).unwrap(); @@ -294,7 +304,7 @@ fn create( .iter() .position(|entry| entry.key() == target.key()) .unwrap_or_default(); - bib_id.variant(i as u64) + bib_id.variant(i) }; let mut db = Database::new(); diff --git a/library/src/meta/counter.rs b/library/src/meta/counter.rs new file mode 100644 index 00000000..ab089d5e --- /dev/null +++ b/library/src/meta/counter.rs @@ -0,0 +1,337 @@ +use std::fmt::{self, Debug, Formatter, Write}; +use std::str::FromStr; + +use ecow::{eco_vec, EcoVec}; +use smallvec::{smallvec, SmallVec}; +use typst::eval::Dynamic; + +use super::{Numbering, NumberingPattern}; +use crate::layout::PageNode; +use crate::prelude::*; + +/// Count through pages, elements, and more. +/// +/// Display: Counter +/// Category: meta +/// Returns: content +#[func] +pub fn counter(key: Counter) -> Value { + Value::dynamic(key) +} + +/// Call a method on counter. +pub fn counter_method( + dynamic: &Dynamic, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<Value> { + let counter = dynamic.downcast::<Counter>().unwrap(); + let pattern = |s| NumberingPattern::from_str(s).unwrap().into(); + let action = match method { + "get" => CounterAction::Get(args.eat()?.unwrap_or_else(|| pattern("1.1"))), + "final" => CounterAction::Final(args.eat()?.unwrap_or_else(|| pattern("1.1"))), + "both" => CounterAction::Both(args.eat()?.unwrap_or_else(|| pattern("1/1"))), + "step" => CounterAction::Update(CounterUpdate::Step( + args.named("level")?.unwrap_or(NonZeroUsize::ONE), + )), + "update" => CounterAction::Update(args.expect("value or function")?), + _ => bail!(span, "type counter has no method `{}`", method), + }; + + args.finish()?; + + let content = CounterNode::new(counter.clone(), action).pack(); + Ok(Value::Content(content)) +} + +/// Executes an action on a counter. +/// +/// Display: Counter +/// Category: special +#[node(Locatable, Show)] +pub struct CounterNode { + /// The counter key. + #[required] + pub key: Counter, + + /// The action. + #[required] + pub action: CounterAction, +} + +impl Show for CounterNode { + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + match self.action() { + CounterAction::Get(numbering) => { + self.key().resolve(vt, self.0.stable_id(), &numbering) + } + CounterAction::Final(numbering) => self.key().resolve(vt, None, &numbering), + CounterAction::Both(numbering) => { + let both = match &numbering { + Numbering::Pattern(pattern) => pattern.pieces() >= 2, + _ => false, + }; + + let key = self.key(); + let id = self.0.stable_id(); + if !both { + return key.resolve(vt, id, &numbering); + } + + let sequence = key.sequence(vt.world, vt.introspector)?; + let numbers = [sequence.single(id), sequence.single(None)]; + Ok(numbering.apply(vt.world, &numbers)?.display()) + } + CounterAction::Update(_) => Ok(Content::empty()), + } + } +} + +/// The action to perform on a counter. +#[derive(Clone, PartialEq, Hash)] +pub enum CounterAction { + /// Displays the current value. + Get(Numbering), + /// Displays the final value. + Final(Numbering), + /// If given a pattern with at least two parts, displays the current value + /// together with the final value. Otherwise, displays just the current + /// value. + Both(Numbering), + /// Updates the value, possibly based on the previous one. + Update(CounterUpdate), +} + +cast_from_value! { + CounterAction: "counter action", +} + +impl Debug for CounterAction { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +/// An update to perform on a counter. +#[derive(Debug, 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), +} + +cast_from_value! { + CounterUpdate, + v: CounterState => Self::Set(v), + v: Func => Self::Func(v), +} + +/// Nodes that have special counting behaviour. +pub trait Count { + /// Get the counter update for this node. + fn update(&self) -> Option<CounterUpdate>; +} + +/// Counts through pages, elements, and more. +#[derive(Clone, PartialEq, Hash)] +pub enum Counter { + /// 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), +} + +impl Counter { + /// Display the value of the counter at the postition of the given stable + /// id. + pub fn resolve( + &self, + vt: &Vt, + stop: Option<StableId>, + numbering: &Numbering, + ) -> SourceResult<Content> { + let sequence = self.sequence(vt.world, vt.introspector)?; + let numbers = sequence.at(stop).0; + Ok(numbering.apply(vt.world, &numbers)?.display()) + } + + /// 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. + #[comemo::memoize] + fn sequence( + &self, + world: Tracked<dyn World>, + introspector: Tracked<Introspector>, + ) -> SourceResult<CounterSequence> { + let mut search = Selector::Node( + NodeId::of::<CounterNode>(), + Some(dict! { "key" => self.clone() }), + ); + + if let Counter::Selector(selector) = self { + search = Selector::Any(eco_vec![search, selector.clone()]); + } + + let mut state = CounterState::new(); + let mut stops = EcoVec::new(); + + let mut prev_page = NonZeroUsize::ONE; + let is_page = *self == Self::Page; + if is_page { + state.0.push(prev_page); + } + + for node in introspector.query(search) { + let id = node.stable_id().unwrap(); + if is_page { + let page = introspector.page(id); + let delta = page.get() - prev_page.get(); + if let Some(delta) = NonZeroUsize::new(delta) { + state.step(delta); + } + prev_page = page; + } + + if let Some(update) = match node.to::<CounterNode>() { + Some(counter) => match counter.action() { + CounterAction::Update(update) => Some(update), + _ => None, + }, + None => match node.with::<dyn Count>() { + Some(countable) => countable.update(), + None => Some(CounterUpdate::Step(NonZeroUsize::ONE)), + }, + } { + state.update(world, update)?; + } + + stops.push((id, state.clone())); + } + + Ok(CounterSequence { stops, is_page }) + } +} + +cast_from_value! { + Counter: "counter", + v: Str => Self::Str(v), + v: Selector => { + match v { + Selector::Node(id, _) => { + if id == NodeId::of::<PageNode>() { + return Ok(Self::Page); + } + + if !Content::new_of(id).can::<dyn Locatable>() { + Err(eco_format!("cannot count through {}s", id.name))?; + } + } + Selector::Label(_) => {} + Selector::Regex(_) => Err("cannot count through text")?, + Selector::Any(_) => {} + } + Self::Selector(v) + } +} + +impl Debug for Counter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("counter(")?; + match self { + Self::Page => f.pad("page")?, + Self::Selector(selector) => selector.fmt(f)?, + Self::Str(str) => str.fmt(f)?, + } + f.write_char(')') + } +} + +/// A sequence of counter values. +#[derive(Debug, Clone)] +struct CounterSequence { + stops: EcoVec<(StableId, CounterState)>, + is_page: bool, +} + +impl CounterSequence { + fn at(&self, stop: Option<StableId>) -> CounterState { + let entry = match stop { + Some(stop) => self.stops.iter().find(|&&(id, _)| id == stop), + None => self.stops.last(), + }; + + if let Some((_, state)) = entry { + return state.clone(); + } + + if self.is_page { + return CounterState(smallvec![NonZeroUsize::ONE]); + } + + CounterState::default() + } + + fn single(&self, stop: Option<StableId>) -> NonZeroUsize { + self.at(stop).0.first().copied().unwrap_or(NonZeroUsize::ONE) + } +} + +/// Counts through elements with different levels. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct CounterState(pub SmallVec<[NonZeroUsize; 3]>); + +impl CounterState { + /// Create a new levelled counter. + pub fn new() -> Self { + Self::default() + } + + /// Advance the counter and return the numbers for the given heading. + pub fn update( + &mut self, + world: Tracked<dyn World>, + update: CounterUpdate, + ) -> SourceResult<()> { + match update { + CounterUpdate::Set(state) => *self = state, + CounterUpdate::Step(level) => self.step(level), + CounterUpdate::Func(func) => { + let args = Args::new(func.span(), self.0.iter().copied().map(Into::into)); + *self = func.call_detached(world, args)?.cast().at(func.span())? + } + } + Ok(()) + } + + /// Advance the top level number by the specified amount. + pub fn step(&mut self, level: NonZeroUsize) { + let level = level.get(); + + if self.0.len() >= level { + self.0[level - 1] = self.0[level - 1].saturating_add(1); + self.0.truncate(level); + } + + while self.0.len() < level { + self.0.push(NonZeroUsize::ONE); + } + } +} + +cast_from_value! { + CounterState, + num: NonZeroUsize => Self(smallvec![num]), + array: Array => Self(array + .into_iter() + .map(Value::cast) + .collect::<StrResult<_>>()?), +} diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index ae29e7a8..75e78184 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -45,8 +45,7 @@ impl LayoutRoot for DocumentNode { } if let Some(page) = child.to::<PageNode>() { - let number = 1 + pages.len(); - let fragment = page.layout(vt, number, styles)?; + let fragment = page.layout(vt, styles)?; pages.extend(fragment); } else { bail!(child.span(), "unexpected document child"); diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index a7668ffb..3c3f6361 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -1,7 +1,10 @@ use std::str::FromStr; -use super::{LocalName, Numbering, NumberingPattern}; -use crate::layout::{BlockNode, TableNode, VNode}; +use super::{ + Count, Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering, + NumberingPattern, +}; +use crate::layout::{BlockNode, VNode}; use crate::prelude::*; use crate::text::TextNode; @@ -23,7 +26,7 @@ use crate::text::TextNode; /// /// Display: Figure /// Category: meta -#[node(Locatable, Synthesize, Show, LocalName)] +#[node(Locatable, Synthesize, Count, Show, LocalName)] pub struct FigureNode { /// The content of the figure. Often, an [image]($func/image). #[required] @@ -34,60 +37,34 @@ pub struct FigureNode { /// How to number the figure. Accepts a /// [numbering pattern or function]($func/numbering). - #[default(Some(Numbering::Pattern(NumberingPattern::from_str("1").unwrap())))] + #[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, - - /// The figure's number. - #[synthesized] - pub number: Option<NonZeroUsize>, -} - -impl FigureNode { - fn element(&self) -> NodeId { - let mut id = self.body().id(); - if id != NodeId::of::<TableNode>() { - id = NodeId::of::<Self>(); - } - id - } } impl Synthesize for FigureNode { - fn synthesize(&mut self, vt: &Vt, styles: StyleChain) { - let my_id = self.0.stable_id(); - let element = self.element(); - - let mut number = None; - let numbering = self.numbering(styles); - if numbering.is_some() { - number = NonZeroUsize::new( - 1 + vt - .query_node::<Self>() - .take_while(|figure| figure.0.stable_id() != my_id) - .filter(|figure| figure.element() == element) - .count(), - ); - } - - self.push_number(number); - self.push_numbering(numbering); + fn synthesize(&mut self, _: &Vt, styles: StyleChain) { + self.push_numbering(self.numbering(styles)); } } impl Show for FigureNode { - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let mut realized = self.body(); if let Some(mut caption) = self.caption(styles) { if let Some(numbering) = self.numbering(styles) { - let number = self.number().unwrap(); let name = self.local_name(TextNode::lang_in(styles)); caption = TextNode::packed(eco_format!("{name}\u{a0}")) - + numbering.apply(vt.world(), &[number])?.display() + + CounterNode::new( + Counter::Selector(Selector::node::<Self>()), + CounterAction::Get(numbering), + ) + .pack() + .spanned(self.span()) + TextNode::packed(": ") + caption; } @@ -104,13 +81,16 @@ impl Show for FigureNode { } } +impl Count for FigureNode { + fn update(&self) -> Option<CounterUpdate> { + self.numbering(StyleChain::default()) + .is_some() + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + impl LocalName for FigureNode { fn local_name(&self, lang: Lang) -> &'static str { - let body = self.body(); - if body.is::<TableNode>() { - return body.with::<dyn LocalName>().unwrap().local_name(lang); - } - match lang { Lang::GERMAN => "Abbildung", Lang::ENGLISH | _ => "Figure", diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 527a93a3..614200b8 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -1,7 +1,8 @@ use typst::font::FontWeight; -use super::{LocalName, Numbering}; +use super::{Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering}; use crate::layout::{BlockNode, HNode, VNode}; +use crate::meta::Count; use crate::prelude::*; use crate::text::{TextNode, TextSize}; @@ -40,10 +41,10 @@ use crate::text::{TextNode, TextSize}; /// /// Display: Heading /// Category: meta -#[node(Locatable, Synthesize, Show, Finalize, LocalName)] +#[node(Locatable, Synthesize, Count, Show, Finalize, LocalName)] pub struct HeadingNode { /// The logical nesting depth of the heading, starting from one. - #[default(NonZeroUsize::new(1).unwrap())] + #[default(NonZeroUsize::ONE)] pub level: NonZeroUsize, /// How to number the heading. Accepts a @@ -76,46 +77,26 @@ pub struct HeadingNode { /// The heading's title. #[required] pub body: Content, - - /// The heading's numbering numbers. - #[synthesized] - pub numbers: Option<Vec<NonZeroUsize>>, } impl Synthesize for HeadingNode { - fn synthesize(&mut self, vt: &Vt, styles: StyleChain) { - let my_id = self.0.stable_id(); - let numbering = self.numbering(styles); - - let mut counter = HeadingCounter::new(); - if numbering.is_some() { - // Advance past existing headings. - for heading in vt - .query_node::<Self>() - .take_while(|figure| figure.0.stable_id() != my_id) - { - if heading.numbering(StyleChain::default()).is_some() { - counter.advance(heading); - } - } - - // Advance passed self. - counter.advance(self); - } - + fn synthesize(&mut self, _: &Vt, styles: StyleChain) { self.push_level(self.level(styles)); + self.push_numbering(self.numbering(styles)); self.push_outlined(self.outlined(styles)); - self.push_numbers(numbering.is_some().then(|| counter.take())); - self.push_numbering(numbering); } } impl Show for HeadingNode { - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let mut realized = self.body(); if let Some(numbering) = self.numbering(styles) { - let numbers = self.numbers().unwrap(); - realized = numbering.apply(vt.world(), &numbers)?.display() + realized = CounterNode::new( + Counter::Selector(Selector::node::<Self>()), + CounterAction::Get(numbering), + ) + .pack() + .spanned(self.span()) + HNode::new(Em::new(0.3).into()).with_weak(true).pack() + realized; } @@ -146,34 +127,11 @@ impl Finalize for HeadingNode { } } -/// Counts through headings with different levels. -pub struct HeadingCounter(Vec<NonZeroUsize>); - -impl HeadingCounter { - /// Create a new heading counter. - pub fn new() -> Self { - Self(vec![]) - } - - /// Advance the counter and return the numbers for the given heading. - pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] { - let level = heading.level(StyleChain::default()).get(); - - if self.0.len() >= level { - self.0[level - 1] = self.0[level - 1].saturating_add(1); - self.0.truncate(level); - } - - while self.0.len() < level { - self.0.push(NonZeroUsize::new(1).unwrap()); - } - - &self.0 - } - - /// Take out the current counts. - pub fn take(self) -> Vec<NonZeroUsize> { - self.0 +impl Count for HeadingNode { + fn update(&self) -> Option<CounterUpdate> { + self.numbering(StyleChain::default()) + .is_some() + .then(|| CounterUpdate::Step(self.level(StyleChain::default()))) } } diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index ba74dac0..a7de2dad 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -1,6 +1,7 @@ //! Interaction between document parts. mod bibliography; +mod counter; mod document; mod figure; mod heading; @@ -10,6 +11,7 @@ mod outline; mod reference; pub use self::bibliography::*; +pub use self::counter::*; pub use self::document::*; pub use self::figure::*; pub use self::heading::*; diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index c59766c8..6febc408 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -1,5 +1,7 @@ use std::str::FromStr; +use ecow::EcoVec; + use crate::prelude::*; use crate::text::Case; @@ -66,7 +68,7 @@ pub fn numbering( } /// How to number a sequence of things. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, PartialEq, Hash)] pub enum Numbering { /// A pattern with prefix, numbering, lower / upper case and suffix. Pattern(NumberingPattern), @@ -82,7 +84,7 @@ impl Numbering { numbers: &[NonZeroUsize], ) -> SourceResult<Value> { Ok(match self { - Self::Pattern(pattern) => Value::Str(pattern.apply(numbers, false).into()), + Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), Self::Func(func) => { let args = Args::new( func.span(), @@ -92,6 +94,20 @@ impl Numbering { } }) } + + /// 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_from_value! { @@ -118,20 +134,21 @@ cast_to_value! { /// - `(I)` #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct NumberingPattern { - pieces: Vec<(EcoString, NumberingKind, Case)>, + pieces: EcoVec<(EcoString, NumberingKind, Case)>, suffix: EcoString, + trimmed: bool, } impl NumberingPattern { /// Apply the pattern to the given number. - pub fn apply(&self, numbers: &[NonZeroUsize], trimmed: bool) -> EcoString { + pub fn apply(&self, numbers: &[NonZeroUsize]) -> EcoString { let mut fmt = EcoString::new(); let mut numbers = numbers.into_iter(); for (i, ((prefix, kind, case), &n)) in self.pieces.iter().zip(&mut numbers).enumerate() { - if i > 0 || !trimmed { + if i > 0 || !self.trimmed { fmt.push_str(prefix); } fmt.push_str(&kind.apply(n, *case)); @@ -148,7 +165,7 @@ impl NumberingPattern { fmt.push_str(&kind.apply(n, *case)); } - if !trimmed { + if !self.trimmed { fmt.push_str(&self.suffix); } @@ -172,13 +189,18 @@ impl NumberingPattern { 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 = vec![]; + let mut pieces = EcoVec::new(); let mut handled = 0; for (i, c) in pattern.char_indices() { @@ -197,7 +219,7 @@ impl FromStr for NumberingPattern { Err("invalid numbering pattern")?; } - Ok(Self { pieces, suffix }) + Ok(Self { pieces, suffix, trimmed: false }) } } diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 1a3e8606..933119ec 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -1,4 +1,4 @@ -use super::{HeadingNode, LocalName}; +use super::{Counter, HeadingNode, LocalName}; use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode}; use crate::prelude::*; use crate::text::{LinebreakNode, SpaceNode, TextNode}; @@ -22,7 +22,7 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode}; /// /// Display: Outline /// Category: meta -#[node(Synthesize, Show, LocalName)] +#[node(Show, LocalName)] pub struct OutlineNode { /// The title of the outline. /// @@ -67,26 +67,6 @@ pub struct OutlineNode { /// ``` #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))] pub fill: Option<Content>, - - /// All outlined headings in the document. - #[synthesized] - pub headings: Vec<HeadingNode>, -} - -impl Synthesize for OutlineNode { - fn synthesize(&mut self, vt: &Vt, _: StyleChain) { - let headings = vt - .introspector - .query(Selector::Node( - NodeId::of::<HeadingNode>(), - Some(dict! { "outlined" => true }), - )) - .into_iter() - .map(|node| node.to::<HeadingNode>().unwrap().clone()) - .collect(); - - self.push_headings(headings); - } } impl Show for OutlineNode { @@ -100,7 +80,7 @@ impl Show for OutlineNode { seq.push( HeadingNode::new(title) - .with_level(NonZeroUsize::new(1).unwrap()) + .with_level(NonZeroUsize::ONE) .with_numbering(None) .with_outlined(false) .pack(), @@ -111,7 +91,11 @@ impl Show for OutlineNode { let depth = self.depth(styles); let mut ancestors: Vec<&HeadingNode> = vec![]; - for heading in self.headings().iter() { + for node in vt.introspector.query(Selector::Node( + NodeId::of::<HeadingNode>(), + Some(dict! { "outlined" => true }), + )) { + let heading = node.to::<HeadingNode>().unwrap(); let stable_id = heading.0.stable_id().unwrap(); if !heading.outlined(StyleChain::default()) { continue; @@ -134,9 +118,9 @@ impl Show for OutlineNode { let mut hidden = Content::empty(); for ancestor in &ancestors { if let Some(numbering) = ancestor.numbering(StyleChain::default()) { - let numbers = ancestor.numbers().unwrap(); - hidden += numbering.apply(vt.world(), &numbers)?.display() - + SpaceNode::new().pack(); + let numbers = Counter::Selector(Selector::node::<HeadingNode>()) + .resolve(vt, ancestor.0.stable_id(), &numbering)?; + hidden += numbers + SpaceNode::new().pack(); }; } @@ -149,10 +133,9 @@ impl Show for OutlineNode { // Format the numbering. let mut start = heading.body(); if let Some(numbering) = heading.numbering(StyleChain::default()) { - let numbers = heading.numbers().unwrap(); - start = numbering.apply(vt.world(), &numbers)?.display() - + SpaceNode::new().pack() - + start; + let numbers = Counter::Selector(Selector::node::<HeadingNode>()) + .resolve(vt, Some(stable_id), &numbering)?; + start = numbers + SpaceNode::new().pack() + start; }; // Add the numbering and section name. @@ -173,8 +156,8 @@ impl Show for OutlineNode { } // Add the page number and linebreak. - let page = vt.introspector.page(stable_id).unwrap(); - let end = TextNode::packed(eco_format!("{}", page)); + let page = vt.introspector.page(stable_id); + let end = TextNode::packed(eco_format!("{page}")); seq.push(end.linked(Link::Node(stable_id))); seq.push(LinebreakNode::new().pack()); ancestors.push(heading); diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 1616adb3..095a846c 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,4 +1,4 @@ -use super::{BibliographyNode, CiteNode, FigureNode, HeadingNode, LocalName, Numbering}; +use super::{BibliographyNode, CiteNode, Counter, LocalName, Numbering}; use crate::prelude::*; use crate::text::TextNode; @@ -35,7 +35,7 @@ use crate::text::TextNode; /// /// Display: Reference /// Category: meta -#[node(Show)] +#[node(Locatable, Show)] pub struct RefNode { /// The target label that should be referenced. #[required] @@ -65,40 +65,36 @@ pub struct RefNode { impl Show for RefNode { fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let target = self.target(); - let supplement = self.supplement(styles); + if !vt.introspector.init() { + return Ok(Content::empty()); + } + let target = self.target(); let matches = vt.introspector.query(Selector::Label(self.target())); - if !vt.locatable() || BibliographyNode::has(vt, &target.0) { + if BibliographyNode::has(vt, &target.0) { if !matches.is_empty() { bail!(self.span(), "label occurs in the document and its bibliography"); } - return Ok(CiteNode::new(vec![target.0]) - .with_supplement(match supplement { - Smart::Custom(Some(Supplement::Content(content))) => Some(content), - _ => None, - }) - .pack() - .spanned(self.span())); + return self.to_citation(styles).show(vt, styles); } - let &[target] = matches.as_slice() else { - if vt.locatable() { - bail!(self.span(), if matches.is_empty() { - "label does not exist in the document" - } else { - "label occurs multiple times in the document" - }); + let &[node] = matches.as_slice() else { + bail!(self.span(), if matches.is_empty() { + "label does not exist in the document" } else { - return Ok(Content::empty()); - } + "label occurs multiple times in the document" + }); }; + if !node.can::<dyn Locatable>() { + bail!(self.span(), "cannot reference {}", node.id().name); + } + let supplement = self.supplement(styles); let mut supplement = match supplement { - Smart::Auto => target + Smart::Auto => node .with::<dyn LocalName>() .map(|node| node.local_name(TextNode::lang_in(styles))) .map(TextNode::packed) @@ -106,8 +102,8 @@ impl Show for RefNode { Smart::Custom(None) => Content::empty(), Smart::Custom(Some(Supplement::Content(content))) => content.clone(), Smart::Custom(Some(Supplement::Func(func))) => { - let args = Args::new(func.span(), [target.clone().into()]); - func.call_detached(vt.world(), args)?.display() + let args = Args::new(func.span(), [node.clone().into()]); + func.call_detached(vt.world, args)?.display() } }; @@ -115,42 +111,31 @@ impl Show for RefNode { supplement += TextNode::packed('\u{a0}'); } - let formatted = if let Some(heading) = target.to::<HeadingNode>() { - if let Some(numbering) = heading.numbering(StyleChain::default()) { - let numbers = heading.numbers().unwrap(); - numbered(vt, supplement, &numbering, &numbers)? - } else { - bail!(self.span(), "cannot reference unnumbered heading"); - } - } else if let Some(figure) = target.to::<FigureNode>() { - if let Some(numbering) = figure.numbering(StyleChain::default()) { - let number = figure.number().unwrap(); - numbered(vt, supplement, &numbering, &[number])? - } else { - bail!(self.span(), "cannot reference unnumbered figure"); - } - } else { - bail!(self.span(), "cannot reference {}", target.id().name); + let Some(numbering) = node.cast_field::<Numbering>("numbering") else { + bail!(self.span(), "only numbered elements can be referenced"); }; - Ok(formatted.linked(Link::Node(target.stable_id().unwrap()))) + let numbers = Counter::Selector(Selector::Node(node.id(), None)).resolve( + vt, + node.stable_id(), + &numbering.trimmed(), + )?; + + Ok((supplement + numbers).linked(Link::Node(node.stable_id().unwrap()))) } } -/// Generate a numbered reference like "Section 1.1". -fn numbered( - vt: &Vt, - prefix: Content, - numbering: &Numbering, - numbers: &[NonZeroUsize], -) -> SourceResult<Content> { - Ok(prefix - + match numbering { - Numbering::Pattern(pattern) => { - TextNode::packed(pattern.apply(&numbers, true)) - } - Numbering::Func(_) => numbering.apply(vt.world(), &numbers)?.display(), - }) +impl RefNode { + /// Turn the rference into a citation. + pub fn to_citation(&self, styles: StyleChain) -> CiteNode { + let mut node = CiteNode::new(vec![self.target().0]); + node.push_supplement(match self.supplement(styles) { + Smart::Custom(Some(Supplement::Content(content))) => Some(content), + _ => None, + }); + node.0.set_stable_id(self.0.stable_id().unwrap()); + node + } } /// Additional content for a reference. diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 36f7cc89..55e5f17b 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -22,16 +22,18 @@ pub use typst::eval::{ pub use typst::geom::*; #[doc(no_inline)] pub use typst::model::{ - node, Construct, Content, Finalize, Fold, Introspector, Label, Locatable, Node, - NodeId, Resolve, Selector, Set, Show, StabilityProvider, StableId, StyleChain, - StyleMap, StyleVec, Synthesize, Unlabellable, Vt, + node, Behave, Behaviour, Construct, Content, Finalize, Fold, Introspector, Label, + Locatable, MetaNode, Node, NodeId, Resolve, Selector, Set, Show, StabilityProvider, + StableId, StyleChain, StyleMap, StyleVec, Synthesize, Unlabellable, Vt, }; #[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; #[doc(no_inline)] +pub use typst::util::NonZeroExt; +#[doc(no_inline)] pub use typst::World; #[doc(no_inline)] pub use crate::layout::{Fragment, Layout, Regions}; #[doc(no_inline)] -pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt}; +pub use crate::shared::{ContentExt, StyleMapExt}; diff --git a/library/src/shared/behave.rs b/library/src/shared/behave.rs index 74c4d151..eff41c0b 100644 --- a/library/src/shared/behave.rs +++ b/library/src/shared/behave.rs @@ -1,38 +1,9 @@ //! Node interaction. -use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder}; - -/// How a node interacts with other nodes. -pub trait Behave { - /// The node's interaction behaviour. - fn behaviour(&self) -> Behaviour; - - /// Whether this weak node is larger than a previous one and thus picked as - /// the maximum when the levels are the same. - #[allow(unused_variables)] - fn larger(&self, prev: &Content) -> bool { - false - } -} - -/// How a node interacts with other nodes in a stream. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Behaviour { - /// A weak node which only survives when a supportive node is before and - /// after it. Furthermore, per consecutive run of weak nodes, only one - /// survives: The one with the lowest weakness level (or the larger one if - /// there is a tie). - Weak(usize), - /// A node that enables adjacent weak nodes to exist. The default. - Supportive, - /// A node that destroys adjacent weak nodes. - Destructive, - /// A node that does not interact at all with other nodes, having the - /// same effect as if it didn't exist. - Ignorant, -} +use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder}; /// A wrapper around a [`StyleVecBuilder`] that allows items to interact. +#[derive(Debug)] pub struct BehavedBuilder<'a> { /// The internal builder. builder: StyleVecBuilder<'a, Content>, @@ -53,11 +24,21 @@ impl<'a> BehavedBuilder<'a> { } } - /// Whether the builder is empty. + /// Whether the builder is totally empty. pub fn is_empty(&self) -> bool { self.builder.is_empty() && self.staged.is_empty() } + /// Whether the builder is empty except for some weak items that will + /// probably collapse. + pub fn is_basically_empty(&self) -> bool { + self.builder.is_empty() + && self + .staged + .iter() + .all(|(_, behaviour, _)| matches!(behaviour, Behaviour::Weak(_))) + } + /// Push an item into the sequence. pub fn push(&mut self, item: Content, styles: StyleChain<'a>) { let interaction = item diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index 53e90bc5..244e7afe 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -136,7 +136,7 @@ impl<'a> ShapedText<'a> { } // Apply metadata. - frame.meta(self.styles); + frame.meta(self.styles, false); frame } @@ -159,7 +159,7 @@ impl<'a> ShapedText<'a> { if self.glyphs.is_empty() { // When there are no glyphs, we just use the vertical metrics of the // first available font. - let world = vt.world(); + let world = vt.world; for family in families(self.styles) { if let Some(font) = world .book() @@ -228,7 +228,7 @@ impl<'a> ShapedText<'a> { /// Push a hyphen to end of the text. pub fn push_hyphen(&mut self, vt: &Vt) { families(self.styles).find_map(|family| { - let world = vt.world(); + let world = vt.world; let font = world .book() .select(family.as_str(), self.variant) @@ -389,7 +389,7 @@ fn shape_segment<'a>( } // Find the next available family. - let world = ctx.vt.world(); + let world = ctx.vt.world; let book = world.book(); let mut selection = families.find_map(|family| { book.select(family.as_str(), ctx.variant) diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs index 20cfaa49..74bb70c7 100644 --- a/library/src/text/shift.rs +++ b/library/src/text/shift.rs @@ -151,7 +151,7 @@ fn search_text(content: &Content, sub: bool) -> Option<EcoString> { /// Checks whether the first retrievable family contains all code points of the /// given string. fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool { - let world = vt.world(); + let world = vt.world; for family in TextNode::font_in(styles) { if let Some(font) = world .book() diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs index 129e07c5..c5016436 100644 --- a/library/src/visualize/image.rs +++ b/library/src/visualize/image.rs @@ -53,7 +53,7 @@ impl Layout for ImageNode { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let image = load(vt.world(), &self.path()).unwrap(); + let image = load(vt.world, &self.path()).unwrap(); let sizing = Axes::new(self.width(styles), self.height(styles)); let region = sizing .zip(regions.base()) @@ -106,7 +106,7 @@ impl Layout for ImageNode { } // Apply metadata. - frame.meta(styles); + frame.meta(styles, false); Ok(Fragment::frame(frame)) } diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs index de459949..8aef3629 100644 --- a/library/src/visualize/shape.rs +++ b/library/src/visualize/shape.rs @@ -536,7 +536,7 @@ fn layout( } // Apply metadata. - frame.meta(styles); + frame.meta(styles, false); Ok(Fragment::frame(frame)) } diff --git a/macros/src/node.rs b/macros/src/node.rs index dfbd9078..1d05b9a6 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -326,12 +326,12 @@ fn create_set_field_method(field: &Field) -> TokenStream { let doc = format!("Create a style property for the `{}` field.", name); quote! { #[doc = #doc] - #vis fn #set_ident(#ident: #ty) -> ::typst::model::Property { - ::typst::model::Property::new( + #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { + ::typst::model::Style::Property(::typst::model::Property::new( ::typst::model::NodeId::of::<Self>(), #name.into(), #ident.into() - ) + )) } } } @@ -14,7 +14,7 @@ use crate::geom::{ Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, }; use crate::image::Image; -use crate::model::{node, Content, Fold, Introspector, StableId, StyleChain}; +use crate::model::{Content, Introspector, MetaNode, StableId, StyleChain}; use crate::syntax::Span; /// A finished document with metadata and page frames. @@ -271,16 +271,15 @@ impl Frame { } /// Attach the metadata from this style chain to the frame. - pub fn meta(&mut self, styles: StyleChain) { - if self.is_empty() { - return; - } - for meta in MetaNode::data_in(styles) { - if matches!(meta, Meta::Hide) { - self.clear(); - break; + pub fn meta(&mut self, styles: StyleChain, force: bool) { + if force || !self.is_empty() { + for meta in MetaNode::data_in(styles) { + if matches!(meta, Meta::Hide) { + self.clear(); + break; + } + self.prepend(Point::zero(), Element::Meta(meta, self.size)); } - self.prepend(Point::zero(), Element::Meta(meta, self.size)); } } @@ -607,6 +606,16 @@ pub enum Meta { Node(Content), } +cast_from_value! { + Meta: "meta", +} + +impl PartialEq for Meta { + fn eq(&self, other: &Self) -> bool { + crate::util::hash128(self) == crate::util::hash128(other) + } +} + /// A possibly unresolved link. #[derive(Debug, Clone, Hash)] pub enum Link { @@ -623,45 +632,14 @@ impl Link { pub fn resolve<'a>( &self, introspector: impl FnOnce() -> &'a Introspector, - ) -> Option<Destination> { + ) -> Destination { match self { - Self::Dest(dest) => Some(dest.clone()), - Self::Node(id) => introspector().location(*id).map(Destination::Internal), + Self::Dest(dest) => dest.clone(), + Self::Node(id) => Destination::Internal(introspector().location(*id)), } } } -/// Host for metadata. -/// -/// Display: Meta -/// Category: special -#[node] -pub struct MetaNode { - /// Metadata that should be attached to all elements affected by this style - /// property. - #[fold] - pub data: Vec<Meta>, -} - -impl Fold for Vec<Meta> { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.extend(outer); - self - } -} - -cast_from_value! { - Meta: "meta", -} - -impl PartialEq for Meta { - fn eq(&self, other: &Self) -> bool { - crate::util::hash128(self) == crate::util::hash128(other) - } -} - /// A link destination. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { diff --git a/src/eval/library.rs b/src/eval/library.rs index 1240d9bb..45c23d17 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -6,11 +6,12 @@ use comemo::Tracked; use ecow::EcoString; use once_cell::sync::OnceCell; -use super::Module; +use super::{Args, Dynamic, Module, Value}; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt}; +use crate::syntax::Span; use crate::util::hash128; use crate::World; @@ -89,6 +90,14 @@ pub struct LangItems { pub math_accent: fn(base: Content, accent: char) -> Content, /// A fraction in a formula: `x/2`. pub math_frac: fn(num: Content, denom: Content) -> Content, + /// Dispatch a method on a counter. This is hacky and should be superseded + /// by more dynamic method dispatch. + pub counter_method: fn( + dynamic: &Dynamic, + method: &str, + args: Args, + span: Span, + ) -> SourceResult<Value>, } impl Debug for LangItems { diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 197a2f65..a449ac16 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -134,6 +134,14 @@ pub fn call( _ => return missing(), }, + Value::Dyn(dynamic) => { + if dynamic.type_name() == "counter" { + return (vm.items.counter_method)(&dynamic, method, args, span); + } + + return missing(); + } + _ => return missing(), }; @@ -281,6 +289,13 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ], "function" => &[("where", true), ("with", true)], "arguments" => &[("named", false), ("pos", false)], + "counter" => &[ + ("get", true), + ("final", true), + ("both", true), + ("step", true), + ("update", true), + ], _ => &[], } } diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index df7b517f..5347d831 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -114,11 +114,7 @@ fn write_page(ctx: &mut PdfContext, page: Page) { let mut annotation = annotations.push(); annotation.subtype(AnnotationType::Link).rect(rect); annotation.border(0.0, 0.0, 0.0, None); - - let dest = link.resolve(|| &ctx.introspector); - let Some(dest) = dest else { continue }; - - match dest { + match link.resolve(|| &ctx.introspector) { Destination::Url(uri) => { annotation .action() diff --git a/src/geom/align.rs b/src/geom/align.rs index b14e6775..239a6e70 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -143,6 +143,26 @@ cast_to_value! { } } +impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { + fn from(axes: Axes<GenAlign>) -> Self { + axes.map(Some) + } +} + +impl From<Axes<Align>> for Axes<Option<GenAlign>> { + fn from(axes: Axes<Align>) -> Self { + axes.map(GenAlign::Specific).into() + } +} + +impl From<Align> for Axes<Option<GenAlign>> { + fn from(align: Align) -> Self { + let mut axes = Axes::splat(None); + axes.set(align.axis(), Some(align.into())); + axes + } +} + impl Resolve for GenAlign { type Output = Align; diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index ed868e53..ccb89a9c 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -78,7 +78,7 @@ pub fn analyze_labels( let items = &world.library().items; // Labels in the document. - for node in introspector.nodes() { + for node in introspector.all() { let Some(label) = node.label() else { continue }; let details = node .field("caption") diff --git a/src/ide/jump.rs b/src/ide/jump.rs index 0aa97b56..17e318a7 100644 --- a/src/ide/jump.rs +++ b/src/ide/jump.rs @@ -36,12 +36,9 @@ pub fn jump_from_click( for (pos, element) in frame.elements() { if let Element::Meta(Meta::Link(link), size) = element { if is_in_rect(*pos, *size, click) { - let dest = link.resolve(|| { + return Some(Jump::Dest(link.resolve(|| { introspector.get_or_insert_with(|| Introspector::new(frames)) - }); - - let Some(dest) = dest else { continue }; - return Some(Jump::Dest(dest)); + }))); } } } diff --git a/src/model/content.rs b/src/model/content.rs index 58b80487..11ad635f 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -8,8 +8,12 @@ use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; use once_cell::sync::Lazy; -use super::{node, Guard, Locatable, Recipe, StableId, Style, StyleMap, Synthesize}; +use super::{ + node, Behave, Behaviour, Fold, Guard, Locatable, Recipe, StableId, Style, StyleMap, + Synthesize, +}; use crate::diag::{SourceResult, StrResult}; +use crate::doc::Meta; use crate::eval::{ cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm, }; @@ -35,9 +39,15 @@ enum Modifier { } impl Content { + /// Create a content of the given node kind. pub fn new<T: Node>() -> Self { + Self::new_of(T::id()) + } + + /// Create a content of the given node kind. + pub fn new_of(id: NodeId) -> Self { Self { - id: T::id(), + id, span: Span::detached(), fields: EcoVec::new(), modifiers: EcoVec::new(), @@ -133,11 +143,10 @@ impl Content { .map(|(_, value)| value) } - /// Access a field on the content as a specified type. - #[track_caller] + /// Try to access a field on the content as a specified type. pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> { match self.field(name) { - Some(value) => Some(value.clone().cast().unwrap()), + Some(value) => value.clone().cast().ok(), None => None, } } @@ -145,7 +154,7 @@ impl Content { /// Expect a field on the content to exist as a specified type. #[track_caller] pub fn expect_field<T: Cast>(&self, name: &str) -> T { - self.cast_field(name).unwrap() + self.field(name).unwrap().clone().cast().unwrap() } /// List all fields on the content. @@ -500,6 +509,33 @@ cast_from_value! { StyleMap: "style map", } +/// Host for metadata. +/// +/// Display: Meta +/// Category: special +#[node(Behave)] +pub struct MetaNode { + /// Metadata that should be attached to all elements affected by this style + /// property. + #[fold] + pub data: Vec<Meta>, +} + +impl Behave for MetaNode { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} + +impl Fold for Vec<Meta> { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.extend(outer); + self + } +} + /// The missing key access error message. #[cold] #[track_caller] diff --git a/src/model/realize.rs b/src/model/realize.rs index 7a171cfc..70c75644 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -1,6 +1,7 @@ -use super::{Content, NodeId, Recipe, Selector, StyleChain, Vt}; +use super::{Content, MetaNode, Node, NodeId, Recipe, Selector, StyleChain, Vt}; use crate::diag::SourceResult; -use crate::doc::{Meta, MetaNode}; +use crate::doc::Meta; +use crate::util::hash128; /// Whether the target is affected by show rules in the given style chain. pub fn applicable(target: &Content, styles: StyleChain) -> bool { @@ -36,7 +37,7 @@ pub fn realize( if target.needs_preparation() { let mut node = target.clone(); if target.can::<dyn Locatable>() || target.label().is_some() { - let id = vt.identify(target); + let id = vt.provider.identify(hash128(target)); node.set_stable_id(id); } @@ -47,8 +48,12 @@ pub fn realize( node.mark_prepared(); if node.stable_id().is_some() { + let span = node.span(); let meta = Meta::Node(node.clone()); - return Ok(Some(node.styled(MetaNode::set_data(vec![meta])))); + return Ok(Some( + (node + MetaNode::new().pack().spanned(span)) + .styled(MetaNode::set_data(vec![meta])), + )); } return Ok(Some(node)); @@ -103,7 +108,7 @@ fn try_apply( return Ok(None); } - recipe.apply(vt.world(), target.clone().guarded(guard)).map(Some) + recipe.apply(vt.world, target.clone().guarded(guard)).map(Some) } Some(Selector::Label(label)) => { @@ -111,7 +116,7 @@ fn try_apply( return Ok(None); } - recipe.apply(vt.world(), target.clone().guarded(guard)).map(Some) + recipe.apply(vt.world, target.clone().guarded(guard)).map(Some) } Some(Selector::Regex(regex)) => { @@ -135,7 +140,7 @@ fn try_apply( } let piece = make(m.as_str().into()).guarded(guard); - let transformed = recipe.apply(vt.world(), piece)?; + let transformed = recipe.apply(vt.world, piece)?; result.push(transformed); cursor = m.end(); } @@ -151,6 +156,9 @@ fn try_apply( Ok(Some(Content::sequence(result))) } + // Not supported here. + Some(Selector::Any(_)) => Ok(None), + None => Ok(None), } } @@ -178,6 +186,36 @@ pub trait Finalize { fn finalize(&self, realized: Content, styles: StyleChain) -> Content; } +/// How a node interacts with other nodes. +pub trait Behave { + /// The node's interaction behaviour. + fn behaviour(&self) -> Behaviour; + + /// Whether this weak node is larger than a previous one and thus picked as + /// the maximum when the levels are the same. + #[allow(unused_variables)] + fn larger(&self, prev: &Content) -> bool { + false + } +} + +/// How a node interacts with other nodes in a stream. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Behaviour { + /// A weak node which only survives when a supportive node is before and + /// after it. Furthermore, per consecutive run of weak nodes, only one + /// survives: The one with the lowest weakness level (or the larger one if + /// there is a tie). + Weak(usize), + /// A node that enables adjacent weak nodes to exist. The default. + Supportive, + /// A node that destroys adjacent weak nodes. + Destructive, + /// A node that does not interact at all with other nodes, having the + /// same effect as if it didn't exist. + Ignorant, +} + /// Guards content against being affected by the same show rule multiple times. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Guard { diff --git a/src/model/styles.rs b/src/model/styles.rs index 9a562c75..359f1461 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use comemo::Tracked; -use ecow::{eco_format, EcoString}; +use ecow::{eco_format, EcoString, EcoVec}; use super::{Content, Label, Node, NodeId}; use crate::diag::{SourceResult, Trace, Tracepoint}; @@ -31,8 +31,8 @@ impl StyleMap { /// If the property needs folding and the value is already contained in the /// style map, `self` contributes the outer values and `value` is the inner /// one. - pub fn set(&mut self, property: Property) { - self.0.push(Style::Property(property)); + pub fn set(&mut self, style: impl Into<Style>) { + self.0.push(style.into()); } /// Remove the style that was last set. @@ -243,6 +243,8 @@ pub enum Selector { Label(Label), /// Matches text nodes through a regular expression. Regex(Regex), + /// Matches if any of the subselectors match. + Any(EcoVec<Self>), } impl Selector { @@ -271,6 +273,7 @@ impl Selector { target.id() == item!(text_id) && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) } + Self::Any(selectors) => selectors.iter().any(|sel| sel.matches(target)), } } } @@ -288,6 +291,12 @@ impl Debug for Selector { } Self::Label(label) => label.fmt(f), Self::Regex(regex) => regex.fmt(f), + Self::Any(selectors) => { + f.write_str("any")?; + let pieces: Vec<_> = + selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, false)) + } } } } @@ -659,6 +668,7 @@ impl<T: Debug> Debug for StyleVec<T> { } /// Assists in the construction of a [`StyleVec`]. +#[derive(Debug)] pub struct StyleVecBuilder<'a, T> { items: Vec<T>, chains: Vec<(StyleChain<'a>, usize)>, diff --git a/src/model/typeset.rs b/src/model/typeset.rs index f68d337d..4c8be135 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -1,15 +1,13 @@ -use std::cell::RefCell; -use std::collections::HashMap; use std::hash::Hash; use std::num::NonZeroUsize; -use comemo::{Track, Tracked, TrackedMut}; +use comemo::{Constraint, Track, Tracked, TrackedMut}; -use super::{Content, Node, Selector, StyleChain}; +use super::{Content, Selector, StyleChain}; use crate::diag::SourceResult; use crate::doc::{Document, Element, Frame, Location, Meta}; -use crate::geom::Transform; -use crate::util::hash128; +use crate::geom::{Point, Transform}; +use crate::util::NonZeroExt; use crate::World; /// Typeset content into a fully layouted document. @@ -25,17 +23,21 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc // Relayout until all introspections stabilize. // If that doesn't happen within five attempts, we give up. loop { + let constraint = Constraint::new(); let mut provider = StabilityProvider::new(); let mut vt = Vt { world, provider: provider.track_mut(), - introspector: introspector.track(), + introspector: introspector.track_with(&constraint), }; document = (library.items.layout)(&mut vt, content, styles)?; iter += 1; - if iter >= 5 || introspector.update(&document.pages) { + introspector = Introspector::new(&document.pages); + introspector.init = true; + + if iter >= 5 || introspector.valid(&constraint) { break; } } @@ -56,137 +58,86 @@ pub struct Vt<'a> { pub introspector: Tracked<'a, Introspector>, } -impl<'a> Vt<'a> { - /// Access the underlying world. - pub fn world(&self) -> Tracked<'a, dyn World> { - self.world +/// Provides stable identities to nodes. +#[derive(Clone)] +pub struct StabilityProvider { + hashes: Vec<u128>, + checkpoints: Vec<usize>, +} + +impl StabilityProvider { + /// Create a new stability provider. + pub fn new() -> Self { + Self { hashes: vec![], checkpoints: vec![] } } +} +#[comemo::track] +impl StabilityProvider { /// Produce a stable identifier for this call site. - /// - /// The key should be something that identifies the call site, but is not - /// necessarily unique. The stable marker incorporates the key's hash plus - /// additional disambiguation from other call sites with the same key. - /// - /// The returned id can be attached to content as metadata is the then - /// locatable through [`locate`](Self::locate). - pub fn identify<T: Hash>(&mut self, key: &T) -> StableId { - self.provider.identify(hash128(key)) + pub fn identify(&mut self, hash: u128) -> StableId { + let count = self.hashes.iter().filter(|&&prev| prev == hash).count(); + self.hashes.push(hash); + StableId(hash, count, 0) } - /// Whether things are locatable already. - pub fn locatable(&self) -> bool { - self.introspector.init() + /// Create a checkpoint of the state that can be restored. + pub fn save(&mut self) { + self.checkpoints.push(self.hashes.len()); } - /// Locate all metadata matches for the given node. - pub fn query_node<T: Node>(&self) -> impl Iterator<Item = &T> { - self.introspector - .query(Selector::node::<T>()) - .into_iter() - .map(|content| content.to::<T>().unwrap()) + /// Restore the last checkpoint. + pub fn restore(&mut self) { + if let Some(checkpoint) = self.checkpoints.pop() { + self.hashes.truncate(checkpoint); + } } } /// Stably identifies a call site across multiple layout passes. /// -/// This struct is created by [`Vt::identify`]. +/// This struct is created by [`StabilityProvider::identify`]. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StableId(u128, u64, u64); +pub struct StableId(u128, usize, usize); impl StableId { /// Produce a variant of this id. - pub fn variant(self, n: u64) -> Self { + pub fn variant(self, n: usize) -> Self { Self(self.0, self.1, n) } } -/// Provides stable identities to nodes. -#[derive(Clone)] -pub struct StabilityProvider(HashMap<u128, u64>); - -impl StabilityProvider { - /// Create a new stability provider. - fn new() -> Self { - Self(HashMap::new()) - } -} - -#[comemo::track] -impl StabilityProvider { - /// Produce a stable identifier for this call site. - fn identify(&mut self, hash: u128) -> StableId { - let slot = self.0.entry(hash).or_default(); - let id = StableId(hash, *slot, 0); - *slot += 1; - id - } -} - /// Provides access to information about the document. pub struct Introspector { init: bool, nodes: Vec<(Content, Location)>, - queries: RefCell<Vec<(Selector, u128)>>, } impl Introspector { /// Create a new introspector. pub fn new(frames: &[Frame]) -> Self { - let mut introspector = Self { - init: false, - nodes: vec![], - queries: RefCell::new(vec![]), - }; - introspector.extract_from_frames(frames); - introspector - } - - /// Update the information given new frames and return whether we can stop - /// layouting. - pub fn update(&mut self, frames: &[Frame]) -> bool { - self.nodes.clear(); - self.extract_from_frames(frames); - - let was_init = std::mem::replace(&mut self.init, true); - let queries = std::mem::take(&mut self.queries).into_inner(); - - for (selector, hash) in &queries { - let nodes = self.query_impl(selector); - if hash128(&nodes) != *hash { - return false; - } - } - - if !was_init && !queries.is_empty() { - return false; + let mut introspector = Self { init: false, nodes: vec![] }; + for (i, frame) in frames.iter().enumerate() { + let page = NonZeroUsize::new(1 + i).unwrap(); + introspector.extract(frame, page, Transform::identity()); } - - true + introspector } /// Iterate over all nodes. - pub fn nodes(&self) -> impl Iterator<Item = &Content> { + pub fn all(&self) -> impl Iterator<Item = &Content> { self.nodes.iter().map(|(node, _)| node) } - /// Extract metadata from frames. - fn extract_from_frames(&mut self, frames: &[Frame]) { - for (i, frame) in frames.iter().enumerate() { - let page = NonZeroUsize::new(1 + i).unwrap(); - self.extract_from_frame(frame, page, Transform::identity()); - } - } - /// Extract metadata from a frame. - fn extract_from_frame(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { + fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { for (pos, element) in frame.elements() { match element { Element::Group(group) => { let ts = ts .pre_concat(Transform::translate(pos.x, pos.y)) .pre_concat(group.transform); - self.extract_from_frame(&group.frame, page, ts); + self.extract(&group.frame, page, ts); } Element::Meta(Meta::Node(content), _) if !self @@ -212,27 +163,20 @@ impl Introspector { /// Query for all metadata matches for the given selector. pub fn query(&self, selector: Selector) -> Vec<&Content> { - let nodes = self.query_impl(&selector); - let mut queries = self.queries.borrow_mut(); - if !queries.iter().any(|(prev, _)| prev == &selector) { - queries.push((selector, hash128(&nodes))); - } - nodes + self.all().filter(|node| selector.matches(node)).collect() } /// Find the page number for the given stable id. - pub fn page(&self, id: StableId) -> Option<NonZeroUsize> { - Some(self.location(id)?.page) + pub fn page(&self, id: StableId) -> NonZeroUsize { + self.location(id).page } /// Find the location for the given stable id. - pub fn location(&self, id: StableId) -> Option<Location> { - Some(self.nodes.iter().find(|(node, _)| node.stable_id() == Some(id))?.1) - } -} - -impl Introspector { - fn query_impl(&self, selector: &Selector) -> Vec<&Content> { - self.nodes().filter(|node| selector.matches(node)).collect() + pub fn location(&self, id: StableId) -> Location { + self.nodes + .iter() + .find(|(node, _)| node.stable_id() == Some(id)) + .map(|(_, loc)| *loc) + .unwrap_or(Location { page: NonZeroUsize::ONE, pos: Point::zero() }) } } diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 8e48358d..760a6499 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -12,6 +12,7 @@ use super::{ is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode, }; use crate::geom::{AbsUnit, AngleUnit}; +use crate::util::NonZeroExt; /// A typed AST node. pub trait AstNode: Sized { @@ -641,7 +642,7 @@ impl Heading { .children() .find(|node| node.kind() == SyntaxKind::HeadingMarker) .and_then(|node| node.len().try_into().ok()) - .unwrap_or(NonZeroUsize::new(1).unwrap()) + .unwrap_or(NonZeroUsize::ONE) } } diff --git a/src/util/mod.rs b/src/util/mod.rs index 1eb19113..596282de 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,6 +8,7 @@ pub use buffer::Buffer; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; +use std::num::NonZeroUsize; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; @@ -39,6 +40,19 @@ pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 { state.finish128().as_u128() } +/// Extra methods for [`NonZeroUsize`]. +pub trait NonZeroExt { + /// The number `1`. + const ONE: Self; +} + +impl NonZeroExt for NonZeroUsize { + const ONE: Self = match Self::new(1) { + Some(v) => v, + None => unreachable!(), + }; +} + /// Extra methods for [`str`]. pub trait StrExt { /// The number of code units this string would use if it was encoded in diff --git a/tests/ref/layout/page-marginals.png b/tests/ref/layout/page-marginals.png Binary files differindex 5ad67ab4..bbe6358e 100644 --- a/tests/ref/layout/page-marginals.png +++ b/tests/ref/layout/page-marginals.png diff --git a/tests/ref/meta/counter-page.png b/tests/ref/meta/counter-page.png Binary files differnew file mode 100644 index 00000000..62f663cc --- /dev/null +++ b/tests/ref/meta/counter-page.png diff --git a/tests/ref/meta/counter.png b/tests/ref/meta/counter.png Binary files differnew file mode 100644 index 00000000..679e748f --- /dev/null +++ b/tests/ref/meta/counter.png diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png Binary files differindex b39f462f..c1b518d3 100644 --- a/tests/ref/meta/figure.png +++ b/tests/ref/meta/figure.png diff --git a/tests/ref/meta/outline.png b/tests/ref/meta/outline.png Binary files differindex 7bcb971e..5d05b015 100644 --- a/tests/ref/meta/outline.png +++ b/tests/ref/meta/outline.png diff --git a/tests/typ/layout/page-marginals.typ b/tests/typ/layout/page-marginals.typ index 30bd39c1..d8ce9b79 100644 --- a/tests/typ/layout/page-marginals.typ +++ b/tests/typ/layout/page-marginals.typ @@ -1,15 +1,15 @@ #set page( paper: "a8", margin: (x: 15pt, y: 30pt), - header: align(horizon, { + header: { text(eastern)[*Typst*] h(1fr) text(0.8em)[_Chapter 1_] - }), - footer: page => v(5pt) + align(center)[\~ #page \~], - background: n => if n <= 2 { + }, + footer: align(center)[\~ #counter(page).get() \~], + background: counter(page).get(n => if n <= 2 { place(center + horizon, circle(radius: 1cm, fill: luma(90%))) - } + }) ) But, soft! what light through yonder window breaks? It is the east, and Juliet diff --git a/tests/typ/meta/counter-page.typ b/tests/typ/meta/counter-page.typ new file mode 100644 index 00000000..b45a26f3 --- /dev/null +++ b/tests/typ/meta/counter-page.typ @@ -0,0 +1,9 @@ +// Test the page counter. + +#set page(height: 50pt, margin: (bottom: 20pt, rest: 10pt)) +#set page(numbering: "(i)") +#lorem(6) +#pagebreak() +#set page(numbering: "1 / 1") +#counter(page).update(1) +#lorem(20) diff --git a/tests/typ/meta/counter.typ b/tests/typ/meta/counter.typ new file mode 100644 index 00000000..9f6f4c8a --- /dev/null +++ b/tests/typ/meta/counter.typ @@ -0,0 +1,48 @@ +// Test counters. + +--- +// Count with string key. +#let mine = counter("mine!") + +Final: #mine.final() \ +#mine.step() +#mine.step() +First: #mine.get() \ +#mine.update(7) +#mine.both("1 of 1") \ +#mine.step() +#mine.step() +Second: #mine.get("I") +#mine.update(n => n * 2) +#mine.step() + +--- +// Count labels. +#let label = <heya> +#let count = counter(label).get() +#let elem(it) = [#box(it) #label] + +#elem[hey, there!] #count \ +#elem[more here!] #count + +--- +// Count headings. +#set heading(numbering: "1.a.") +#show heading: set text(10pt) +#counter(heading).step() + += Alpha +== Beta +In #counter(heading).get(). + +#set heading(numbering: none) += Gamma +#heading(numbering: "I.")[Delta] + +--- +// Count figures. +#figure(numbering: "A", caption: [Four 'A's])[_AAAA!_] +#figure(numbering: none, caption: [Four 'B's])[_BBBB!_] +#figure(caption: [Four 'C's])[_CCCC!_] +#counter(figure).update(n => n + 3) +#figure(caption: [Four 'D's])[_DDDD!_] diff --git a/tests/typ/meta/outline.typ b/tests/typ/meta/outline.typ index 1f882cec..83b2e906 100644 --- a/tests/typ/meta/outline.typ +++ b/tests/typ/meta/outline.typ @@ -1,4 +1,4 @@ -#set page("a7", margin: 20pt, footer: n => align(center, [#n])) +#set page("a7", margin: 20pt, numbering: "1") #set heading(numbering: "(1/a)") #show heading.where(level: 1): set text(12pt) #show heading.where(level: 2): set text(10pt) |
