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 /library/src | |
| parent | e8435df5ec718e8ecc8a2ad48e4eb3ddd1f92a72 (diff) | |
Counters
Diffstat (limited to 'library/src')
26 files changed, 707 insertions, 360 deletions
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)) } |
