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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,7 +128,7 @@ impl Layout for ListNode {
};
let depth = self.depth(styles);
let marker = self.marker(styles).resolve(vt.world(), depth)?;
let marker = self.marker(styles).resolve(vt.world, depth)?;
let mut cells = vec![];
for item in self.children() {

View File

@ -47,10 +47,7 @@ use std::mem;
use typed_arena::Arena;
use typst::diag::SourceResult;
use typst::model::{
applicable, realize, Content, Node, SequenceNode, StyleChain, StyleVecBuilder,
StyledNode,
};
use typst::model::{applicable, realize, SequenceNode, StyleVecBuilder, StyledNode};
use crate::math::{FormulaNode, LayoutMath};
use crate::meta::DocumentNode;
@ -103,6 +100,22 @@ pub trait Layout {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>;
/// Layout without side effects.
///
/// This node must be layouted again in the same order for the results to be
/// valid.
fn measure(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
vt.provider.save();
let result = self.layout(vt, styles, regions);
vt.provider.restore();
result
}
}
impl Layout for Content {
@ -417,7 +430,10 @@ impl<'a> FlowBuilder<'a> {
let last_was_parbreak = self.1;
self.1 = false;
if content.is::<VNode>() || content.is::<ColbreakNode>() {
if content.is::<VNode>()
|| content.is::<ColbreakNode>()
|| content.is::<MetaNode>()
{
self.0.push(content.clone(), styles);
return true;
}
@ -457,7 +473,12 @@ struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
if content.is::<SpaceNode>()
if content.is::<MetaNode>() {
if !self.0.is_basically_empty() {
self.0.push(content.clone(), styles);
return true;
}
} else if content.is::<SpaceNode>()
|| content.is::<TextNode>()
|| content.is::<HNode>()
|| content.is::<LinebreakNode>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>() {
let number = 1 + pages.len();
let fragment = page.layout(vt, number, styles)?;
let fragment = page.layout(vt, styles)?;
pages.extend(fragment);
} else {
bail!(child.span(), "unexpected document child");

View File

@ -1,7 +1,10 @@
use std::str::FromStr;
use super::{LocalName, Numbering, NumberingPattern};
use crate::layout::{BlockNode, TableNode, VNode};
use super::{
Count, Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering,
NumberingPattern,
};
use crate::layout::{BlockNode, VNode};
use crate::prelude::*;
use crate::text::TextNode;
@ -23,7 +26,7 @@ use crate::text::TextNode;
///
/// Display: Figure
/// Category: meta
#[node(Locatable, Synthesize, Show, LocalName)]
#[node(Locatable, Synthesize, Count, Show, LocalName)]
pub struct FigureNode {
/// The content of the figure. Often, an [image]($func/image).
#[required]
@ -34,60 +37,34 @@ pub struct FigureNode {
/// How to number the figure. Accepts a
/// [numbering pattern or function]($func/numbering).
#[default(Some(Numbering::Pattern(NumberingPattern::from_str("1").unwrap())))]
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
pub numbering: Option<Numbering>,
/// The vertical gap between the body and caption.
#[default(Em::new(0.65).into())]
pub gap: Length,
/// The figure's number.
#[synthesized]
pub number: Option<NonZeroUsize>,
}
impl FigureNode {
fn element(&self) -> NodeId {
let mut id = self.body().id();
if id != NodeId::of::<TableNode>() {
id = NodeId::of::<Self>();
}
id
}
}
impl Synthesize for FigureNode {
fn synthesize(&mut self, vt: &Vt, styles: StyleChain) {
let my_id = self.0.stable_id();
let element = self.element();
let mut number = None;
let numbering = self.numbering(styles);
if numbering.is_some() {
number = NonZeroUsize::new(
1 + vt
.query_node::<Self>()
.take_while(|figure| figure.0.stable_id() != my_id)
.filter(|figure| figure.element() == element)
.count(),
);
}
self.push_number(number);
self.push_numbering(numbering);
fn synthesize(&mut self, _: &Vt, styles: StyleChain) {
self.push_numbering(self.numbering(styles));
}
}
impl Show for FigureNode {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body();
if let Some(mut caption) = self.caption(styles) {
if let Some(numbering) = self.numbering(styles) {
let number = self.number().unwrap();
let name = self.local_name(TextNode::lang_in(styles));
caption = TextNode::packed(eco_format!("{name}\u{a0}"))
+ numbering.apply(vt.world(), &[number])?.display()
+ CounterNode::new(
Counter::Selector(Selector::node::<Self>()),
CounterAction::Get(numbering),
)
.pack()
.spanned(self.span())
+ TextNode::packed(": ")
+ caption;
}
@ -104,13 +81,16 @@ impl Show for FigureNode {
}
}
impl Count for FigureNode {
fn update(&self) -> Option<CounterUpdate> {
self.numbering(StyleChain::default())
.is_some()
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
}
}
impl LocalName for FigureNode {
fn local_name(&self, lang: Lang) -> &'static str {
let body = self.body();
if body.is::<TableNode>() {
return body.with::<dyn LocalName>().unwrap().local_name(lang);
}
match lang {
Lang::GERMAN => "Abbildung",
Lang::ENGLISH | _ => "Figure",

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -1,38 +1,9 @@
//! Node interaction.
use typst::model::{Content, StyleChain, StyleVec, StyleVecBuilder};
/// How a node interacts with other nodes.
pub trait Behave {
/// The node's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak node is larger than a previous one and thus picked as
/// the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &Content) -> bool {
false
}
}
/// How a node interacts with other nodes in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak node which only survives when a supportive node is before and
/// after it. Furthermore, per consecutive run of weak nodes, only one
/// survives: The one with the lowest weakness level (or the larger one if
/// there is a tie).
Weak(usize),
/// A node that enables adjacent weak nodes to exist. The default.
Supportive,
/// A node that destroys adjacent weak nodes.
Destructive,
/// A node that does not interact at all with other nodes, having the
/// same effect as if it didn't exist.
Ignorant,
}
use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder};
/// A wrapper around a [`StyleVecBuilder`] that allows items to interact.
#[derive(Debug)]
pub struct BehavedBuilder<'a> {
/// The internal builder.
builder: StyleVecBuilder<'a, Content>,
@ -53,11 +24,21 @@ impl<'a> BehavedBuilder<'a> {
}
}
/// Whether the builder is empty.
/// Whether the builder is totally empty.
pub fn is_empty(&self) -> bool {
self.builder.is_empty() && self.staged.is_empty()
}
/// Whether the builder is empty except for some weak items that will
/// probably collapse.
pub fn is_basically_empty(&self) -> bool {
self.builder.is_empty()
&& self
.staged
.iter()
.all(|(_, behaviour, _)| matches!(behaviour, Behaviour::Weak(_)))
}
/// Push an item into the sequence.
pub fn push(&mut self, item: Content, styles: StyleChain<'a>) {
let interaction = item

View File

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

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
/// given string.
fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool {
let world = vt.world();
let world = vt.world;
for family in TextNode::font_in(styles) {
if let Some(font) = world
.book()

View File

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

View File

@ -536,7 +536,7 @@ fn layout(
}
// Apply metadata.
frame.meta(styles);
frame.meta(styles, false);
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);
quote! {
#[doc = #doc]
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Property {
::typst::model::Property::new(
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
::typst::model::Style::Property(::typst::model::Property::new(
::typst::model::NodeId::of::<Self>(),
#name.into(),
#ident.into()
)
))
}
}
}

View File

@ -14,7 +14,7 @@ use crate::geom::{
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
};
use crate::image::Image;
use crate::model::{node, Content, Fold, Introspector, StableId, StyleChain};
use crate::model::{Content, Introspector, MetaNode, StableId, StyleChain};
use crate::syntax::Span;
/// A finished document with metadata and page frames.
@ -271,16 +271,15 @@ impl Frame {
}
/// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) {
if self.is_empty() {
return;
}
for meta in MetaNode::data_in(styles) {
if matches!(meta, Meta::Hide) {
self.clear();
break;
pub fn meta(&mut self, styles: StyleChain, force: bool) {
if force || !self.is_empty() {
for meta in MetaNode::data_in(styles) {
if matches!(meta, Meta::Hide) {
self.clear();
break;
}
self.prepend(Point::zero(), Element::Meta(meta, self.size));
}
self.prepend(Point::zero(), Element::Meta(meta, self.size));
}
}
@ -607,6 +606,16 @@ pub enum Meta {
Node(Content),
}
cast_from_value! {
Meta: "meta",
}
impl PartialEq for Meta {
fn eq(&self, other: &Self) -> bool {
crate::util::hash128(self) == crate::util::hash128(other)
}
}
/// A possibly unresolved link.
#[derive(Debug, Clone, Hash)]
pub enum Link {
@ -623,45 +632,14 @@ impl Link {
pub fn resolve<'a>(
&self,
introspector: impl FnOnce() -> &'a Introspector,
) -> Option<Destination> {
) -> Destination {
match self {
Self::Dest(dest) => Some(dest.clone()),
Self::Node(id) => introspector().location(*id).map(Destination::Internal),
Self::Dest(dest) => dest.clone(),
Self::Node(id) => Destination::Internal(introspector().location(*id)),
}
}
}
/// Host for metadata.
///
/// Display: Meta
/// Category: special
#[node]
pub struct MetaNode {
/// Metadata that should be attached to all elements affected by this style
/// property.
#[fold]
pub data: Vec<Meta>,
}
impl Fold for Vec<Meta> {
type Output = Self;
fn fold(mut self, outer: Self::Output) -> Self::Output {
self.extend(outer);
self
}
}
cast_from_value! {
Meta: "meta",
}
impl PartialEq for Meta {
fn eq(&self, other: &Self) -> bool {
crate::util::hash128(self) == crate::util::hash128(other)
}
}
/// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination {

View File

@ -6,11 +6,12 @@ use comemo::Tracked;
use ecow::EcoString;
use once_cell::sync::OnceCell;
use super::Module;
use super::{Args, Dynamic, Module, Value};
use crate::diag::SourceResult;
use crate::doc::Document;
use crate::geom::{Abs, Dir};
use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt};
use crate::syntax::Span;
use crate::util::hash128;
use crate::World;
@ -89,6 +90,14 @@ pub struct LangItems {
pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// Dispatch a method on a counter. This is hacky and should be superseded
/// by more dynamic method dispatch.
pub counter_method: fn(
dynamic: &Dynamic,
method: &str,
args: Args,
span: Span,
) -> SourceResult<Value>,
}
impl Debug for LangItems {

View File

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

View File

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

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 {
type Output = Align;

View File

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

View File

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

View File

@ -8,8 +8,12 @@ use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec};
use once_cell::sync::Lazy;
use super::{node, Guard, Locatable, Recipe, StableId, Style, StyleMap, Synthesize};
use super::{
node, Behave, Behaviour, Fold, Guard, Locatable, Recipe, StableId, Style, StyleMap,
Synthesize,
};
use crate::diag::{SourceResult, StrResult};
use crate::doc::Meta;
use crate::eval::{
cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm,
};
@ -35,9 +39,15 @@ enum Modifier {
}
impl Content {
/// Create a content of the given node kind.
pub fn new<T: Node>() -> Self {
Self::new_of(T::id())
}
/// Create a content of the given node kind.
pub fn new_of(id: NodeId) -> Self {
Self {
id: T::id(),
id,
span: Span::detached(),
fields: EcoVec::new(),
modifiers: EcoVec::new(),
@ -133,11 +143,10 @@ impl Content {
.map(|(_, value)| value)
}
/// Access a field on the content as a specified type.
#[track_caller]
/// Try to access a field on the content as a specified type.
pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> {
match self.field(name) {
Some(value) => Some(value.clone().cast().unwrap()),
Some(value) => value.clone().cast().ok(),
None => None,
}
}
@ -145,7 +154,7 @@ impl Content {
/// Expect a field on the content to exist as a specified type.
#[track_caller]
pub fn expect_field<T: Cast>(&self, name: &str) -> T {
self.cast_field(name).unwrap()
self.field(name).unwrap().clone().cast().unwrap()
}
/// List all fields on the content.
@ -500,6 +509,33 @@ cast_from_value! {
StyleMap: "style map",
}
/// Host for metadata.
///
/// Display: Meta
/// Category: special
#[node(Behave)]
pub struct MetaNode {
/// Metadata that should be attached to all elements affected by this style
/// property.
#[fold]
pub data: Vec<Meta>,
}
impl Behave for MetaNode {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}
impl Fold for Vec<Meta> {
type Output = Self;
fn fold(mut self, outer: Self::Output) -> Self::Output {
self.extend(outer);
self
}
}
/// The missing key access error message.
#[cold]
#[track_caller]

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::doc::{Meta, MetaNode};
use crate::doc::Meta;
use crate::util::hash128;
/// Whether the target is affected by show rules in the given style chain.
pub fn applicable(target: &Content, styles: StyleChain) -> bool {
@ -36,7 +37,7 @@ pub fn realize(
if target.needs_preparation() {
let mut node = target.clone();
if target.can::<dyn Locatable>() || target.label().is_some() {
let id = vt.identify(target);
let id = vt.provider.identify(hash128(target));
node.set_stable_id(id);
}
@ -47,8 +48,12 @@ pub fn realize(
node.mark_prepared();
if node.stable_id().is_some() {
let span = node.span();
let meta = Meta::Node(node.clone());
return Ok(Some(node.styled(MetaNode::set_data(vec![meta]))));
return Ok(Some(
(node + MetaNode::new().pack().spanned(span))
.styled(MetaNode::set_data(vec![meta])),
));
}
return Ok(Some(node));
@ -103,7 +108,7 @@ fn try_apply(
return Ok(None);
}
recipe.apply(vt.world(), target.clone().guarded(guard)).map(Some)
recipe.apply(vt.world, target.clone().guarded(guard)).map(Some)
}
Some(Selector::Label(label)) => {
@ -111,7 +116,7 @@ fn try_apply(
return Ok(None);
}
recipe.apply(vt.world(), target.clone().guarded(guard)).map(Some)
recipe.apply(vt.world, target.clone().guarded(guard)).map(Some)
}
Some(Selector::Regex(regex)) => {
@ -135,7 +140,7 @@ fn try_apply(
}
let piece = make(m.as_str().into()).guarded(guard);
let transformed = recipe.apply(vt.world(), piece)?;
let transformed = recipe.apply(vt.world, piece)?;
result.push(transformed);
cursor = m.end();
}
@ -151,6 +156,9 @@ fn try_apply(
Ok(Some(Content::sequence(result)))
}
// Not supported here.
Some(Selector::Any(_)) => Ok(None),
None => Ok(None),
}
}
@ -178,6 +186,36 @@ pub trait Finalize {
fn finalize(&self, realized: Content, styles: StyleChain) -> Content;
}
/// How a node interacts with other nodes.
pub trait Behave {
/// The node's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak node is larger than a previous one and thus picked as
/// the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &Content) -> bool {
false
}
}
/// How a node interacts with other nodes in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak node which only survives when a supportive node is before and
/// after it. Furthermore, per consecutive run of weak nodes, only one
/// survives: The one with the lowest weakness level (or the larger one if
/// there is a tie).
Weak(usize),
/// A node that enables adjacent weak nodes to exist. The default.
Supportive,
/// A node that destroys adjacent weak nodes.
Destructive,
/// A node that does not interact at all with other nodes, having the
/// same effect as if it didn't exist.
Ignorant,
}
/// Guards content against being affected by the same show rule multiple times.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Guard {

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ pub use buffer::Buffer;
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::num::NonZeroUsize;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
@ -39,6 +40,19 @@ pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
state.finish128().as_u128()
}
/// Extra methods for [`NonZeroUsize`].
pub trait NonZeroExt {
/// The number `1`.
const ONE: Self;
}
impl NonZeroExt for NonZeroUsize {
const ONE: Self = match Self::new(1) {
Some(v) => v,
None => unreachable!(),
};
}
/// Extra methods for [`str`].
pub trait StrExt {
/// The number of code units this string would use if it was encoded in

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

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