Counters
5
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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(),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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(())
|
||||
|
@ -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());
|
||||
|
@ -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() {
|
||||
|
@ -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>()
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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())?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
@ -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
@ -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<_>>()?),
|
||||
}
|
@ -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");
|
||||
|
@ -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",
|
||||
|
@ -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())))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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::*;
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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};
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -536,7 +536,7 @@ fn layout(
|
||||
}
|
||||
|
||||
// Apply metadata.
|
||||
frame.meta(styles);
|
||||
frame.meta(styles, false);
|
||||
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/doc.rs
@ -14,7 +14,7 @@ use crate::geom::{
|
||||
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
||||
};
|
||||
use crate::image::Image;
|
||||
use crate::model::{node, Content, Fold, Introspector, StableId, StyleChain};
|
||||
use crate::model::{Content, Introspector, MetaNode, StableId, StyleChain};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// A finished document with metadata and page frames.
|
||||
@ -271,16 +271,15 @@ impl Frame {
|
||||
}
|
||||
|
||||
/// Attach the metadata from this style chain to the frame.
|
||||
pub fn meta(&mut self, styles: StyleChain) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
for meta in MetaNode::data_in(styles) {
|
||||
if matches!(meta, Meta::Hide) {
|
||||
self.clear();
|
||||
break;
|
||||
pub fn meta(&mut self, styles: StyleChain, force: bool) {
|
||||
if force || !self.is_empty() {
|
||||
for meta in MetaNode::data_in(styles) {
|
||||
if matches!(meta, Meta::Hide) {
|
||||
self.clear();
|
||||
break;
|
||||
}
|
||||
self.prepend(Point::zero(), Element::Meta(meta, self.size));
|
||||
}
|
||||
self.prepend(Point::zero(), Element::Meta(meta, self.size));
|
||||
}
|
||||
}
|
||||
|
||||
@ -607,6 +606,16 @@ pub enum Meta {
|
||||
Node(Content),
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Meta: "meta",
|
||||
}
|
||||
|
||||
impl PartialEq for Meta {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
crate::util::hash128(self) == crate::util::hash128(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A possibly unresolved link.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Link {
|
||||
@ -623,45 +632,14 @@ impl Link {
|
||||
pub fn resolve<'a>(
|
||||
&self,
|
||||
introspector: impl FnOnce() -> &'a Introspector,
|
||||
) -> Option<Destination> {
|
||||
) -> Destination {
|
||||
match self {
|
||||
Self::Dest(dest) => Some(dest.clone()),
|
||||
Self::Node(id) => introspector().location(*id).map(Destination::Internal),
|
||||
Self::Dest(dest) => dest.clone(),
|
||||
Self::Node(id) => Destination::Internal(introspector().location(*id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Host for metadata.
|
||||
///
|
||||
/// Display: Meta
|
||||
/// Category: special
|
||||
#[node]
|
||||
pub struct MetaNode {
|
||||
/// Metadata that should be attached to all elements affected by this style
|
||||
/// property.
|
||||
#[fold]
|
||||
pub data: Vec<Meta>,
|
||||
}
|
||||
|
||||
impl Fold for Vec<Meta> {
|
||||
type Output = Self;
|
||||
|
||||
fn fold(mut self, outer: Self::Output) -> Self::Output {
|
||||
self.extend(outer);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Meta: "meta",
|
||||
}
|
||||
|
||||
impl PartialEq for Meta {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
crate::util::hash128(self) == crate::util::hash128(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A link destination.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Destination {
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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));
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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 {
|
||||
|
@ -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)>,
|
||||
|
@ -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() })
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
BIN
tests/ref/meta/counter-page.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
tests/ref/meta/counter.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@ -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
|
||||
|
9
tests/typ/meta/counter-page.typ
Normal 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)
|
48
tests/typ/meta/counter.typ
Normal 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!_]
|
@ -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)
|
||||
|