diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index dc373ff5e..eb440b7f0 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 8677aa55a..1bff3af4e 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 4e6e1aedd..d71fb2339 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 d66a573d4..a2b125110 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 bfc317854..a46198bde 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 49afc9ca9..a9b19f589 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 99fb89d29..3f03ba8e3 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 f89ee8df8..678a154d7 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 c37c16fd6..14f02d98c 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 145f961ae..fe56c0607 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 a0d5e9a4b..a7b001ae1 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 17fa786b9..be7373314 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 c4c67a4f1..4685a6052 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 6361e6cee..377c7c76f 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 b4321dbee..201d78fa7 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 000000000..13e4db334 Binary files /dev/null and b/tests/ref/meta/ref.png differ diff --git a/tests/typ/compiler/set.typ b/tests/typ/compiler/set.typ index 36a427452..a4482e6d3 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 000000000..85750712d --- /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