This commit is contained in:
Laurenz 2023-03-17 11:32:15 +01:00
parent e8435df5ec
commit 312197b276
54 changed files with 1050 additions and 612 deletions

5
Cargo.lock generated
View File

@ -172,7 +172,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "comemo" name = "comemo"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/typst/comemo#36fb31c76eb42d67244bd9c7a2630c29767912f2" source = "git+https://github.com/typst/comemo#9b520e8f5284d1c39d0bb13eb426f923972775f8"
dependencies = [ dependencies = [
"comemo-macros", "comemo-macros",
"siphasher", "siphasher",
@ -181,7 +181,7 @@ dependencies = [
[[package]] [[package]]
name = "comemo-macros" name = "comemo-macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/typst/comemo#36fb31c76eb42d67244bd9c7a2630c29767912f2" source = "git+https://github.com/typst/comemo#9b520e8f5284d1c39d0bb13eb426f923972775f8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1415,6 +1415,7 @@ dependencies = [
"roxmltree", "roxmltree",
"rustybuzz", "rustybuzz",
"serde_json", "serde_json",
"smallvec",
"syntect", "syntect",
"ttf-parser 0.18.1", "ttf-parser 0.18.1",
"typed-arena", "typed-arena",

View File

@ -86,8 +86,9 @@ fantasy encyclopedia.
#show heading: it => block[ #show heading: it => block[
#set align(center) #set align(center)
#set text(font: "Inria Serif") #set text(font: "Inria Serif")
\~ _#it.body;_ \~ #emph(it.body)
#it.numbers \~ #(counter(heading)
.get(it.numbering)) \~
] ]
= Dragon = Dragon

View File

@ -56,14 +56,11 @@ Let's start by writing some set rules for the document.
#set page( #set page(
>>> margin: auto, >>> margin: auto,
paper: "us-letter", paper: "us-letter",
header: align(right + horizon)[ header: align(right)[
A fluid dynamic model for A fluid dynamic model for
glacier flow glacier flow
], ],
footer: nr => align( numbering: "1",
center + horizon,
[#nr],
),
) )
#lorem(600) #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 size to `{11pt}` and the font to Linux Libertine. We also enable paragraph
justification and set the page size to US letter. justification and set the page size to US letter.
The `header` and `footer` arguments are new: With these, we can provide content The `header` argument is new: With it, we can provide content to fill the top
to fill the top and bottom margins of every page. In the header, we specify our margin of every page. In the header, we specify our paper's title as requested
paper's title as requested by the conference style guide. We use the `align` by the conference style guide. We use the `align` function to align the text to
function to align the text to the right and the `horizon` keyword to make sure the right.
that it is vertically centered in the margin.
Because we need a page number in the footer, we have to put different content Last but not least is the `numbering` argument. Here, we can provide a
onto each page. To do that, we can pass a [numbering pattern]($func/numbering) that defines how to number the pages. By
[custom function]($type/function) to the footer argument that defines setting into to `{"1"}`, Typst only displays the bare page number. Setting it to
how the footer should look for a given page number. Typst provides the page `{"(1/1)"}` would have displayed the current page and total number of pages
number to this function. Once more, we use the `align` function to center the surrounded by parentheses. And we can even have provided a completely custom
page number horizontally and vertically. function here to format things to our liking.
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).
## Creating a title and abstract ## Creating a title and abstract
Now, let's add a title and an abstract. We'll start with the title. We center 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 >>> A fluid dynamic model for
>>> glacier flow >>> glacier flow
>>> ], >>> ],
>>> footer: page => align( >>> numbering: "1",
>>> center+horizon,
>>> [#page]
>>> ),
>>> ) >>> )
>>> >>>
>>> #align(center, text(17pt)[ >>> #align(center, text(17pt)[
@ -220,10 +208,7 @@ keyword:
title title
), ),
<<< ... <<< ...
>>> footer: page => align( >>> numbering: "1",
>>> center+horizon,
>>> [#page]
>>> ),
) )
#align(center, text(17pt)[ #align(center, text(17pt)[
@ -289,10 +274,7 @@ content. In our case, it passes it on to the `columns` function.
>>> right + horizon, >>> right + horizon,
>>> title >>> title
>>> ), >>> ),
>>> footer: page => align( >>> numbering: "1",
>>> center+horizon,
>>> [#page]
>>> ),
>>> ) >>> )
>>> >>>
>>> #align(center, text( >>> #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, >>> right + horizon,
>>> title >>> title
>>> ), >>> ),
>>> footer: page => align( >>> numbering: "1",
>>> center + horizon,
>>> [#page]
>>> ),
>>> ) >>> )
#show heading: it => block[ #show heading: it => block[
#set align(center) #set align(center)
@ -430,10 +409,7 @@ differentiate between section and subsection headings:
>>> right + horizon, >>> right + horizon,
>>> title >>> title
>>> ), >>> ),
>>> footer: page => align( >>> numbering: "1",
>>> center + horizon,
>>> [#page]
>>> ),
>>> ) >>> )
>>> >>>
#show heading.where( #show heading.where(

View File

@ -275,10 +275,7 @@ path of the file after the `{from}` keyword.
>>> right + horizon, >>> right + horizon,
>>> title >>> title
>>> ), >>> ),
>>> footer: page => align( >>> numbering: "1",
>>> center + horizon,
>>> [#page]
>>> ),
>>> ) >>> )
>>> >>>
>>> show heading.where( >>> show heading.where(

View File

@ -23,6 +23,7 @@ once_cell = "1"
roxmltree = "0.14" roxmltree = "0.14"
rustybuzz = "0.5" rustybuzz = "0.5"
serde_json = "1" serde_json = "1"
smallvec = "1.10"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
ttf-parser = "0.18.1" ttf-parser = "0.18.1"
typed-arena = "2" typed-arena = "2"

View File

@ -145,7 +145,7 @@ impl Layout for BoxNode {
} }
// Apply metadata. // Apply metadata.
frame.meta(styles); frame.meta(styles, false);
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }
@ -336,7 +336,7 @@ impl Layout for BlockNode {
// Measure to ensure frames for all regions have the same width. // Measure to ensure frames for all regions have the same width.
if sizing.x == Smart::Auto { if sizing.x == Smart::Auto {
let pod = Regions::one(size, Axes::splat(false)); 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(); size.x = frame.width();
expand.x = true; expand.x = true;
} }
@ -389,7 +389,7 @@ impl Layout for BlockNode {
// Apply metadata. // Apply metadata.
for frame in &mut frames { for frame in &mut frames {
frame.meta(styles); frame.meta(styles, false);
} }
Ok(Fragment::frames(frames)) Ok(Fragment::frames(frames))

View File

@ -100,7 +100,7 @@ pub struct EnumNode {
/// [Ahead], /// [Ahead],
/// ) /// )
/// ``` /// ```
#[default(NonZeroUsize::new(1).unwrap())] #[default(NonZeroUsize::ONE)]
pub start: NonZeroUsize, pub start: NonZeroUsize,
/// Whether to display the full numbering, including the numbers of /// Whether to display the full numbering, including the numbers of
@ -180,7 +180,7 @@ impl Layout for EnumNode {
let resolved = if full { let resolved = if full {
parents.push(number); parents.push(number);
let content = numbering.apply(vt.world(), &parents)?.display(); let content = numbering.apply(vt.world, &parents)?.display();
parents.pop(); parents.pop();
content content
} else { } else {
@ -188,7 +188,7 @@ impl Layout for EnumNode {
Numbering::Pattern(pattern) => { Numbering::Pattern(pattern) => {
TextNode::packed(pattern.apply_kth(parents.len(), number)) TextNode::packed(pattern.apply_kth(parents.len(), number))
} }
other => other.apply(vt.world(), &[number])?.display(), other => other.apply(vt.world, &[number])?.display(),
} }
}; };

View File

@ -47,7 +47,16 @@ impl Layout for FlowNode {
|| child.is::<CircleNode>() || child.is::<CircleNode>()
|| child.is::<ImageNode>() || 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>() { } else if child.can::<dyn Layout>() {
layouter.layout_multiple(vt, &child, styles)?; layouter.layout_multiple(vt, &child, styles)?;
} else if child.is::<ColbreakNode>() { } else if child.is::<ColbreakNode>() {
@ -173,14 +182,13 @@ impl<'a> FlowLayouter<'a> {
fn layout_single( fn layout_single(
&mut self, &mut self,
vt: &mut Vt, vt: &mut Vt,
content: &Content, content: &dyn Layout,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let aligns = AlignNode::alignment_in(styles).resolve(styles); let aligns = AlignNode::alignment_in(styles).resolve(styles);
let sticky = BlockNode::sticky_in(styles); let sticky = BlockNode::sticky_in(styles);
let pod = Regions::one(self.regions.base(), Axes::splat(false)); let pod = Regions::one(self.regions.base(), Axes::splat(false));
let layoutable = content.with::<dyn Layout>().unwrap(); let frame = content.layout(vt, styles, pod)?.into_frame();
let frame = layoutable.layout(vt, styles, pod)?.into_frame();
self.layout_item(FlowItem::Frame(frame, aligns, sticky)); self.layout_item(FlowItem::Frame(frame, aligns, sticky));
self.last_was_par = false; self.last_was_par = false;
Ok(()) Ok(())

View File

@ -386,7 +386,7 @@ impl<'a, 'v> GridLayouter<'a, 'v> {
let size = Size::new(available, height); let size = Size::new(available, height);
let pod = Regions::one(size, Axes::splat(false)); 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()); resolved.set_max(frame.width());
} }
} }
@ -457,7 +457,7 @@ impl<'a, 'v> GridLayouter<'a, 'v> {
let mut pod = self.regions; let mut pod = self.regions;
pod.size.x = rcol; 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() { if let [first, rest @ ..] = frames.as_slice() {
skip |= skip |=
first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); first.is_empty() && rest.iter().any(|frame| !frame.is_empty());

View File

@ -128,7 +128,7 @@ impl Layout for ListNode {
}; };
let depth = self.depth(styles); 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![]; let mut cells = vec![];
for item in self.children() { for item in self.children() {

View File

@ -47,10 +47,7 @@ use std::mem;
use typed_arena::Arena; use typed_arena::Arena;
use typst::diag::SourceResult; use typst::diag::SourceResult;
use typst::model::{ use typst::model::{applicable, realize, SequenceNode, StyleVecBuilder, StyledNode};
applicable, realize, Content, Node, SequenceNode, StyleChain, StyleVecBuilder,
StyledNode,
};
use crate::math::{FormulaNode, LayoutMath}; use crate::math::{FormulaNode, LayoutMath};
use crate::meta::DocumentNode; use crate::meta::DocumentNode;
@ -103,6 +100,22 @@ pub trait Layout {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment>; ) -> 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 { impl Layout for Content {
@ -417,7 +430,10 @@ impl<'a> FlowBuilder<'a> {
let last_was_parbreak = self.1; let last_was_parbreak = self.1;
self.1 = false; 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); self.0.push(content.clone(), styles);
return true; return true;
} }
@ -457,7 +473,12 @@ struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> { impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { 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::<TextNode>()
|| content.is::<HNode>() || content.is::<HNode>()
|| content.is::<LinebreakNode>() || content.is::<LinebreakNode>()

View File

@ -1,6 +1,8 @@
use std::ptr;
use std::str::FromStr; use std::str::FromStr;
use super::ColumnsNode; use super::{AlignNode, ColumnsNode};
use crate::meta::{Counter, CounterAction, CounterNode, Numbering};
use crate::prelude::*; use crate::prelude::*;
/// Layouts its child onto one or multiple pages. /// Layouts its child onto one or multiple pages.
@ -130,7 +132,7 @@ pub struct PageNode {
/// emissions and mitigate the impacts /// emissions and mitigate the impacts
/// of a rapidly changing climate. /// of a rapidly changing climate.
/// ``` /// ```
#[default(NonZeroUsize::new(1).unwrap())] #[default(NonZeroUsize::ONE)]
pub columns: NonZeroUsize, pub columns: NonZeroUsize,
/// The page's background color. /// The page's background color.
@ -147,49 +149,84 @@ pub struct PageNode {
/// ``` /// ```
pub fill: Option<Paint>, 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 /// ```example
/// #set par(justify: true)
/// #set page( /// #set page(
/// margin: (x: 24pt, y: 32pt), /// height: 100pt,
/// header: align(horizon + right, text(8pt)[_Exercise Sheet 3_]), /// margin: (top: 16pt, bottom: 24pt),
/// numbering: "1 / 1",
/// ) /// )
/// ///
/// #lorem(18) /// #lorem(48)
/// ``` /// ```
pub header: Option<Marginal>, pub numbering: Option<Numbering>,
/// The page's footer. /// The alignment of the page numbering.
/// ///
/// The footer is placed in the bottom margin of each page. /// ```example
/// #set page(
/// margin: (top: 16pt, bottom: 24pt),
/// numbering: "1",
/// number-align: right,
/// )
/// ///
/// - Content: The content will be placed in the footer. /// #lorem(30)
/// - 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 #[default(Align::Center.into())]
/// the footer. pub number_align: Axes<Option<GenAlign>>,
/// - `{none}`: The footer will be empty.
/// The page's header. Fills the top margin of each page.
/// ///
/// ```example /// ```example
/// #set par(justify: true) /// #set par(justify: true)
/// #set page( /// #set page(
/// margin: (x: 24pt, y: 32pt), /// margin: (top: 32pt, bottom: 20pt),
/// footer: i => align(horizon + right, /// header: [
/// text(8pt, numbering("I", i)) /// #set text(8pt)
/// ) /// #smallcaps[Typst Academcy]
/// #h(1fr) _Exercise Sheet 3_
/// ],
/// ) /// )
/// ///
/// #lorem(18) /// #lorem(19)
/// ``` /// ```
pub footer: Option<Marginal>, 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(
/// height: 100pt,
/// margin: 20pt,
/// footer: [
/// #set align(right)
/// #set text(8pt)
/// #counter(page).get("1") of
/// #counter(page).final("I")
/// ]
/// )
///
/// #lorem(48)
/// ```
pub footer: Option<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. /// Content in the page's background.
/// ///
@ -197,35 +234,30 @@ pub struct PageNode {
/// used to place a background image or a watermark. /// used to place a background image or a watermark.
/// ///
/// ```example /// ```example
/// #set page(background: align( /// #set page(background: rotate(24deg,
/// center + horizon, /// text(18pt, fill: rgb("FFCBC4"))[
/// rotate(24deg, /// *CONFIDENTIAL*
/// text(18pt, fill: rgb("FFCBC4"))[*CONFIDENTIAL*] /// ]
/// ),
/// )) /// ))
/// ///
/// = Typst's secret plans /// = Typst's secret plans
/// /// In the year 2023, we plan to take
/// In the year 2023, we plan to take over the world /// over the world (of typesetting).
/// (of typesetting).
/// ``` /// ```
pub background: Option<Marginal>, pub background: Option<Content>,
/// Content in the page's foreground. /// Content in the page's foreground.
/// ///
/// This content will overlay the page's body. /// This content will overlay the page's body.
/// ///
/// ```example /// ```example
/// #set page(foreground: align( /// #set page(foreground: text(24pt)[🥸])
/// center + horizon,
/// text(24pt)[🥸],
/// ))
/// ///
/// Reviewer 2 has marked our paper /// Reviewer 2 has marked our paper
/// "Weak Reject" because they did /// "Weak Reject" because they did
/// not understand our approach... /// not understand our approach...
/// ``` /// ```
pub foreground: Option<Marginal>, pub foreground: Option<Content>,
/// The contents of the page(s). /// The contents of the page(s).
/// ///
@ -238,12 +270,7 @@ pub struct PageNode {
impl PageNode { impl PageNode {
/// Layout the page run into a sequence of frames, one per page. /// Layout the page run into a sequence of frames, one per page.
pub fn layout( pub fn layout(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Fragment> {
&self,
vt: &mut Vt,
mut page: usize,
styles: StyleChain,
) -> SourceResult<Fragment> {
// When one of the lengths is infinite the page fits its content along // When one of the lengths is infinite the page fits its content along
// that axis. // that axis.
let width = self.width(styles).unwrap_or(Abs::inf()); let width = self.width(styles).unwrap_or(Abs::inf());
@ -278,10 +305,18 @@ impl PageNode {
let mut fragment = child.layout(vt, styles, regions)?; let mut fragment = child.layout(vt, styles, regions)?;
let fill = self.fill(styles); let fill = self.fill(styles);
let header = self.header(styles);
let footer = self.footer(styles);
let foreground = self.foreground(styles); let foreground = self.foreground(styles);
let background = self.background(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. // Realize overlays.
for frame in &mut fragment { for frame in &mut fragment {
@ -292,26 +327,38 @@ impl PageNode {
let size = frame.size(); let size = frame.size();
let pad = padding.resolve(styles).relative_to(size); let pad = padding.resolve(styles).relative_to(size);
let pw = size.x - pad.left - pad.right; let pw = size.x - pad.left - pad.right;
let py = size.y - pad.bottom; for marginal in [&header, &footer, &background, &foreground] {
for (marginal, pos, area) in [ let Some(content) = marginal else { continue };
(&header, Point::with_x(pad.left), Size::new(pw, pad.top)),
(&footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), let (pos, area, align);
(&foreground, Point::zero(), size), if ptr::eq(marginal, &header) {
(&background, Point::zero(), size), let ascent = header_ascent.relative_to(pad.top);
] { pos = Point::with_x(pad.left);
let in_background = std::ptr::eq(marginal, &background); area = Size::new(pw, pad.top - ascent);
let Some(marginal) = marginal else { continue }; align = Align::Bottom.into();
let content = marginal.resolve(vt, page)?; } 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 pod = Regions::one(area, Axes::splat(true));
let sub = content.layout(vt, styles, pod)?.into_frame(); let sub = content
if in_background { .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); frame.prepend_frame(pos, sub);
} else { } else {
frame.push_frame(pos, sub); frame.push_frame(pos, sub);
} }
} }
page += 1;
} }
Ok(fragment) Ok(fragment)
@ -358,7 +405,7 @@ impl Marginal {
Self::Content(content) => content.clone(), Self::Content(content) => content.clone(),
Self::Func(func) => { Self::Func(func) => {
let args = Args::new(func.span(), [Value::Int(page as i64)]); 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()
} }
}) })
} }

View File

@ -325,6 +325,8 @@ enum Segment<'a> {
Formula(&'a FormulaNode), Formula(&'a FormulaNode),
/// A box with arbitrary content. /// A box with arbitrary content.
Box(&'a BoxNode, bool), Box(&'a BoxNode, bool),
/// Metadata.
Meta,
} }
impl Segment<'_> { impl Segment<'_> {
@ -334,7 +336,7 @@ impl Segment<'_> {
Self::Text(len) => len, Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Box(_, true) => 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(); let frac = node.width(styles).is_fractional();
full.push(if frac { SPACING_REPLACE } else { NODE_REPLACE }); full.push(if frac { SPACING_REPLACE } else { NODE_REPLACE });
Segment::Box(node, frac) Segment::Box(node, frac)
} else if child.is::<MetaNode>() {
full.push(NODE_REPLACE);
Segment::Meta
} else { } else {
bail!(child.span(), "unexpected paragraph child"); bail!(child.span(), "unexpected paragraph child");
}; };
@ -679,6 +684,11 @@ fn prepare<'a>(
items.push(Item::Frame(frame)); 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; cursor = end;

View File

@ -233,7 +233,7 @@ impl<T: Cast + Clone> Celled<T> {
Self::Func(func) => { Self::Func(func) => {
let args = let args =
Args::new(func.span(), [Value::Int(x as i64), Value::Int(y as i64)]); 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())?
} }
}) })
} }

View File

@ -91,6 +91,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("figure", meta::FigureNode::id()); global.define("figure", meta::FigureNode::id());
global.define("cite", meta::CiteNode::id()); global.define("cite", meta::CiteNode::id());
global.define("bibliography", meta::BibliographyNode::id()); global.define("bibliography", meta::BibliographyNode::id());
global.define("counter", meta::counter);
global.define("numbering", meta::numbering); global.define("numbering", meta::numbering);
// Symbols. // Symbols.
@ -224,5 +225,6 @@ fn items() -> LangItems {
math::AccentNode::new(base, math::Accent::new(accent)).pack() math::AccentNode::new(base, math::Accent::new(accent)).pack()
}, },
math_frac: |num, denom| math::FracNode::new(num, denom).pack(), math_frac: |num, denom| math::FracNode::new(num, denom).pack(),
counter_method: meta::counter_method,
} }
} }

View File

@ -174,7 +174,7 @@ impl Layout for FormulaNode {
// Find a math font. // Find a math font.
let variant = variant(styles); let variant = variant(styles);
let world = vt.world(); let world = vt.world;
let Some(font) = families(styles) let Some(font) = families(styles)
.find_map(|family| { .find_map(|family| {
let id = world.book().select(family.as_str(), variant)?; let id = world.book().select(family.as_str(), variant)?;

View File

@ -3,12 +3,12 @@ use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use ecow::EcoVec; use ecow::{eco_vec, EcoVec};
use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting}; use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting};
use hayagriva::Entry; use hayagriva::Entry;
use super::LocalName; use super::{LocalName, RefNode};
use crate::layout::{BlockNode, GridNode, ParNode, Sizing, TrackSizings, VNode}; use crate::layout::{BlockNode, GridNode, ParNode, Sizing, TrackSizings, VNode};
use crate::meta::HeadingNode; use crate::meta::HeadingNode;
use crate::prelude::*; use crate::prelude::*;
@ -65,7 +65,7 @@ impl BibliographyNode {
vt.introspector vt.introspector
.query(Selector::node::<Self>()) .query(Selector::node::<Self>())
.into_iter() .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() .flatten()
.any(|entry| entry.key() == key) .any(|entry| entry.key() == key)
} }
@ -100,12 +100,6 @@ impl Show for BibliographyNode {
const COLUMN_GUTTER: Em = Em::new(0.65); const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5); 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![]; let mut seq = vec![];
if let Some(title) = self.title(styles) { if let Some(title) = self.title(styles) {
let title = title.clone().unwrap_or_else(|| { let title = title.clone().unwrap_or_else(|| {
@ -115,12 +109,18 @@ impl Show for BibliographyNode {
seq.push( seq.push(
HeadingNode::new(title) HeadingNode::new(title)
.with_level(NonZeroUsize::new(1).unwrap()) .with_level(NonZeroUsize::ONE)
.with_numbering(None) .with_numbering(None)
.pack(), .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(); let row_gutter = BlockNode::below_in(styles).amount();
if works.references.iter().any(|(prefix, _)| prefix.is_some()) { if works.references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![]; let mut cells = vec![];
@ -227,18 +227,17 @@ impl Synthesize for CiteNode {
impl Show for CiteNode { impl Show for CiteNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let works = Works::new(vt).at(self.span())?;
let id = self.0.stable_id().unwrap(); let id = self.0.stable_id().unwrap();
let works = match Works::new(vt) { works
Ok(works) => works, .citations
Err(error) if vt.locatable() => bail!(self.span(), error), .get(&id)
Err(_) => Arc::new(Works::default()), .cloned()
}; .flatten()
let Some(citation) = works.citations.get(&id).cloned() else {
return Ok(TextNode::packed("[1]"));
};
citation
.ok_or("bibliography does not contain this key") .ok_or("bibliography does not contain this key")
.at(self.span()) .at(self.span())
} }
@ -264,17 +263,28 @@ pub enum CitationStyle {
/// Fully formatted citations and references. /// Fully formatted citations and references.
#[derive(Default)] #[derive(Default)]
pub struct Works { struct Works {
citations: HashMap<StableId, Option<Content>>, citations: HashMap<StableId, Option<Content>>,
references: Vec<(Option<Content>, Content)>, references: Vec<(Option<Content>, Content)>,
} }
impl Works { impl Works {
/// Prepare all things need to cite a work or format a bibliography. /// 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 bibliography = BibliographyNode::find(vt.introspector)?;
let citations = vt.query_node::<CiteNode>().collect(); let citations = vt
Ok(create(vt.world(), &bibliography, citations)) .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] #[comemo::memoize]
fn create( fn create(
world: Tracked<dyn World>, world: Tracked<dyn World>,
bibliography: &BibliographyNode, bibliography: BibliographyNode,
citations: Vec<&CiteNode>, citations: Vec<CiteNode>,
) -> Arc<Works> { ) -> Arc<Works> {
let span = bibliography.span(); let span = bibliography.span();
let entries = load(world, &bibliography.path()).unwrap(); let entries = load(world, &bibliography.path()).unwrap();
@ -294,7 +304,7 @@ fn create(
.iter() .iter()
.position(|entry| entry.key() == target.key()) .position(|entry| entry.key() == target.key())
.unwrap_or_default(); .unwrap_or_default();
bib_id.variant(i as u64) bib_id.variant(i)
}; };
let mut db = Database::new(); let mut db = Database::new();

337
library/src/meta/counter.rs Normal file
View File

@ -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<_>>()?),
}

View File

@ -45,8 +45,7 @@ impl LayoutRoot for DocumentNode {
} }
if let Some(page) = child.to::<PageNode>() { if let Some(page) = child.to::<PageNode>() {
let number = 1 + pages.len(); let fragment = page.layout(vt, styles)?;
let fragment = page.layout(vt, number, styles)?;
pages.extend(fragment); pages.extend(fragment);
} else { } else {
bail!(child.span(), "unexpected document child"); bail!(child.span(), "unexpected document child");

View File

@ -1,7 +1,10 @@
use std::str::FromStr; use std::str::FromStr;
use super::{LocalName, Numbering, NumberingPattern}; use super::{
use crate::layout::{BlockNode, TableNode, VNode}; Count, Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering,
NumberingPattern,
};
use crate::layout::{BlockNode, VNode};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
@ -23,7 +26,7 @@ use crate::text::TextNode;
/// ///
/// Display: Figure /// Display: Figure
/// Category: meta /// Category: meta
#[node(Locatable, Synthesize, Show, LocalName)] #[node(Locatable, Synthesize, Count, Show, LocalName)]
pub struct FigureNode { pub struct FigureNode {
/// The content of the figure. Often, an [image]($func/image). /// The content of the figure. Often, an [image]($func/image).
#[required] #[required]
@ -34,60 +37,34 @@ pub struct FigureNode {
/// How to number the figure. Accepts a /// How to number the figure. Accepts a
/// [numbering pattern or function]($func/numbering). /// [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>, pub numbering: Option<Numbering>,
/// The vertical gap between the body and caption. /// The vertical gap between the body and caption.
#[default(Em::new(0.65).into())] #[default(Em::new(0.65).into())]
pub gap: Length, 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 { impl Synthesize for FigureNode {
fn synthesize(&mut self, vt: &Vt, styles: StyleChain) { fn synthesize(&mut self, _: &Vt, styles: StyleChain) {
let my_id = self.0.stable_id(); self.push_numbering(self.numbering(styles));
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);
} }
} }
impl Show for FigureNode { 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(); let mut realized = self.body();
if let Some(mut caption) = self.caption(styles) { if let Some(mut caption) = self.caption(styles) {
if let Some(numbering) = self.numbering(styles) { if let Some(numbering) = self.numbering(styles) {
let number = self.number().unwrap();
let name = self.local_name(TextNode::lang_in(styles)); let name = self.local_name(TextNode::lang_in(styles));
caption = TextNode::packed(eco_format!("{name}\u{a0}")) 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(": ") + TextNode::packed(": ")
+ caption; + 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 { impl LocalName for FigureNode {
fn local_name(&self, lang: Lang) -> &'static str { 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 { match lang {
Lang::GERMAN => "Abbildung", Lang::GERMAN => "Abbildung",
Lang::ENGLISH | _ => "Figure", Lang::ENGLISH | _ => "Figure",

View File

@ -1,7 +1,8 @@
use typst::font::FontWeight; use typst::font::FontWeight;
use super::{LocalName, Numbering}; use super::{Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering};
use crate::layout::{BlockNode, HNode, VNode}; use crate::layout::{BlockNode, HNode, VNode};
use crate::meta::Count;
use crate::prelude::*; use crate::prelude::*;
use crate::text::{TextNode, TextSize}; use crate::text::{TextNode, TextSize};
@ -40,10 +41,10 @@ use crate::text::{TextNode, TextSize};
/// ///
/// Display: Heading /// Display: Heading
/// Category: meta /// Category: meta
#[node(Locatable, Synthesize, Show, Finalize, LocalName)] #[node(Locatable, Synthesize, Count, Show, Finalize, LocalName)]
pub struct HeadingNode { pub struct HeadingNode {
/// The logical nesting depth of the heading, starting from one. /// The logical nesting depth of the heading, starting from one.
#[default(NonZeroUsize::new(1).unwrap())] #[default(NonZeroUsize::ONE)]
pub level: NonZeroUsize, pub level: NonZeroUsize,
/// How to number the heading. Accepts a /// How to number the heading. Accepts a
@ -76,46 +77,26 @@ pub struct HeadingNode {
/// The heading's title. /// The heading's title.
#[required] #[required]
pub body: Content, pub body: Content,
/// The heading's numbering numbers.
#[synthesized]
pub numbers: Option<Vec<NonZeroUsize>>,
} }
impl Synthesize for HeadingNode { impl Synthesize for HeadingNode {
fn synthesize(&mut self, vt: &Vt, styles: StyleChain) { fn synthesize(&mut self, _: &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);
}
self.push_level(self.level(styles)); self.push_level(self.level(styles));
self.push_numbering(self.numbering(styles));
self.push_outlined(self.outlined(styles)); self.push_outlined(self.outlined(styles));
self.push_numbers(numbering.is_some().then(|| counter.take()));
self.push_numbering(numbering);
} }
} }
impl Show for HeadingNode { 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(); let mut realized = self.body();
if let Some(numbering) = self.numbering(styles) { if let Some(numbering) = self.numbering(styles) {
let numbers = self.numbers().unwrap(); realized = CounterNode::new(
realized = numbering.apply(vt.world(), &numbers)?.display() Counter::Selector(Selector::node::<Self>()),
CounterAction::Get(numbering),
)
.pack()
.spanned(self.span())
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack() + HNode::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized; + realized;
} }
@ -146,34 +127,11 @@ impl Finalize for HeadingNode {
} }
} }
/// Counts through headings with different levels. impl Count for HeadingNode {
pub struct HeadingCounter(Vec<NonZeroUsize>); fn update(&self) -> Option<CounterUpdate> {
self.numbering(StyleChain::default())
impl HeadingCounter { .is_some()
/// Create a new heading counter. .then(|| CounterUpdate::Step(self.level(StyleChain::default())))
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
} }
} }

View File

@ -1,6 +1,7 @@
//! Interaction between document parts. //! Interaction between document parts.
mod bibliography; mod bibliography;
mod counter;
mod document; mod document;
mod figure; mod figure;
mod heading; mod heading;
@ -10,6 +11,7 @@ mod outline;
mod reference; mod reference;
pub use self::bibliography::*; pub use self::bibliography::*;
pub use self::counter::*;
pub use self::document::*; pub use self::document::*;
pub use self::figure::*; pub use self::figure::*;
pub use self::heading::*; pub use self::heading::*;

View File

@ -1,5 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use ecow::EcoVec;
use crate::prelude::*; use crate::prelude::*;
use crate::text::Case; use crate::text::Case;
@ -66,7 +68,7 @@ pub fn numbering(
} }
/// How to number a sequence of things. /// How to number a sequence of things.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum Numbering { pub enum Numbering {
/// A pattern with prefix, numbering, lower / upper case and suffix. /// A pattern with prefix, numbering, lower / upper case and suffix.
Pattern(NumberingPattern), Pattern(NumberingPattern),
@ -82,7 +84,7 @@ impl Numbering {
numbers: &[NonZeroUsize], numbers: &[NonZeroUsize],
) -> SourceResult<Value> { ) -> SourceResult<Value> {
Ok(match self { 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) => { Self::Func(func) => {
let args = Args::new( let args = Args::new(
func.span(), 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! { cast_from_value! {
@ -118,20 +134,21 @@ cast_to_value! {
/// - `(I)` /// - `(I)`
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct NumberingPattern { pub struct NumberingPattern {
pieces: Vec<(EcoString, NumberingKind, Case)>, pieces: EcoVec<(EcoString, NumberingKind, Case)>,
suffix: EcoString, suffix: EcoString,
trimmed: bool,
} }
impl NumberingPattern { impl NumberingPattern {
/// Apply the pattern to the given number. /// 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 fmt = EcoString::new();
let mut numbers = numbers.into_iter(); let mut numbers = numbers.into_iter();
for (i, ((prefix, kind, case), &n)) in for (i, ((prefix, kind, case), &n)) in
self.pieces.iter().zip(&mut numbers).enumerate() self.pieces.iter().zip(&mut numbers).enumerate()
{ {
if i > 0 || !trimmed { if i > 0 || !self.trimmed {
fmt.push_str(prefix); fmt.push_str(prefix);
} }
fmt.push_str(&kind.apply(n, *case)); fmt.push_str(&kind.apply(n, *case));
@ -148,7 +165,7 @@ impl NumberingPattern {
fmt.push_str(&kind.apply(n, *case)); fmt.push_str(&kind.apply(n, *case));
} }
if !trimmed { if !self.trimmed {
fmt.push_str(&self.suffix); fmt.push_str(&self.suffix);
} }
@ -172,13 +189,18 @@ impl NumberingPattern {
fmt.push_str(&self.suffix); fmt.push_str(&self.suffix);
fmt fmt
} }
/// How many counting symbols this pattern has.
pub fn pieces(&self) -> usize {
self.pieces.len()
}
} }
impl FromStr for NumberingPattern { impl FromStr for NumberingPattern {
type Err = &'static str; type Err = &'static str;
fn from_str(pattern: &str) -> Result<Self, Self::Err> { fn from_str(pattern: &str) -> Result<Self, Self::Err> {
let mut pieces = vec![]; let mut pieces = EcoVec::new();
let mut handled = 0; let mut handled = 0;
for (i, c) in pattern.char_indices() { for (i, c) in pattern.char_indices() {
@ -197,7 +219,7 @@ impl FromStr for NumberingPattern {
Err("invalid numbering pattern")?; Err("invalid numbering pattern")?;
} }
Ok(Self { pieces, suffix }) Ok(Self { pieces, suffix, trimmed: false })
} }
} }

View File

@ -1,4 +1,4 @@
use super::{HeadingNode, LocalName}; use super::{Counter, HeadingNode, LocalName};
use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode}; use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode};
use crate::prelude::*; use crate::prelude::*;
use crate::text::{LinebreakNode, SpaceNode, TextNode}; use crate::text::{LinebreakNode, SpaceNode, TextNode};
@ -22,7 +22,7 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode};
/// ///
/// Display: Outline /// Display: Outline
/// Category: meta /// Category: meta
#[node(Synthesize, Show, LocalName)] #[node(Show, LocalName)]
pub struct OutlineNode { pub struct OutlineNode {
/// The title of the outline. /// The title of the outline.
/// ///
@ -67,26 +67,6 @@ pub struct OutlineNode {
/// ``` /// ```
#[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))] #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))]
pub fill: Option<Content>, 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 { impl Show for OutlineNode {
@ -100,7 +80,7 @@ impl Show for OutlineNode {
seq.push( seq.push(
HeadingNode::new(title) HeadingNode::new(title)
.with_level(NonZeroUsize::new(1).unwrap()) .with_level(NonZeroUsize::ONE)
.with_numbering(None) .with_numbering(None)
.with_outlined(false) .with_outlined(false)
.pack(), .pack(),
@ -111,7 +91,11 @@ impl Show for OutlineNode {
let depth = self.depth(styles); let depth = self.depth(styles);
let mut ancestors: Vec<&HeadingNode> = vec![]; 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(); let stable_id = heading.0.stable_id().unwrap();
if !heading.outlined(StyleChain::default()) { if !heading.outlined(StyleChain::default()) {
continue; continue;
@ -134,9 +118,9 @@ impl Show for OutlineNode {
let mut hidden = Content::empty(); let mut hidden = Content::empty();
for ancestor in &ancestors { for ancestor in &ancestors {
if let Some(numbering) = ancestor.numbering(StyleChain::default()) { if let Some(numbering) = ancestor.numbering(StyleChain::default()) {
let numbers = ancestor.numbers().unwrap(); let numbers = Counter::Selector(Selector::node::<HeadingNode>())
hidden += numbering.apply(vt.world(), &numbers)?.display() .resolve(vt, ancestor.0.stable_id(), &numbering)?;
+ SpaceNode::new().pack(); hidden += numbers + SpaceNode::new().pack();
}; };
} }
@ -149,10 +133,9 @@ impl Show for OutlineNode {
// Format the numbering. // Format the numbering.
let mut start = heading.body(); let mut start = heading.body();
if let Some(numbering) = heading.numbering(StyleChain::default()) { if let Some(numbering) = heading.numbering(StyleChain::default()) {
let numbers = heading.numbers().unwrap(); let numbers = Counter::Selector(Selector::node::<HeadingNode>())
start = numbering.apply(vt.world(), &numbers)?.display() .resolve(vt, Some(stable_id), &numbering)?;
+ SpaceNode::new().pack() start = numbers + SpaceNode::new().pack() + start;
+ start;
}; };
// Add the numbering and section name. // Add the numbering and section name.
@ -173,8 +156,8 @@ impl Show for OutlineNode {
} }
// Add the page number and linebreak. // Add the page number and linebreak.
let page = vt.introspector.page(stable_id).unwrap(); let page = vt.introspector.page(stable_id);
let end = TextNode::packed(eco_format!("{}", page)); let end = TextNode::packed(eco_format!("{page}"));
seq.push(end.linked(Link::Node(stable_id))); seq.push(end.linked(Link::Node(stable_id)));
seq.push(LinebreakNode::new().pack()); seq.push(LinebreakNode::new().pack());
ancestors.push(heading); ancestors.push(heading);

View File

@ -1,4 +1,4 @@
use super::{BibliographyNode, CiteNode, FigureNode, HeadingNode, LocalName, Numbering}; use super::{BibliographyNode, CiteNode, Counter, LocalName, Numbering};
use crate::prelude::*; use crate::prelude::*;
use crate::text::TextNode; use crate::text::TextNode;
@ -35,7 +35,7 @@ use crate::text::TextNode;
/// ///
/// Display: Reference /// Display: Reference
/// Category: meta /// Category: meta
#[node(Show)] #[node(Locatable, Show)]
pub struct RefNode { pub struct RefNode {
/// The target label that should be referenced. /// The target label that should be referenced.
#[required] #[required]
@ -65,40 +65,36 @@ pub struct RefNode {
impl Show for RefNode { impl Show for RefNode {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let target = self.target(); if !vt.introspector.init() {
let supplement = self.supplement(styles); return Ok(Content::empty());
}
let target = self.target();
let matches = vt.introspector.query(Selector::Label(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() { if !matches.is_empty() {
bail!(self.span(), "label occurs in the document and its bibliography"); bail!(self.span(), "label occurs in the document and its bibliography");
} }
return Ok(CiteNode::new(vec![target.0]) return self.to_citation(styles).show(vt, styles);
.with_supplement(match supplement {
Smart::Custom(Some(Supplement::Content(content))) => Some(content),
_ => None,
})
.pack()
.spanned(self.span()));
} }
let &[target] = matches.as_slice() else { let &[node] = matches.as_slice() else {
if vt.locatable() {
bail!(self.span(), if matches.is_empty() { bail!(self.span(), if matches.is_empty() {
"label does not exist in the document" "label does not exist in the document"
} else { } else {
"label occurs multiple times in the document" "label occurs multiple times in the document"
}); });
} else {
return Ok(Content::empty());
}
}; };
if !node.can::<dyn Locatable>() {
bail!(self.span(), "cannot reference {}", node.id().name);
}
let supplement = self.supplement(styles); let supplement = self.supplement(styles);
let mut supplement = match supplement { let mut supplement = match supplement {
Smart::Auto => target Smart::Auto => node
.with::<dyn LocalName>() .with::<dyn LocalName>()
.map(|node| node.local_name(TextNode::lang_in(styles))) .map(|node| node.local_name(TextNode::lang_in(styles)))
.map(TextNode::packed) .map(TextNode::packed)
@ -106,8 +102,8 @@ impl Show for RefNode {
Smart::Custom(None) => Content::empty(), Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(Supplement::Content(content))) => content.clone(), Smart::Custom(Some(Supplement::Content(content))) => content.clone(),
Smart::Custom(Some(Supplement::Func(func))) => { Smart::Custom(Some(Supplement::Func(func))) => {
let args = Args::new(func.span(), [target.clone().into()]); let args = Args::new(func.span(), [node.clone().into()]);
func.call_detached(vt.world(), args)?.display() func.call_detached(vt.world, args)?.display()
} }
}; };
@ -115,42 +111,31 @@ impl Show for RefNode {
supplement += TextNode::packed('\u{a0}'); supplement += TextNode::packed('\u{a0}');
} }
let formatted = if let Some(heading) = target.to::<HeadingNode>() { let Some(numbering) = node.cast_field::<Numbering>("numbering") else {
if let Some(numbering) = heading.numbering(StyleChain::default()) { bail!(self.span(), "only numbered elements can be referenced");
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);
}; };
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". impl RefNode {
fn numbered( /// Turn the rference into a citation.
vt: &Vt, pub fn to_citation(&self, styles: StyleChain) -> CiteNode {
prefix: Content, let mut node = CiteNode::new(vec![self.target().0]);
numbering: &Numbering, node.push_supplement(match self.supplement(styles) {
numbers: &[NonZeroUsize], Smart::Custom(Some(Supplement::Content(content))) => Some(content),
) -> SourceResult<Content> { _ => None,
Ok(prefix });
+ match numbering { node.0.set_stable_id(self.0.stable_id().unwrap());
Numbering::Pattern(pattern) => { node
TextNode::packed(pattern.apply(&numbers, true))
} }
Numbering::Func(_) => numbering.apply(vt.world(), &numbers)?.display(),
})
} }
/// Additional content for a reference. /// Additional content for a reference.

View File

@ -22,16 +22,18 @@ pub use typst::eval::{
pub use typst::geom::*; pub use typst::geom::*;
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::model::{ pub use typst::model::{
node, Construct, Content, Finalize, Fold, Introspector, Label, Locatable, Node, node, Behave, Behaviour, Construct, Content, Finalize, Fold, Introspector, Label,
NodeId, Resolve, Selector, Set, Show, StabilityProvider, StableId, StyleChain, Locatable, MetaNode, Node, NodeId, Resolve, Selector, Set, Show, StabilityProvider,
StyleMap, StyleVec, Synthesize, Unlabellable, Vt, StableId, StyleChain, StyleMap, StyleVec, Synthesize, Unlabellable, Vt,
}; };
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::syntax::{Span, Spanned}; pub use typst::syntax::{Span, Spanned};
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::util::NonZeroExt;
#[doc(no_inline)]
pub use typst::World; pub use typst::World;
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::layout::{Fragment, Layout, Regions}; pub use crate::layout::{Fragment, Layout, Regions};
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::shared::{Behave, Behaviour, ContentExt, StyleMapExt}; pub use crate::shared::{ContentExt, StyleMapExt};

View File

@ -1,38 +1,9 @@
//! Node interaction. //! Node interaction.
use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder}; use typst::model::{Behave, Behaviour, 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,
}
/// A wrapper around a [`StyleVecBuilder`] that allows items to interact. /// A wrapper around a [`StyleVecBuilder`] that allows items to interact.
#[derive(Debug)]
pub struct BehavedBuilder<'a> { pub struct BehavedBuilder<'a> {
/// The internal builder. /// The internal builder.
builder: StyleVecBuilder<'a, Content>, 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 { pub fn is_empty(&self) -> bool {
self.builder.is_empty() && self.staged.is_empty() 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. /// Push an item into the sequence.
pub fn push(&mut self, item: Content, styles: StyleChain<'a>) { pub fn push(&mut self, item: Content, styles: StyleChain<'a>) {
let interaction = item let interaction = item

View File

@ -136,7 +136,7 @@ impl<'a> ShapedText<'a> {
} }
// Apply metadata. // Apply metadata.
frame.meta(self.styles); frame.meta(self.styles, false);
frame frame
} }
@ -159,7 +159,7 @@ impl<'a> ShapedText<'a> {
if self.glyphs.is_empty() { if self.glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the // When there are no glyphs, we just use the vertical metrics of the
// first available font. // first available font.
let world = vt.world(); let world = vt.world;
for family in families(self.styles) { for family in families(self.styles) {
if let Some(font) = world if let Some(font) = world
.book() .book()
@ -228,7 +228,7 @@ impl<'a> ShapedText<'a> {
/// Push a hyphen to end of the text. /// Push a hyphen to end of the text.
pub fn push_hyphen(&mut self, vt: &Vt) { pub fn push_hyphen(&mut self, vt: &Vt) {
families(self.styles).find_map(|family| { families(self.styles).find_map(|family| {
let world = vt.world(); let world = vt.world;
let font = world let font = world
.book() .book()
.select(family.as_str(), self.variant) .select(family.as_str(), self.variant)
@ -389,7 +389,7 @@ fn shape_segment<'a>(
} }
// Find the next available family. // Find the next available family.
let world = ctx.vt.world(); let world = ctx.vt.world;
let book = world.book(); let book = world.book();
let mut selection = families.find_map(|family| { let mut selection = families.find_map(|family| {
book.select(family.as_str(), ctx.variant) book.select(family.as_str(), ctx.variant)

View File

@ -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 /// Checks whether the first retrievable family contains all code points of the
/// given string. /// given string.
fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool { 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) { for family in TextNode::font_in(styles) {
if let Some(font) = world if let Some(font) = world
.book() .book()

View File

@ -53,7 +53,7 @@ impl Layout for ImageNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> 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 sizing = Axes::new(self.width(styles), self.height(styles));
let region = sizing let region = sizing
.zip(regions.base()) .zip(regions.base())
@ -106,7 +106,7 @@ impl Layout for ImageNode {
} }
// Apply metadata. // Apply metadata.
frame.meta(styles); frame.meta(styles, false);
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }

View File

@ -536,7 +536,7 @@ fn layout(
} }
// Apply metadata. // Apply metadata.
frame.meta(styles); frame.meta(styles, false);
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }

View File

@ -326,12 +326,12 @@ fn create_set_field_method(field: &Field) -> TokenStream {
let doc = format!("Create a style property for the `{}` field.", name); let doc = format!("Create a style property for the `{}` field.", name);
quote! { quote! {
#[doc = #doc] #[doc = #doc]
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Property { #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
::typst::model::Property::new( ::typst::model::Style::Property(::typst::model::Property::new(
::typst::model::NodeId::of::<Self>(), ::typst::model::NodeId::of::<Self>(),
#name.into(), #name.into(),
#ident.into() #ident.into()
) ))
} }
} }
} }

View File

@ -14,7 +14,7 @@ use crate::geom::{
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
}; };
use crate::image::Image; 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; use crate::syntax::Span;
/// A finished document with metadata and page frames. /// A finished document with metadata and page frames.
@ -271,10 +271,8 @@ impl Frame {
} }
/// Attach the metadata from this style chain to the frame. /// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) { pub fn meta(&mut self, styles: StyleChain, force: bool) {
if self.is_empty() { if force || !self.is_empty() {
return;
}
for meta in MetaNode::data_in(styles) { for meta in MetaNode::data_in(styles) {
if matches!(meta, Meta::Hide) { if matches!(meta, Meta::Hide) {
self.clear(); self.clear();
@ -283,6 +281,7 @@ impl Frame {
self.prepend(Point::zero(), Element::Meta(meta, self.size)); self.prepend(Point::zero(), Element::Meta(meta, self.size));
} }
} }
}
/// Add a background fill. /// Add a background fill.
pub fn fill(&mut self, fill: Paint) { pub fn fill(&mut self, fill: Paint) {
@ -607,6 +606,16 @@ pub enum Meta {
Node(Content), 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. /// A possibly unresolved link.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum Link { pub enum Link {
@ -623,45 +632,14 @@ impl Link {
pub fn resolve<'a>( pub fn resolve<'a>(
&self, &self,
introspector: impl FnOnce() -> &'a Introspector, introspector: impl FnOnce() -> &'a Introspector,
) -> Option<Destination> { ) -> Destination {
match self { match self {
Self::Dest(dest) => Some(dest.clone()), Self::Dest(dest) => dest.clone(),
Self::Node(id) => introspector().location(*id).map(Destination::Internal), 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. /// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination { pub enum Destination {

View File

@ -6,11 +6,12 @@ use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use super::Module; use super::{Args, Dynamic, Module, Value};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::doc::Document; use crate::doc::Document;
use crate::geom::{Abs, Dir}; use crate::geom::{Abs, Dir};
use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt}; use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt};
use crate::syntax::Span;
use crate::util::hash128; use crate::util::hash128;
use crate::World; use crate::World;
@ -89,6 +90,14 @@ pub struct LangItems {
pub math_accent: fn(base: Content, accent: char) -> Content, pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in a formula: `x/2`. /// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content, 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 { impl Debug for LangItems {

View File

@ -134,6 +134,14 @@ pub fn call(
_ => return missing(), _ => return missing(),
}, },
Value::Dyn(dynamic) => {
if dynamic.type_name() == "counter" {
return (vm.items.counter_method)(&dynamic, method, args, span);
}
return missing();
}
_ => return missing(), _ => return missing(),
}; };
@ -281,6 +289,13 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
], ],
"function" => &[("where", true), ("with", true)], "function" => &[("where", true), ("with", true)],
"arguments" => &[("named", false), ("pos", false)], "arguments" => &[("named", false), ("pos", false)],
"counter" => &[
("get", true),
("final", true),
("both", true),
("step", true),
("update", true),
],
_ => &[], _ => &[],
} }
} }

View File

@ -114,11 +114,7 @@ fn write_page(ctx: &mut PdfContext, page: Page) {
let mut annotation = annotations.push(); let mut annotation = annotations.push();
annotation.subtype(AnnotationType::Link).rect(rect); annotation.subtype(AnnotationType::Link).rect(rect);
annotation.border(0.0, 0.0, 0.0, None); annotation.border(0.0, 0.0, 0.0, None);
match link.resolve(|| &ctx.introspector) {
let dest = link.resolve(|| &ctx.introspector);
let Some(dest) = dest else { continue };
match dest {
Destination::Url(uri) => { Destination::Url(uri) => {
annotation annotation
.action() .action()

View File

@ -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 { impl Resolve for GenAlign {
type Output = Align; type Output = Align;

View File

@ -78,7 +78,7 @@ pub fn analyze_labels(
let items = &world.library().items; let items = &world.library().items;
// Labels in the document. // Labels in the document.
for node in introspector.nodes() { for node in introspector.all() {
let Some(label) = node.label() else { continue }; let Some(label) = node.label() else { continue };
let details = node let details = node
.field("caption") .field("caption")

View File

@ -36,12 +36,9 @@ pub fn jump_from_click(
for (pos, element) in frame.elements() { for (pos, element) in frame.elements() {
if let Element::Meta(Meta::Link(link), size) = element { if let Element::Meta(Meta::Link(link), size) = element {
if is_in_rect(*pos, *size, click) { 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)) introspector.get_or_insert_with(|| Introspector::new(frames))
}); })));
let Some(dest) = dest else { continue };
return Some(Jump::Dest(dest));
} }
} }
} }

View File

@ -8,8 +8,12 @@ use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use once_cell::sync::Lazy; 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::diag::{SourceResult, StrResult};
use crate::doc::Meta;
use crate::eval::{ use crate::eval::{
cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm, cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm,
}; };
@ -35,9 +39,15 @@ enum Modifier {
} }
impl Content { impl Content {
/// Create a content of the given node kind.
pub fn new<T: Node>() -> Self { 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 { Self {
id: T::id(), id,
span: Span::detached(), span: Span::detached(),
fields: EcoVec::new(), fields: EcoVec::new(),
modifiers: EcoVec::new(), modifiers: EcoVec::new(),
@ -133,11 +143,10 @@ impl Content {
.map(|(_, value)| value) .map(|(_, value)| value)
} }
/// Access a field on the content as a specified type. /// Try to access a field on the content as a specified type.
#[track_caller]
pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> { pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> {
match self.field(name) { match self.field(name) {
Some(value) => Some(value.clone().cast().unwrap()), Some(value) => value.clone().cast().ok(),
None => None, None => None,
} }
} }
@ -145,7 +154,7 @@ impl Content {
/// Expect a field on the content to exist as a specified type. /// Expect a field on the content to exist as a specified type.
#[track_caller] #[track_caller]
pub fn expect_field<T: Cast>(&self, name: &str) -> T { 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. /// List all fields on the content.
@ -500,6 +509,33 @@ cast_from_value! {
StyleMap: "style map", 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. /// The missing key access error message.
#[cold] #[cold]
#[track_caller] #[track_caller]

View File

@ -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::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. /// Whether the target is affected by show rules in the given style chain.
pub fn applicable(target: &Content, styles: StyleChain) -> bool { pub fn applicable(target: &Content, styles: StyleChain) -> bool {
@ -36,7 +37,7 @@ pub fn realize(
if target.needs_preparation() { if target.needs_preparation() {
let mut node = target.clone(); let mut node = target.clone();
if target.can::<dyn Locatable>() || target.label().is_some() { 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); node.set_stable_id(id);
} }
@ -47,8 +48,12 @@ pub fn realize(
node.mark_prepared(); node.mark_prepared();
if node.stable_id().is_some() { if node.stable_id().is_some() {
let span = node.span();
let meta = Meta::Node(node.clone()); 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)); return Ok(Some(node));
@ -103,7 +108,7 @@ fn try_apply(
return Ok(None); 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)) => { Some(Selector::Label(label)) => {
@ -111,7 +116,7 @@ fn try_apply(
return Ok(None); 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)) => { Some(Selector::Regex(regex)) => {
@ -135,7 +140,7 @@ fn try_apply(
} }
let piece = make(m.as_str().into()).guarded(guard); 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); result.push(transformed);
cursor = m.end(); cursor = m.end();
} }
@ -151,6 +156,9 @@ fn try_apply(
Ok(Some(Content::sequence(result))) Ok(Some(Content::sequence(result)))
} }
// Not supported here.
Some(Selector::Any(_)) => Ok(None),
None => Ok(None), None => Ok(None),
} }
} }
@ -178,6 +186,36 @@ pub trait Finalize {
fn finalize(&self, realized: Content, styles: StyleChain) -> Content; 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. /// Guards content against being affected by the same show rule multiple times.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Guard { pub enum Guard {

View File

@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter, Write};
use std::iter; use std::iter;
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString, EcoVec};
use super::{Content, Label, Node, NodeId}; use super::{Content, Label, Node, NodeId};
use crate::diag::{SourceResult, Trace, Tracepoint}; 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 /// 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 /// style map, `self` contributes the outer values and `value` is the inner
/// one. /// one.
pub fn set(&mut self, property: Property) { pub fn set(&mut self, style: impl Into<Style>) {
self.0.push(Style::Property(property)); self.0.push(style.into());
} }
/// Remove the style that was last set. /// Remove the style that was last set.
@ -243,6 +243,8 @@ pub enum Selector {
Label(Label), Label(Label),
/// Matches text nodes through a regular expression. /// Matches text nodes through a regular expression.
Regex(Regex), Regex(Regex),
/// Matches if any of the subselectors match.
Any(EcoVec<Self>),
} }
impl Selector { impl Selector {
@ -271,6 +273,7 @@ impl Selector {
target.id() == item!(text_id) target.id() == item!(text_id)
&& item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) && 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::Label(label) => label.fmt(f),
Self::Regex(regex) => regex.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`]. /// Assists in the construction of a [`StyleVec`].
#[derive(Debug)]
pub struct StyleVecBuilder<'a, T> { pub struct StyleVecBuilder<'a, T> {
items: Vec<T>, items: Vec<T>,
chains: Vec<(StyleChain<'a>, usize)>, chains: Vec<(StyleChain<'a>, usize)>,

View File

@ -1,15 +1,13 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
use std::num::NonZeroUsize; 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::diag::SourceResult;
use crate::doc::{Document, Element, Frame, Location, Meta}; use crate::doc::{Document, Element, Frame, Location, Meta};
use crate::geom::Transform; use crate::geom::{Point, Transform};
use crate::util::hash128; use crate::util::NonZeroExt;
use crate::World; use crate::World;
/// Typeset content into a fully layouted document. /// 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. // Relayout until all introspections stabilize.
// If that doesn't happen within five attempts, we give up. // If that doesn't happen within five attempts, we give up.
loop { loop {
let constraint = Constraint::new();
let mut provider = StabilityProvider::new(); let mut provider = StabilityProvider::new();
let mut vt = Vt { let mut vt = Vt {
world, world,
provider: provider.track_mut(), provider: provider.track_mut(),
introspector: introspector.track(), introspector: introspector.track_with(&constraint),
}; };
document = (library.items.layout)(&mut vt, content, styles)?; document = (library.items.layout)(&mut vt, content, styles)?;
iter += 1; iter += 1;
if iter >= 5 || introspector.update(&document.pages) { introspector = Introspector::new(&document.pages);
introspector.init = true;
if iter >= 5 || introspector.valid(&constraint) {
break; break;
} }
} }
@ -56,70 +58,52 @@ pub struct Vt<'a> {
pub introspector: Tracked<'a, Introspector>, pub introspector: Tracked<'a, Introspector>,
} }
impl<'a> Vt<'a> {
/// Access the underlying world.
pub fn world(&self) -> Tracked<'a, dyn World> {
self.world
}
/// 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))
}
/// Whether things are locatable already.
pub fn locatable(&self) -> bool {
self.introspector.init()
}
/// 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())
}
}
/// Stably identifies a call site across multiple layout passes.
///
/// This struct is created by [`Vt::identify`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StableId(u128, u64, u64);
impl StableId {
/// Produce a variant of this id.
pub fn variant(self, n: u64) -> Self {
Self(self.0, self.1, n)
}
}
/// Provides stable identities to nodes. /// Provides stable identities to nodes.
#[derive(Clone)] #[derive(Clone)]
pub struct StabilityProvider(HashMap<u128, u64>); pub struct StabilityProvider {
hashes: Vec<u128>,
checkpoints: Vec<usize>,
}
impl StabilityProvider { impl StabilityProvider {
/// Create a new stability provider. /// Create a new stability provider.
fn new() -> Self { pub fn new() -> Self {
Self(HashMap::new()) Self { hashes: vec![], checkpoints: vec![] }
} }
} }
#[comemo::track] #[comemo::track]
impl StabilityProvider { impl StabilityProvider {
/// Produce a stable identifier for this call site. /// Produce a stable identifier for this call site.
fn identify(&mut self, hash: u128) -> StableId { pub fn identify(&mut self, hash: u128) -> StableId {
let slot = self.0.entry(hash).or_default(); let count = self.hashes.iter().filter(|&&prev| prev == hash).count();
let id = StableId(hash, *slot, 0); self.hashes.push(hash);
*slot += 1; StableId(hash, count, 0)
id }
/// Create a checkpoint of the state that can be restored.
pub fn save(&mut self) {
self.checkpoints.push(self.hashes.len());
}
/// 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 [`StabilityProvider::identify`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StableId(u128, usize, usize);
impl StableId {
/// Produce a variant of this id.
pub fn variant(self, n: usize) -> Self {
Self(self.0, self.1, n)
} }
} }
@ -127,66 +111,33 @@ impl StabilityProvider {
pub struct Introspector { pub struct Introspector {
init: bool, init: bool,
nodes: Vec<(Content, Location)>, nodes: Vec<(Content, Location)>,
queries: RefCell<Vec<(Selector, u128)>>,
} }
impl Introspector { impl Introspector {
/// Create a new introspector. /// Create a new introspector.
pub fn new(frames: &[Frame]) -> Self { pub fn new(frames: &[Frame]) -> Self {
let mut introspector = Self { let mut introspector = Self { init: false, nodes: vec![] };
init: false, for (i, frame) in frames.iter().enumerate() {
nodes: vec![], let page = NonZeroUsize::new(1 + i).unwrap();
queries: RefCell::new(vec![]), introspector.extract(frame, page, Transform::identity());
}; }
introspector.extract_from_frames(frames);
introspector 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;
}
true
}
/// Iterate over all nodes. /// 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) 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. /// 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() { for (pos, element) in frame.elements() {
match element { match element {
Element::Group(group) => { Element::Group(group) => {
let ts = ts let ts = ts
.pre_concat(Transform::translate(pos.x, pos.y)) .pre_concat(Transform::translate(pos.x, pos.y))
.pre_concat(group.transform); .pre_concat(group.transform);
self.extract_from_frame(&group.frame, page, ts); self.extract(&group.frame, page, ts);
} }
Element::Meta(Meta::Node(content), _) Element::Meta(Meta::Node(content), _)
if !self if !self
@ -212,27 +163,20 @@ impl Introspector {
/// Query for all metadata matches for the given selector. /// Query for all metadata matches for the given selector.
pub fn query(&self, selector: Selector) -> Vec<&Content> { pub fn query(&self, selector: Selector) -> Vec<&Content> {
let nodes = self.query_impl(&selector); self.all().filter(|node| selector.matches(node)).collect()
let mut queries = self.queries.borrow_mut();
if !queries.iter().any(|(prev, _)| prev == &selector) {
queries.push((selector, hash128(&nodes)));
}
nodes
} }
/// Find the page number for the given stable id. /// Find the page number for the given stable id.
pub fn page(&self, id: StableId) -> Option<NonZeroUsize> { pub fn page(&self, id: StableId) -> NonZeroUsize {
Some(self.location(id)?.page) self.location(id).page
} }
/// Find the location for the given stable id. /// Find the location for the given stable id.
pub fn location(&self, id: StableId) -> Option<Location> { pub fn location(&self, id: StableId) -> Location {
Some(self.nodes.iter().find(|(node, _)| node.stable_id() == Some(id))?.1) self.nodes
} .iter()
} .find(|(node, _)| node.stable_id() == Some(id))
.map(|(_, loc)| *loc)
impl Introspector { .unwrap_or(Location { page: NonZeroUsize::ONE, pos: Point::zero() })
fn query_impl(&self, selector: &Selector) -> Vec<&Content> {
self.nodes().filter(|node| selector.matches(node)).collect()
} }
} }

View File

@ -12,6 +12,7 @@ use super::{
is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode, is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode,
}; };
use crate::geom::{AbsUnit, AngleUnit}; use crate::geom::{AbsUnit, AngleUnit};
use crate::util::NonZeroExt;
/// A typed AST node. /// A typed AST node.
pub trait AstNode: Sized { pub trait AstNode: Sized {
@ -641,7 +642,7 @@ impl Heading {
.children() .children()
.find(|node| node.kind() == SyntaxKind::HeadingMarker) .find(|node| node.kind() == SyntaxKind::HeadingMarker)
.and_then(|node| node.len().try_into().ok()) .and_then(|node| node.len().try_into().ok())
.unwrap_or(NonZeroUsize::new(1).unwrap()) .unwrap_or(NonZeroUsize::ONE)
} }
} }

View File

@ -8,6 +8,7 @@ pub use buffer::Buffer;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::num::NonZeroUsize;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@ -39,6 +40,19 @@ pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
state.finish128().as_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`]. /// Extra methods for [`str`].
pub trait StrExt { pub trait StrExt {
/// The number of code units this string would use if it was encoded in /// The number of code units this string would use if it was encoded in

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
tests/ref/meta/counter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,15 +1,15 @@
#set page( #set page(
paper: "a8", paper: "a8",
margin: (x: 15pt, y: 30pt), margin: (x: 15pt, y: 30pt),
header: align(horizon, { header: {
text(eastern)[*Typst*] text(eastern)[*Typst*]
h(1fr) h(1fr)
text(0.8em)[_Chapter 1_] text(0.8em)[_Chapter 1_]
}), },
footer: page => v(5pt) + align(center)[\~ #page \~], footer: align(center)[\~ #counter(page).get() \~],
background: n => if n <= 2 { background: counter(page).get(n => if n <= 2 {
place(center + horizon, circle(radius: 1cm, fill: luma(90%))) place(center + horizon, circle(radius: 1cm, fill: luma(90%)))
} })
) )
But, soft! what light through yonder window breaks? It is the east, and Juliet But, soft! what light through yonder window breaks? It is the east, and Juliet

View File

@ -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)

View File

@ -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!_]

View File

@ -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)") #set heading(numbering: "(1/a)")
#show heading.where(level: 1): set text(12pt) #show heading.where(level: 1): set text(12pt)
#show heading.where(level: 2): set text(10pt) #show heading.where(level: 2): set text(10pt)