From 529d3e10c6b4d973e88b6c295eb22a45ea426e42 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 11 Mar 2023 17:42:40 +0100 Subject: [PATCH] Section references --- library/src/layout/mod.rs | 14 ---- library/src/meta/heading.rs | 73 ++++++++++++------ library/src/meta/numbering.rs | 17 ++-- library/src/meta/outline.rs | 60 ++++++++------- library/src/meta/reference.rs | 141 +++++++++++++++++++++++++++++++--- library/src/prelude.rs | 4 +- library/src/text/raw.rs | 10 +-- macros/src/node.rs | 24 ++++-- src/eval/library.rs | 4 +- src/eval/mod.rs | 2 +- src/ide/complete.rs | 1 - src/model/content.rs | 33 ++++++-- src/model/realize.rs | 20 ++++- src/model/typeset.rs | 31 ++++++-- src/syntax/parser.rs | 38 ++++----- tests/ref/meta/ref.png | Bin 0 -> 11693 bytes tests/typ/compiler/set.typ | 4 +- tests/typ/meta/ref.typ | 21 +++++ 18 files changed, 362 insertions(+), 135 deletions(-) create mode 100644 tests/ref/meta/ref.png create mode 100644 tests/typ/meta/ref.typ diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index dc373ff5..eb440b7f 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -232,20 +232,6 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { self.scratch.content.alloc(FormulaNode::new(content.clone()).pack()); } - // Prepare only if this is the first application for this node. - if content.can::() { - if !content.is_prepared() { - let prepared = content - .clone() - .prepared() - .with::() - .unwrap() - .prepare(self.vt, styles)?; - let stored = self.scratch.content.alloc(prepared); - return self.accept(stored, styles); - } - } - if let Some(styled) = content.to::() { return self.styled(styled, styles); } diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 8677aa55..1bff3af4 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -40,7 +40,7 @@ use crate::text::{TextNode, TextSize}; /// /// Display: Heading /// Category: meta -#[node(Prepare, Show, Finalize)] +#[node(Synthesize, Show, Finalize)] pub struct HeadingNode { /// The logical nesting depth of the heading, starting from one. #[default(NonZeroUsize::new(1).unwrap())] @@ -76,44 +76,61 @@ pub struct HeadingNode { /// The heading's title. #[required] pub body: Content, + + /// The heading's numbering numbers. + /// + /// ```example + /// #show heading: it => it.numbers + /// + /// = First + /// == Second + /// = Third + /// ``` + #[synthesized] + pub numbers: Option>, } -impl Prepare for HeadingNode { - fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { +impl Synthesize for HeadingNode { + fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content { let my_id = vt.identify(self); + let numbered = self.numbering(styles).is_some(); let mut counter = HeadingCounter::new(); - for (node_id, node) in vt.locate(Selector::node::()) { - if node_id == my_id { - break; + if numbered { + // Advance passed existing headings. + for (_, node) in vt + .locate(Selector::node::()) + .into_iter() + .take_while(|&(id, _)| id != my_id) + { + let heading = node.to::().unwrap(); + if heading.numbering(StyleChain::default()).is_some() { + counter.advance(heading); + } } - let numbers = node.field("numbers").unwrap(); - if *numbers != Value::None { - let heading = node.to::().unwrap(); - counter.advance(heading); - } + // Advance passed self. + counter.advance(self); } - let mut numbers = Value::None; - if let Some(numbering) = self.numbering(styles) { - numbers = numbering.apply(vt.world(), counter.advance(self))?; - } + let node = self + .clone() + .with_outlined(self.outlined(styles)) + .with_numbering(self.numbering(styles)) + .with_numbers(numbered.then(|| counter.take())) + .pack(); - let mut node = self.clone().pack(); - node.push_field("outlined", Value::Bool(self.outlined(styles))); - node.push_field("numbers", numbers); let meta = Meta::Node(my_id, node.clone()); - Ok(node.styled(MetaNode::set_data(vec![meta]))) + node.styled(MetaNode::set_data(vec![meta])) } } impl Show for HeadingNode { - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { let mut realized = self.body(); - let numbers = self.0.field("numbers").unwrap(); - if *numbers != Value::None { - realized = numbers.clone().display() + if let Some(numbering) = self.numbering(styles) { + let numbers = self.numbers().unwrap(); + realized = numbering.apply(vt.world(), &numbers)?.display() + HNode::new(Em::new(0.3).into()).with_weak(true).pack() + realized; } @@ -168,4 +185,14 @@ impl HeadingCounter { &self.0 } + + /// Take out the current counts. + pub fn take(self) -> Vec { + self.0 + } +} + +cast_from_value! { + HeadingNode, + v: Content => v.to::().ok_or("expected heading")?.clone(), } diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index 4e6e1aed..d71fb233 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -82,7 +82,7 @@ impl Numbering { numbers: &[NonZeroUsize], ) -> SourceResult { Ok(match self { - Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), + Self::Pattern(pattern) => Value::Str(pattern.apply(numbers, false).into()), Self::Func(func) => { let args = Args::new( func.span(), @@ -124,12 +124,16 @@ pub struct NumberingPattern { impl NumberingPattern { /// Apply the pattern to the given number. - pub fn apply(&self, numbers: &[NonZeroUsize]) -> EcoString { + pub fn apply(&self, numbers: &[NonZeroUsize], trimmed: bool) -> EcoString { let mut fmt = EcoString::new(); let mut numbers = numbers.into_iter(); - for ((prefix, kind, case), &n) in self.pieces.iter().zip(&mut numbers) { - fmt.push_str(prefix); + for (i, ((prefix, kind, case), &n)) in + self.pieces.iter().zip(&mut numbers).enumerate() + { + if i > 0 || !trimmed { + fmt.push_str(prefix); + } fmt.push_str(&kind.apply(n, *case)); } @@ -144,7 +148,10 @@ impl NumberingPattern { fmt.push_str(&kind.apply(n, *case)); } - fmt.push_str(&self.suffix); + if !trimmed { + fmt.push_str(&self.suffix); + } + fmt } diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index d66a573d..a2b12511 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -22,7 +22,7 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode}; /// /// Display: Outline /// Category: meta -#[node(Prepare, Show)] +#[node(Synthesize, Show)] pub struct OutlineNode { /// The title of the outline. /// @@ -67,21 +67,22 @@ pub struct OutlineNode { /// ``` #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))] pub fill: Option, + + /// All outlined headings in the document. + #[synthesized] + pub headings: Vec, } -impl Prepare for OutlineNode { - fn prepare(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { +impl Synthesize for OutlineNode { + fn synthesize(&self, vt: &mut Vt, _: StyleChain) -> Content { let headings = vt .locate(Selector::node::()) .into_iter() - .map(|(_, node)| node) - .filter(|node| *node.field("outlined").unwrap() == Value::Bool(true)) - .map(|node| Value::Content(node.clone())) + .map(|(_, node)| node.to::().unwrap().clone()) + .filter(|node| node.outlined(StyleChain::default())) .collect(); - let mut node = self.clone().pack(); - node.push_field("headings", Value::Array(Array::from_vec(headings))); - Ok(node) + self.clone().with_headings(headings).pack() } } @@ -108,13 +109,12 @@ impl Show for OutlineNode { let indent = self.indent(styles); let depth = self.depth(styles); - let mut ancestors: Vec<&Content> = vec![]; - for (_, node) in vt.locate(Selector::node::()) { - if *node.field("outlined").unwrap() != Value::Bool(true) { + let mut ancestors: Vec<&HeadingNode> = vec![]; + for heading in self.headings().iter() { + if !heading.outlined(StyleChain::default()) { continue; } - let heading = node.to::().unwrap(); if let Some(depth) = depth { if depth < heading.level(StyleChain::default()) { continue; @@ -122,37 +122,40 @@ impl Show for OutlineNode { } while ancestors.last().map_or(false, |last| { - last.to::().unwrap().level(StyleChain::default()) - >= heading.level(StyleChain::default()) + last.level(StyleChain::default()) >= heading.level(StyleChain::default()) }) { ancestors.pop(); } // Adjust the link destination a bit to the topleft so that the // heading is fully visible. - let mut loc = node.field("loc").unwrap().clone().cast::().unwrap(); + let mut loc = heading.0.expect_field::("location"); loc.pos -= Point::splat(Abs::pt(10.0)); // Add hidden ancestors numberings to realize the indent. if indent { - let hidden: Vec<_> = ancestors - .iter() - .map(|node| node.field("numbers").unwrap()) - .filter(|&numbers| *numbers != Value::None) - .map(|numbers| numbers.clone().display() + SpaceNode::new().pack()) - .collect(); + 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(); + }; + } - if !hidden.is_empty() { - seq.push(HideNode::new(Content::sequence(hidden)).pack()); + if !ancestors.is_empty() { + seq.push(HideNode::new(hidden).pack()); seq.push(SpaceNode::new().pack()); } } // Format the numbering. let mut start = heading.body(); - let numbers = node.field("numbers").unwrap(); - if *numbers != Value::None { - start = numbers.clone().display() + SpaceNode::new().pack() + start; + if let Some(numbering) = heading.numbering(StyleChain::default()) { + let numbers = heading.numbers().unwrap(); + start = numbering.apply(vt.world(), &numbers)?.display() + + SpaceNode::new().pack() + + start; }; // Add the numbering and section name. @@ -176,8 +179,7 @@ impl Show for OutlineNode { let end = TextNode::packed(eco_format!("{}", loc.page)); seq.push(end.linked(Destination::Internal(loc))); seq.push(LinebreakNode::new().pack()); - - ancestors.push(node); + ancestors.push(heading); } seq.push(ParbreakNode::new().pack()); diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index bfc31785..a46198bd 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,31 +1,152 @@ +use super::{HeadingNode, Numbering}; use crate::prelude::*; use crate::text::TextNode; /// A reference to a label. /// -/// *Note: This function is currently unimplemented.* -/// /// The reference function produces a textual reference to a label. For example, /// a reference to a heading will yield an appropriate string such as "Section -/// 1" for a reference to the first heading's label. The references are also -/// links to the respective labels. +/// 1" for a reference to the first heading. The references are also links to +/// the respective element. +/// +/// # Example +/// ```example +/// #set heading(numbering: "1.") +/// +/// = Introduction +/// Recent developments in typesetting +/// software have rekindled hope in +/// previously frustrated researchers. +/// As shown in @results, we ... +/// +/// = Results +/// We evaluate our method in a +/// series of tests. @perf discusses +/// the performance aspects of ... +/// +/// == Performance +/// As described in @intro, we ... +/// ``` /// /// ## Syntax /// This function also has dedicated syntax: A reference to a label can be -/// created by typing an `@` followed by the name of the label (e.g. `[= -/// Introduction ]` can be referenced by typing `[@intro]`). +/// created by typing an `@` followed by the name of the label (e.g. +/// `[= Introduction ]` can be referenced by typing `[@intro]`). /// /// Display: Reference /// Category: meta -#[node(Show)] +#[node(Synthesize, Show)] pub struct RefNode { /// The label that should be referenced. #[required] - pub target: EcoString, + pub label: Label, + + /// The prefix before the referenced number. + /// + /// ```example + /// #set heading(numbering: "1.") + /// #set ref(prefix: it => { + /// if it.func() == heading { + /// "Chapter" + /// } else { + /// "Thing" + /// } + /// }) + /// + /// = Introduction + /// In @intro, we see how to turn + /// Sections into Chapters. + /// ``` + pub prefix: Smart>, + + /// All elements with the `target` label in the document. + #[synthesized] + pub matches: Vec, +} + +impl Synthesize for RefNode { + fn synthesize(&self, vt: &mut Vt, _: StyleChain) -> Content { + let matches = vt + .locate(Selector::Label(self.label())) + .into_iter() + .map(|(_, node)| node.clone()) + .collect(); + + self.clone().with_matches(matches).pack() + } } impl Show for RefNode { - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult { - Ok(TextNode::packed(eco_format!("@{}", self.target()))) + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { + let matches = self.matches(); + 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" + }); + } else { + return Ok(Content::empty()); + } + }; + + let mut prefix = match self.prefix(styles) { + Smart::Auto => prefix(target, TextNode::lang_in(styles)) + .map(TextNode::packed) + .unwrap_or_default(), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(func)) => { + let args = Args::new(func.span(), [target.clone().into()]); + func.call_detached(vt.world(), args)?.display() + } + }; + + if !prefix.is_empty() { + prefix += TextNode::packed('\u{a0}'); + } + + let formatted = if let Some(heading) = target.to::() { + if let Some(numbering) = heading.numbering(StyleChain::default()) { + let numbers = heading.numbers().unwrap(); + numbered(vt, prefix, &numbering, &numbers)? + } else { + bail!(self.span(), "cannot reference unnumbered heading"); + } + } else { + bail!(self.span(), "cannot reference {}", target.id().name); + }; + + let loc = target.expect_field::("location"); + Ok(formatted.linked(Destination::Internal(loc))) + } +} + +/// Generate a numbered reference like "Section 1.1". +fn numbered( + vt: &Vt, + prefix: Content, + numbering: &Numbering, + numbers: &[NonZeroUsize], +) -> SourceResult { + Ok(prefix + + match numbering { + Numbering::Pattern(pattern) => { + TextNode::packed(pattern.apply(&numbers, true)) + } + Numbering::Func(_) => numbering.apply(vt.world(), &numbers)?.display(), + }) +} + +/// The default prefix. +fn prefix(node: &Content, lang: Lang) -> Option<&str> { + if node.is::() { + match lang { + Lang::ENGLISH => Some("Section"), + Lang::GERMAN => Some("Abschnitt"), + _ => None, + } + } else { + None } } diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 49afc9ca..a9b19f58 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -22,8 +22,8 @@ pub use typst::eval::{ pub use typst::geom::*; #[doc(no_inline)] pub use typst::model::{ - node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Prepare, - Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec, + node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Resolve, + Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec, Synthesize, Unlabellable, Vt, }; #[doc(no_inline)] diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index 99fb89d2..3f03ba8e 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -35,7 +35,7 @@ use crate::prelude::*; /// /// Display: Raw Text / Code /// Category: text -#[node(Prepare, Show, Finalize)] +#[node(Synthesize, Show, Finalize)] pub struct RawNode { /// The raw text. /// @@ -120,11 +120,9 @@ impl RawNode { } } -impl Prepare for RawNode { - fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { - let mut node = self.clone().pack(); - node.push_field("lang", self.lang(styles).clone()); - Ok(node) +impl Synthesize for RawNode { + fn synthesize(&self, _: &mut Vt, styles: StyleChain) -> Content { + self.clone().with_lang(self.lang(styles)).pack() } } diff --git a/macros/src/node.rs b/macros/src/node.rs index f89ee8df..678a154d 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -25,6 +25,7 @@ struct Field { positional: bool, required: bool, variadic: bool, + synthesized: bool, fold: bool, resolve: bool, parse: Option, @@ -88,6 +89,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { positional, required, variadic, + synthesized: has_attr(&mut attrs, "synthesized"), fold: has_attr(&mut attrs, "fold"), resolve: has_attr(&mut attrs, "resolve"), parse: parse_attr(&mut attrs, "parse")?.flatten(), @@ -154,7 +156,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { fn create(node: &Node) -> TokenStream { let Node { vis, ident, docs, .. } = node; let all = node.fields.iter().filter(|field| !field.external); - let settable = all.clone().filter(|field| field.settable()); + let settable = all.clone().filter(|field| !field.synthesized && field.settable()); // Inherent methods and functions. let new = create_new_func(node); @@ -176,7 +178,7 @@ fn create(node: &Node) -> TokenStream { #[doc = #docs] #[derive(Debug, Clone, Hash)] #[repr(transparent)] - #vis struct #ident(::typst::model::Content); + #vis struct #ident(pub ::typst::model::Content); impl #ident { #new @@ -205,7 +207,10 @@ fn create(node: &Node) -> TokenStream { /// Create the `new` function for the node. fn create_new_func(node: &Node) -> TokenStream { - let relevant = node.fields.iter().filter(|field| !field.external && field.inherent()); + let relevant = node + .fields + .iter() + .filter(|field| !field.external && !field.synthesized && field.inherent()); let params = relevant.clone().map(|Field { ident, ty, .. }| { quote! { #ident: #ty } }); @@ -224,11 +229,11 @@ fn create_new_func(node: &Node) -> TokenStream { /// Create an accessor methods for a field. fn create_field_method(field: &Field) -> TokenStream { let Field { vis, docs, ident, name, output, .. } = field; - if field.inherent() { + if field.inherent() || field.synthesized { quote! { #[doc = #docs] #vis fn #ident(&self) -> #output { - self.0.field(#name).unwrap().clone().cast().unwrap() + self.0.expect_field(#name) } } } else { @@ -311,7 +316,7 @@ fn create_node_impl(node: &Node) -> TokenStream { let infos = node .fields .iter() - .filter(|field| !field.internal) + .filter(|field| !field.internal && !field.synthesized) .map(create_param_info); quote! { impl ::typst::model::Node for #ident { @@ -395,7 +400,11 @@ fn create_construct_impl(node: &Node) -> TokenStream { let handlers = node .fields .iter() - .filter(|field| !field.external && (!field.internal || field.parse.is_some())) + .filter(|field| { + !field.external + && !field.synthesized + && (!field.internal || field.parse.is_some()) + }) .map(|field| { let with_ident = &field.with_ident; let (prefix, value) = create_field_parser(field); @@ -436,6 +445,7 @@ fn create_set_impl(node: &Node) -> TokenStream { .iter() .filter(|field| { !field.external + && !field.synthesized && field.settable() && (!field.internal || field.parse.is_some()) }) diff --git a/src/eval/library.rs b/src/eval/library.rs index c37c16fd..14f02d98 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -9,7 +9,7 @@ use super::Module; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; -use crate::model::{Content, NodeId, StyleChain, StyleMap, Vt}; +use crate::model::{Content, Label, NodeId, StyleChain, StyleMap, Vt}; use crate::util::hash128; /// Definition of Typst's standard library. @@ -60,7 +60,7 @@ pub struct LangItems { /// A hyperlink: `https://typst.org`. pub link: fn(url: EcoString) -> Content, /// A reference: `@target`. - pub ref_: fn(target: EcoString) -> Content, + pub ref_: fn(target: Label) -> Content, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, /// An item in a bullet list: `- ...`. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 145f961a..fe56c060 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -561,7 +561,7 @@ impl Eval for ast::Ref { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.ref_)(self.get().into())) + Ok((vm.items.ref_)(Label(self.get().into()))) } } diff --git a/src/ide/complete.rs b/src/ide/complete.rs index a0d5e9a4..a7b001ae 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -974,7 +974,6 @@ impl<'a> CompletionContext<'a> { let detail = docs.map(Into::into).or_else(|| match value { Value::Symbol(_) => None, - Value::Content(_) => None, Value::Func(func) => { func.info().map(|info| plain_docs_sentence(info.docs).into()) } diff --git a/src/model/content.rs b/src/model/content.rs index 17fa786b..be737331 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -10,7 +10,7 @@ use once_cell::sync::Lazy; use super::{node, Guard, Recipe, Style, StyleMap}; use crate::diag::{SourceResult, StrResult}; -use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm}; +use crate::eval::{cast_from_value, Args, Cast, FuncInfo, Str, Value, Vm}; use crate::syntax::Span; use crate::util::pretty_array_like; use crate::World; @@ -27,7 +27,7 @@ pub struct Content { /// Modifiers that can be attached to content. #[derive(Debug, Clone, PartialEq, Hash)] enum Modifier { - Prepared, + Synthesized, Guard(Guard), } @@ -59,6 +59,12 @@ impl Content { self.id } + /// Whether the content is empty. + pub fn is_empty(&self) -> bool { + self.to::() + .map_or(false, |seq| seq.children().is_empty()) + } + /// Whether the contained node is of type `T`. pub fn is(&self) -> bool where @@ -112,6 +118,21 @@ impl Content { .map(|(_, value)| value) } + /// Access a field on the content as a specified type. + #[track_caller] + pub fn cast_field(&self, name: &str) -> Option { + match self.field(name) { + Some(value) => Some(value.clone().cast().unwrap()), + None => None, + } + } + + /// Expect a field on the content to exist as a specified type. + #[track_caller] + pub fn expect_field(&self, name: &str) -> T { + self.cast_field(name).unwrap() + } + /// List all fields on the content. pub fn fields(&self) -> &[(EcoString, Value)] { &self.fields @@ -209,14 +230,14 @@ impl Content { } /// Mark this content as prepared. - pub fn prepared(mut self) -> Self { - self.modifiers.push(Modifier::Prepared); + pub fn synthesized(mut self) -> Self { + self.modifiers.push(Modifier::Synthesized); self } /// Whether this node was prepared. - pub fn is_prepared(&self) -> bool { - self.modifiers.contains(&Modifier::Prepared) + pub fn is_synthesized(&self) -> bool { + self.modifiers.contains(&Modifier::Synthesized) } /// Whether no show rule was executed for this node so far. diff --git a/src/model/realize.rs b/src/model/realize.rs index c4c67a4f..4685a605 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -3,7 +3,7 @@ use crate::diag::SourceResult; /// Whether the target is affected by show rules in the given style chain. pub fn applicable(target: &Content, styles: StyleChain) -> bool { - if target.can::() && !target.is_prepared() { + if target.can::() && !target.is_synthesized() { return true; } @@ -34,6 +34,18 @@ pub fn realize( // Find out how many recipes there are. let mut n = styles.recipes().count(); + // Synthesize if not already happened for this node. + if target.can::() && !target.is_synthesized() { + return Ok(Some( + target + .clone() + .synthesized() + .with::() + .unwrap() + .synthesize(vt, styles), + )); + } + // Find an applicable recipe. let mut realized = None; for recipe in styles.recipes() { @@ -132,10 +144,10 @@ fn try_apply( } } -/// Preparations before execution of any show rule. -pub trait Prepare { +/// Synthesize fields on a node. This happens before execution of any show rule. +pub trait Synthesize { /// Prepare the node for show rule application. - fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult; + fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content; } /// The base recipe for a node. diff --git a/src/model/typeset.rs b/src/model/typeset.rs index 6361e6ce..377c7c76 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -77,6 +77,11 @@ impl<'a> Vt<'a> { 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 selector. pub fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { self.introspector.locate(selector) @@ -115,6 +120,7 @@ impl StabilityProvider { /// Provides access to information about the document. #[doc(hidden)] pub struct Introspector { + init: bool, nodes: Vec<(StableId, Content)>, queries: RefCell>, } @@ -122,7 +128,11 @@ pub struct Introspector { impl Introspector { /// Create a new introspector. fn new() -> Self { - Self { nodes: vec![], queries: RefCell::new(vec![]) } + Self { + init: false, + nodes: vec![], + queries: RefCell::new(vec![]), + } } /// Update the information given new frames and return whether we can stop @@ -135,14 +145,20 @@ impl Introspector { self.extract(frame, page, Transform::identity()); } + 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.locate_impl(&selector); - if hash128(&nodes) != hash { + + for (selector, hash) in &queries { + let nodes = self.locate_impl(selector); + if hash128(&nodes) != *hash { return false; } } + if !was_init && !queries.is_empty() { + return false; + } + true } @@ -161,7 +177,7 @@ impl Introspector { let pos = pos.transform(ts); let mut node = content.clone(); let loc = Location { page, pos }; - node.push_field("loc", loc); + node.push_field("location", loc); self.nodes.push((id, node)); } } @@ -173,6 +189,11 @@ impl Introspector { #[comemo::track] impl Introspector { + /// Whether this introspector is not yet initialized. + fn init(&self) -> bool { + self.init + } + /// Locate all metadata matches for the given selector. fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { let nodes = self.locate_impl(&selector); diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index b4321dbe..201d78fa 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -26,7 +26,7 @@ fn markup( p: &mut Parser, mut at_start: bool, min_indent: usize, - mut stop: impl FnMut(SyntaxKind) -> bool, + mut stop: impl FnMut(&Parser) -> bool, ) { let m = p.marker(); let mut nesting: usize = 0; @@ -34,7 +34,7 @@ fn markup( match p.current() { SyntaxKind::LeftBracket => nesting += 1, SyntaxKind::RightBracket if nesting > 0 => nesting -= 1, - _ if stop(p.current) => break, + _ if stop(p) => break, _ => {} } @@ -133,10 +133,10 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) { fn strong(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::Star); - markup(p, false, 0, |kind| { - kind == SyntaxKind::Star - || kind == SyntaxKind::Parbreak - || kind == SyntaxKind::RightBracket + markup(p, false, 0, |p| { + p.at(SyntaxKind::Star) + || p.at(SyntaxKind::Parbreak) + || p.at(SyntaxKind::RightBracket) }); p.expect(SyntaxKind::Star); p.wrap(m, SyntaxKind::Strong); @@ -145,10 +145,10 @@ fn strong(p: &mut Parser) { fn emph(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::Underscore); - markup(p, false, 0, |kind| { - kind == SyntaxKind::Underscore - || kind == SyntaxKind::Parbreak - || kind == SyntaxKind::RightBracket + markup(p, false, 0, |p| { + p.at(SyntaxKind::Underscore) + || p.at(SyntaxKind::Parbreak) + || p.at(SyntaxKind::RightBracket) }); p.expect(SyntaxKind::Underscore); p.wrap(m, SyntaxKind::Emph); @@ -158,8 +158,10 @@ fn heading(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::HeadingMarker); whitespace_line(p); - markup(p, false, usize::MAX, |kind| { - kind == SyntaxKind::Label || kind == SyntaxKind::RightBracket + markup(p, false, usize::MAX, |p| { + p.at(SyntaxKind::Label) + || p.at(SyntaxKind::RightBracket) + || (p.at(SyntaxKind::Space) && p.lexer.clone().next() == SyntaxKind::Label) }); p.wrap(m, SyntaxKind::Heading); } @@ -169,7 +171,7 @@ fn list_item(p: &mut Parser) { p.assert(SyntaxKind::ListMarker); let min_indent = p.column(p.prev_end()); whitespace_line(p); - markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); p.wrap(m, SyntaxKind::ListItem); } @@ -178,7 +180,7 @@ fn enum_item(p: &mut Parser) { p.assert(SyntaxKind::EnumMarker); let min_indent = p.column(p.prev_end()); whitespace_line(p); - markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); p.wrap(m, SyntaxKind::EnumItem); } @@ -187,12 +189,12 @@ fn term_item(p: &mut Parser) { p.assert(SyntaxKind::TermMarker); let min_indent = p.column(p.prev_end()); whitespace_line(p); - markup(p, false, usize::MAX, |kind| { - kind == SyntaxKind::Colon || kind == SyntaxKind::RightBracket + markup(p, false, usize::MAX, |p| { + p.at(SyntaxKind::Colon) || p.at(SyntaxKind::RightBracket) }); p.expect(SyntaxKind::Colon); whitespace_line(p); - markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); p.wrap(m, SyntaxKind::TermItem); } @@ -679,7 +681,7 @@ fn content_block(p: &mut Parser) { let m = p.marker(); p.enter(LexMode::Markup); p.assert(SyntaxKind::LeftBracket); - markup(p, true, 0, |kind| kind == SyntaxKind::RightBracket); + markup(p, true, 0, |p| p.at(SyntaxKind::RightBracket)); p.expect(SyntaxKind::RightBracket); p.exit(); p.wrap(m, SyntaxKind::ContentBlock); diff --git a/tests/ref/meta/ref.png b/tests/ref/meta/ref.png new file mode 100644 index 0000000000000000000000000000000000000000..13e4db334dafb5709cd79c53cdd4cbb1eec6eccb GIT binary patch literal 11693 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 Ai~s-t literal 0 HcmV?d00001 diff --git a/tests/typ/compiler/set.typ b/tests/typ/compiler/set.typ index 36a42745..a4482e6d 100644 --- a/tests/typ/compiler/set.typ +++ b/tests/typ/compiler/set.typ @@ -51,8 +51,8 @@ Hello *#x* --- // Test conditional set. #show ref: it => { - set text(red) if it.target == "unknown" - it + set text(red) if it.label == + "@" + str(it.label) } @hello from the @unknown diff --git a/tests/typ/meta/ref.typ b/tests/typ/meta/ref.typ new file mode 100644 index 00000000..85750712 --- /dev/null +++ b/tests/typ/meta/ref.typ @@ -0,0 +1,21 @@ +// Test references. + +--- +#set heading(numbering: "1.") + += Introduction +See @setup. + +== Setup +As seen in @intro, we proceed. + +--- +// Error: 1-5 label does not exist in the document +@foo + +--- += First += Second + +// Error: 1-5 label occurs multiple times in the document +@foo