diff --git a/library/src/prelude.rs b/library/src/prelude.rs index c6bbe676b..02d4ad10f 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -17,7 +17,7 @@ pub use typst::geom::*; pub use typst::model::{ array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast, Content, Dict, Finalize, Fold, Func, Node, NodeId, RecipeId, Resolve, Show, Smart, - Str, StyleChain, StyleMap, StyleVec, Value, Vm, + Str, StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm, }; #[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index c12a61cb0..458083108 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -414,13 +414,15 @@ impl Fold for FontFeatures { #[derive(Debug, Hash)] pub struct SpaceNode; -#[node(Behave)] +#[node(Unlabellable, Behave)] impl SpaceNode { fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { Ok(Self.pack()) } } +impl Unlabellable for SpaceNode {} + impl Behave for SpaceNode { fn behaviour(&self) -> Behaviour { Behaviour::Weak(2) diff --git a/library/src/text/par.rs b/library/src/text/par.rs index 6551ae9c4..37d5e3965 100644 --- a/library/src/text/par.rs +++ b/library/src/text/par.rs @@ -117,13 +117,15 @@ castable! { #[derive(Debug, Hash)] pub struct ParbreakNode; -#[node] +#[node(Unlabellable)] impl ParbreakNode { fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { Ok(Self.pack()) } } +impl Unlabellable for ParbreakNode {} + /// Repeats content to fill a line. #[derive(Debug, Hash)] pub struct RepeatNode(pub Content); diff --git a/src/model/content.rs b/src/model/content.rs index c28082c2f..d72e4e19a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -12,7 +12,7 @@ use typst_macros::node; use super::{Args, Key, Property, Recipe, RecipeId, Style, StyleMap, Value, Vm}; use crate::diag::{SourceResult, StrResult}; use crate::syntax::Span; -use crate::util::ReadableTypeId; +use crate::util::{EcoString, ReadableTypeId}; use crate::World; /// Composable representation of styled content. @@ -21,7 +21,7 @@ use crate::World; /// - anything written between square brackets in Typst /// - any constructor function #[derive(Clone, Hash)] -pub struct Content(Arc, Vec, Option); +pub struct Content(Arc, Vec, Option, Option); impl Content { /// Create empty content. @@ -42,11 +42,6 @@ impl Content { self.downcast::().map_or(false, |seq| seq.0.is_empty()) } - /// The node's span. - pub fn span(&self) -> Option { - self.2 - } - /// The node's human-readable name. pub fn name(&self) -> &'static str { (*self.0).name() @@ -74,6 +69,13 @@ impl Content { /// Access a field on this content. pub fn field(&self, name: &str) -> Option { + if name == "label" { + return Some(match &self.3 { + Some(label) => Value::Str(label.clone().into()), + None => Value::None, + }); + } + self.0.field(name) } @@ -150,21 +152,6 @@ impl Content { StyledNode { sub: self, map: styles }.pack() } - /// Attach a span to the content. - pub fn spanned(mut self, span: Span) -> Self { - if let Some(styled) = self.try_downcast_mut::() { - styled.sub.2 = Some(span); - } else if let Some(styled) = self.downcast::() { - self = StyledNode { - sub: styled.sub.clone().spanned(span), - map: styled.map.clone(), - } - .pack(); - } - self.2 = Some(span); - self - } - /// Disable a show rule recipe. pub fn guard(mut self, id: RecipeId) -> Self { self.1.push(id); @@ -180,6 +167,54 @@ impl Content { pub fn guarded(&self, id: RecipeId) -> bool { self.1.contains(&id) } + + /// The node's span. + pub fn span(&self) -> Option { + self.2 + } + + /// Set the content's span. + pub fn set_span(&mut self, span: Span) { + if let Some(styled) = self.try_downcast_mut::() { + styled.sub.2 = Some(span); + } else if let Some(styled) = self.downcast::() { + *self = StyledNode { + sub: styled.sub.clone().spanned(span), + map: styled.map.clone(), + } + .pack(); + } + self.2 = Some(span); + } + + /// Attach a span to the content. + pub fn spanned(mut self, span: Span) -> Self { + self.set_span(span); + self + } + + /// The content's label. + pub fn label(&self) -> Option<&EcoString> { + self.3.as_ref() + } + + /// Set the content's label. + pub fn set_label(&mut self, label: EcoString) { + self.3 = Some(label); + } + + /// Attacha label to the content. + pub fn labelled(mut self, label: EcoString) -> Self { + self.set_label(label); + self + } + + /// Copy the metadata from other content. + pub fn copy_meta(&mut self, from: &Content) { + self.1 = from.1.clone(); + self.2 = from.2; + self.3 = from.3.clone(); + } } impl Default for Content { @@ -278,7 +313,7 @@ pub trait Node: 'static { where Self: Debug + Hash + Sync + Send + Sized + 'static, { - Content(Arc::new(self), vec![], None) + Content(Arc::new(self), vec![], None, None) } /// A unique identifier of the node type. diff --git a/src/model/eval.rs b/src/model/eval.rs index 8a596afbf..eb6b8ddb7 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -7,7 +7,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, - Recipe, Scope, Scopes, Selector, StyleMap, Transform, Value, Vm, + Recipe, Scope, Scopes, Selector, StyleMap, Transform, Unlabellable, Value, Vm, }; use crate::diag::{bail, error, At, SourceResult, StrResult, Trace, Tracepoint}; use crate::geom::{Abs, Angle, Em, Fr, Ratio}; @@ -118,14 +118,14 @@ fn eval_markup( let mut seq = Vec::with_capacity(nodes.size_hint().1.unwrap_or_default()); while let Some(node) = nodes.next() { - seq.push(match node { + match node { ast::MarkupNode::Expr(ast::Expr::Set(set)) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; } - eval_markup(vm, nodes)?.styled_with_map(styles) + seq.push(eval_markup(vm, nodes)?.styled_with_map(styles)) } ast::MarkupNode::Expr(ast::Expr::Show(show)) => { let recipe = show.eval(vm)?; @@ -134,10 +134,17 @@ fn eval_markup( } let tail = eval_markup(vm, nodes)?; - tail.styled_with_recipe(vm.world, recipe)? + seq.push(tail.styled_with_recipe(vm.world, recipe)?) } - _ => node.eval(vm)?, - }); + ast::MarkupNode::Label(label) => { + if let Some(node) = + seq.iter_mut().rev().find(|node| !node.has::()) + { + node.set_label(label.get().clone()); + } + } + _ => seq.push(node.eval(vm)?), + } if vm.flow.is_some() { break; @@ -174,7 +181,7 @@ impl Eval for ast::MarkupNode { Self::List(v) => v.eval(vm)?, Self::Enum(v) => v.eval(vm)?, Self::Desc(v) => v.eval(vm)?, - Self::Label(v) => v.eval(vm)?, + Self::Label(_) => unimplemented!("handled above"), Self::Ref(v) => v.eval(vm)?, Self::Expr(v) => v.eval(vm)?.display(vm.world), } @@ -249,14 +256,6 @@ impl Eval for ast::Link { } } -impl Eval for ast::Label { - type Output = Content; - - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Content::empty()) - } -} - impl Eval for ast::Ref { type Output = Content; diff --git a/src/model/styles.rs b/src/model/styles.rs index 76c673707..324b52f50 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -350,6 +350,10 @@ pub trait Finalize: 'static + Sync + Send { ) -> SourceResult; } +/// Indicates that a node cannot be labelled. +#[capability] +pub trait Unlabellable: 'static + Sync + Send {} + /// A show rule recipe. #[derive(Clone, PartialEq, Hash)] pub struct Recipe { @@ -392,9 +396,7 @@ impl Recipe { let make = |s| { let mut content = item!(text)(s); - if let Some(span) = target.span() { - content = content.spanned(span); - } + content.copy_meta(target); content }; diff --git a/tests/ref/style/label.png b/tests/ref/style/label.png new file mode 100644 index 000000000..836899e1e Binary files /dev/null and b/tests/ref/style/label.png differ diff --git a/tests/typ/style/label.typ b/tests/typ/style/label.typ new file mode 100644 index 000000000..0b6677835 --- /dev/null +++ b/tests/typ/style/label.typ @@ -0,0 +1,54 @@ +// Test labels. + +--- +// Test labelled headings. +#show heading: text.with(10pt) +#show heading.where(label: "intro"): underline + += Introduction +The beginning. + += Conclusion +The end. + +--- +// Test label after expression. +#show strong.where(label: "v"): text.with(red) + +#let a = [*A*] +#let b = [*B*] +#a #b + +--- +// Test labelled text. +#show "t": it => { + set text(blue) if it.label == "last" + it +} + +This is a thing [that ] happened. + +--- +// Test abusing labels for styling. +#show strong.where(label: "red"): text.with(red) +#show strong.where(label: "blue"): text.with(blue) + +*A* *B* *C* *D* + +--- +// Test that label ignores parbreak. +#show emph.where(label: "hide"): none + +_Hidden_ + + +_Hidden_ + + +_Visible_ + +--- +// Test that label only works within one content block. +#show strong.where(label: "strike"): strike +*This is* [] *protected.* +*This is not.*