diff --git a/Cargo.lock b/Cargo.lock index b0371e780..bedcea3ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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 43585b136..2a50019ef 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 3784bc8e0..7d6bccf6e 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. +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. -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. - -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 47f9dfc60..d49ee6a19 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 3c1f79d45..98da25bd6 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 a48933a70..0b7f7eb1f 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 33b297e79..1c08cd5f7 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 2671b9831..096c575e6 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -47,7 +47,16 @@ impl Layout for FlowNode { || child.is::() || child.is::() { - layouter.layout_single(vt, &child, styles)?; + let layoutable = child.with::().unwrap(); + layouter.layout_single(vt, layoutable, styles)?; + } else if child.is::() { + 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::() { layouter.layout_multiple(vt, &child, styles)?; } else if child.is::() { @@ -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::().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 b6e86afda..47d3ab86f 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 6d6058684..c954ab674 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 eb440b7f0..fc0279eb9 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; + + /// 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 { + 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::() || content.is::() { + if content.is::() + || content.is::() + || content.is::() + { 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::() + if content.is::() { + if !self.0.is_basically_empty() { + self.0.push(content.clone(), styles); + return true; + } + } else if content.is::() || content.is::() || content.is::() || content.is::() diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index e469bf10c..8ad76387a 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, - /// The page's header. + /// How to [number]($func/numbering) the pages. /// - /// The header is placed in the top margin of each page. - /// - /// - 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. + /// If an explicit `footer` is given, the numbering is ignored. /// /// ```example - /// #set par(justify: true) /// #set page( - /// margin: (x: 24pt, y: 32pt), - /// header: align(horizon + right, text(8pt)[_Exercise Sheet 3_]), + /// height: 100pt, + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1 / 1", /// ) /// - /// #lorem(18) + /// #lorem(48) /// ``` - pub header: Option, + pub numbering: Option, - /// The page's footer. + /// The alignment of the page numbering. /// - /// The footer is placed in the bottom margin of each page. + /// ```example + /// #set page( + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1", + /// number-align: right, + /// ) /// - /// - 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(30) + /// ``` + #[default(Align::Center.into())] + pub number_align: Axes>, + + /// The page's header. Fills the top margin of each page. /// /// ```example /// #set par(justify: true) /// #set page( - /// margin: (x: 24pt, y: 32pt), - /// footer: i => align(horizon + right, - /// text(8pt, numbering("I", i)) - /// ) + /// margin: (top: 32pt, bottom: 20pt), + /// header: [ + /// #set text(8pt) + /// #smallcaps[Typst Academcy] + /// #h(1fr) _Exercise Sheet 3_ + /// ], /// ) /// - /// #lorem(18) + /// #lorem(19) /// ``` - pub footer: Option, + pub header: Option, + + /// The amount the header is raised into the top margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + pub header_ascent: Rel, + + /// 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( + /// height: 100pt, + /// margin: 20pt, + /// footer: [ + /// #set align(right) + /// #set text(8pt) + /// #counter(page).get("1") of + /// #counter(page).final("I") + /// ] + /// ) + /// + /// #lorem(48) + /// ``` + pub footer: Option, + + /// The amount the footer is lowered into the bottom margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + pub footer_descent: Rel, /// 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, + pub background: Option, /// 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, + pub foreground: Option, /// 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 { + pub fn layout(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { // 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 244a61a95..1906dd7c7 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::() { + 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 6daafdbbf..024bb0d66 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -233,7 +233,7 @@ impl Celled { 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 c4a421d21..b397bfb45 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 0cc7f43ca..6df1d87af 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 e99905245..64ac1f8e5 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::()) .into_iter() - .flat_map(|node| load(vt.world(), &node.to::().unwrap().path())) + .flat_map(|node| load(vt.world, &node.to::().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 { + if !vt.introspector.init() { + return Ok(Content::empty()); + } + + let works = Works::new(vt).at(self.span())?; 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]")); - }; - - citation + 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>, references: Vec<(Option, Content)>, } impl Works { /// Prepare all things need to cite a work or format a bibliography. - pub fn new(vt: &Vt) -> StrResult> { + fn new(vt: &Vt) -> StrResult> { let bibliography = BibliographyNode::find(vt.introspector)?; - let citations = vt.query_node::().collect(); - Ok(create(vt.world(), &bibliography, citations)) + let citations = vt + .introspector + .query(Selector::Any(eco_vec![ + Selector::node::(), + Selector::node::(), + ])) + .into_iter() + .map(|node| match node.to::() { + Some(reference) => reference.to_citation(StyleChain::default()), + _ => node.to::().unwrap().clone(), + }) + .collect(); + Ok(create(vt.world, bibliography, citations)) } } @@ -282,8 +292,8 @@ impl Works { #[comemo::memoize] fn create( world: Tracked, - bibliography: &BibliographyNode, - citations: Vec<&CiteNode>, + bibliography: BibliographyNode, + citations: Vec, ) -> Arc { 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 000000000..ab089d5e4 --- /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 { + let counter = dynamic.downcast::().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 { + 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; +} + +/// 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, + numbering: &Numbering, + ) -> SourceResult { + 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, + introspector: Tracked, + ) -> SourceResult { + let mut search = Selector::Node( + NodeId::of::(), + 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::() { + Some(counter) => match counter.action() { + CounterAction::Update(update) => Some(update), + _ => None, + }, + None => match node.with::() { + 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::() { + return Ok(Self::Page); + } + + if !Content::new_of(id).can::() { + 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) -> 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) -> 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, + 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::>()?), +} diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index ae29e7a8b..75e78184c 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::() { - 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 a7668ffb5..3c3f6361b 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, /// 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, -} - -impl FigureNode { - fn element(&self) -> NodeId { - let mut id = self.body().id(); - if id != NodeId::of::() { - id = NodeId::of::(); - } - 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::() - .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 { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { 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::()), + 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 { + 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::() { - return body.with::().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 527a93a35..614200b83 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>, } 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::() - .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 { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { 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::()), + 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); - -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 { - self.0 +impl Count for HeadingNode { + fn update(&self) -> Option { + 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 ba74dac05..a7de2dad6 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 c59766c80..6febc408b 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 { 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 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 { - 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 1a3e8606c..933119ece 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, - - /// All outlined headings in the document. - #[synthesized] - pub headings: Vec, -} - -impl Synthesize for OutlineNode { - fn synthesize(&mut self, vt: &Vt, _: StyleChain) { - let headings = vt - .introspector - .query(Selector::Node( - NodeId::of::(), - Some(dict! { "outlined" => true }), - )) - .into_iter() - .map(|node| node.to::().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::(), + Some(dict! { "outlined" => true }), + )) { + let heading = node.to::().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::()) + .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::()) + .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 1616adb3c..095a846c2 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 { - 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::() { + 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::() .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::() { - 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::() { - 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") 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 { - 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 36f7cc89b..55e5f17b4 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 74c4d151f..eff41c0bd 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 53e90bc56..244e7afe5 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 20cfaa499..74bb70c75 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 { /// 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 129e07c5b..c5016436c 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 { - 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 de4599494..8aef3629f 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 dfbd9078b..1d05b9a6d 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::(), #name.into(), #ident.into() - ) + )) } } } diff --git a/src/doc.rs b/src/doc.rs index 03885b03f..f575ff1f9 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -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 { 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, -} - -impl Fold for Vec { - 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 1240d9bb2..45c23d170 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, } impl Debug for LangItems { diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 197a2f653..a449ac166 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 df7b517f3..5347d831a 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 b14e6775b..239a6e709 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -143,6 +143,26 @@ cast_to_value! { } } +impl From> for Axes> { + fn from(axes: Axes) -> Self { + axes.map(Some) + } +} + +impl From> for Axes> { + fn from(axes: Axes) -> Self { + axes.map(GenAlign::Specific).into() + } +} + +impl From for Axes> { + 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 ed868e53d..ccb89a9ca 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 0aa97b56a..17e318a77 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 58b804877..11ad635fc 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() -> 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(&self, name: &str) -> Option { 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(&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, +} + +impl Behave for MetaNode { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} + +impl Fold for Vec { + 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 7a171cfc0..70c756448 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::() || 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 9a562c758..359f1461e 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