summaryrefslogtreecommitdiff
path: root/library/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-17 11:32:15 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-17 11:45:57 +0100
commit312197b276748e1a17258ad21837850f582a467c (patch)
tree3fd0c078a2673a98b74bc12b4d654a4c143b4e1f /library/src
parente8435df5ec718e8ecc8a2ad48e4eb3ddd1f92a72 (diff)
Counters
Diffstat (limited to 'library/src')
-rw-r--r--library/src/layout/container.rs6
-rw-r--r--library/src/layout/enum.rs6
-rw-r--r--library/src/layout/flow.rs16
-rw-r--r--library/src/layout/grid.rs4
-rw-r--r--library/src/layout/list.rs2
-rw-r--r--library/src/layout/mod.rs33
-rw-r--r--library/src/layout/page.rs175
-rw-r--r--library/src/layout/par.rs12
-rw-r--r--library/src/layout/table.rs2
-rw-r--r--library/src/lib.rs2
-rw-r--r--library/src/math/mod.rs2
-rw-r--r--library/src/meta/bibliography.rs66
-rw-r--r--library/src/meta/counter.rs337
-rw-r--r--library/src/meta/document.rs3
-rw-r--r--library/src/meta/figure.rs68
-rw-r--r--library/src/meta/heading.rs78
-rw-r--r--library/src/meta/mod.rs2
-rw-r--r--library/src/meta/numbering.rs38
-rw-r--r--library/src/meta/outline.rs49
-rw-r--r--library/src/meta/reference.rs95
-rw-r--r--library/src/prelude.rs10
-rw-r--r--library/src/shared/behave.rs45
-rw-r--r--library/src/text/shaping.rs8
-rw-r--r--library/src/text/shift.rs2
-rw-r--r--library/src/visualize/image.rs4
-rw-r--r--library/src/visualize/shape.rs2
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))
}