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