From 2a86e4db0bb3894d1cc3b94e1a1af31a6cd87b80 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 14 Mar 2023 22:33:22 +0100 Subject: [PATCH] Reference supplements --- library/src/lib.rs | 18 ++++++++++---- library/src/meta/mod.rs | 8 ++++++ library/src/meta/reference.rs | 46 ++++++++++++++++++++++++----------- macros/src/node.rs | 6 ++--- src/eval/library.rs | 6 ++--- src/eval/mod.rs | 4 ++- src/ide/highlight.rs | 1 + src/syntax/ast.rs | 17 ++++++++++--- src/syntax/kind.rs | 7 ++++-- src/syntax/lexer.rs | 12 ++++----- src/syntax/parser.rs | 13 ++++++++-- 11 files changed, 98 insertions(+), 40 deletions(-) diff --git a/library/src/lib.rs b/library/src/lib.rs index 19539ccf4..5b114d9b3 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -11,7 +11,7 @@ pub mod text; pub mod visualize; use typst::eval::{LangItems, Library, Module, Scope}; -use typst::geom::{Align, Color, Dir, GenAlign}; +use typst::geom::{Align, Color, Dir, GenAlign, Smart}; use typst::model::{Node, NodeId, StyleMap}; use self::layout::LayoutRoot; @@ -185,13 +185,21 @@ fn items() -> LangItems { }, raw_languages: text::RawNode::languages, link: |url| meta::LinkNode::from_url(url).pack(), - ref_: |target| meta::RefNode::new(target).pack(), + reference: |target, supplement| { + let mut node = meta::RefNode::new(target); + if let Some(supplement) = supplement { + node.push_supplement(Smart::Custom(Some(meta::Supplement::Content( + supplement, + )))); + } + node.pack() + }, heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), list_item: |body| layout::ListItem::new(body).pack(), enum_item: |number, body| { let mut node = layout::EnumItem::new(body); if let Some(number) = number { - node = node.with_number(Some(number)); + node.push_number(Some(number)); } node.pack() }, @@ -202,10 +210,10 @@ fn items() -> LangItems { math_attach: |base, bottom, top| { let mut node = math::AttachNode::new(base); if let Some(bottom) = bottom { - node = node.with_bottom(Some(bottom)); + node.push_bottom(Some(bottom)); } if let Some(top) = top { - node = node.with_top(Some(top)); + node.push_top(Some(top)); } node.pack() }, diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index f5b9bf2f0..3cde2b8e0 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -15,3 +15,11 @@ pub use self::link::*; pub use self::numbering::*; pub use self::outline::*; pub use self::reference::*; + +use typst::doc::Lang; + +/// The named with which an element is referenced. +pub trait LocalName { + /// Get the name in the given language. + fn local_name(&self, lang: Lang) -> &'static str; +} diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 18f0aa3fb..f63c7e4c7 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,4 +1,4 @@ -use super::{FigureNode, HeadingNode, Numbering}; +use super::{FigureNode, HeadingNode, LocalName, Numbering}; use crate::prelude::*; use crate::text::TextNode; @@ -41,11 +41,14 @@ pub struct RefNode { #[required] pub target: Label, - /// The prefix before the referenced number. + /// A supplement for the reference. + /// + /// For references to headings or figures, this is added before the + /// referenced number. For citations, this can be used to add a page number. /// /// ```example /// #set heading(numbering: "1.") - /// #set ref(prefix: it => { + /// #set ref(supplement: it => { /// if it.func() == heading { /// "Chapter" /// } else { @@ -57,11 +60,11 @@ pub struct RefNode { /// 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, + pub supplement: Smart>, } impl Synthesize for RefNode { @@ -90,34 +93,36 @@ impl Show for RefNode { } }; - let mut prefix = match self.prefix(styles) { + let supplement = self.supplement(styles); + let mut supplement = match supplement { Smart::Auto => target .with::() .map(|node| node.local_name(TextNode::lang_in(styles))) .map(TextNode::packed) .unwrap_or_default(), Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(func)) => { + 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() } }; - if !prefix.is_empty() { - prefix += TextNode::packed('\u{a0}'); + if !supplement.is_empty() { + supplement += 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)? + numbered(vt, supplement, &numbering, &numbers)? } else { bail!(self.span(), "cannot reference unnumbered heading"); } } else if let Some(figure) = target.to::() { if let Some(numbering) = figure.numbering(StyleChain::default()) { let number = figure.number().unwrap(); - numbered(vt, prefix, &numbering, &[number])? + numbered(vt, supplement, &numbering, &[number])? } else { bail!(self.span(), "cannot reference unnumbered figure"); } @@ -146,8 +151,21 @@ fn numbered( }) } -/// The named with which an element is referenced. -pub trait LocalName { - /// Get the name in the given language. - fn local_name(&self, lang: Lang) -> &'static str; +/// Additional content for a reference. +pub enum Supplement { + Content(Content), + Func(Func), +} + +cast_from_value! { + Supplement, + v: Content => Self::Content(v), + v: Func => Self::Func(v), +} + +cast_to_value! { + v: Supplement => match v { + Supplement::Content(v) => v.into(), + Supplement::Func(v) => v.into(), + } } diff --git a/macros/src/node.rs b/macros/src/node.rs index 148560743..dfbd9078b 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -433,19 +433,19 @@ fn create_construct_impl(node: &Node) -> TokenStream { && (!field.internal || field.parse.is_some()) }) .map(|field| { - let with_ident = &field.with_ident; + let push_ident = &field.push_ident; let (prefix, value) = create_field_parser(field); if field.settable() { quote! { #prefix if let Some(value) = #value { - node = node.#with_ident(value); + node.#push_ident(value); } } } else { quote! { #prefix - node = node.#with_ident(#value); + node.#push_ident(#value); } } }); diff --git a/src/eval/library.rs b/src/eval/library.rs index 14f02d98c..d3f7547d6 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -59,8 +59,8 @@ pub struct LangItems { pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>, /// A hyperlink: `https://typst.org`. pub link: fn(url: EcoString) -> Content, - /// A reference: `@target`. - pub ref_: fn(target: Label) -> Content, + /// A reference: `@target`, `@target[..]`. + pub reference: fn(target: Label, supplement: Option) -> Content, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, /// An item in a bullet list: `- ...`. @@ -106,7 +106,7 @@ impl Hash for LangItems { self.emph.hash(state); self.raw.hash(state); self.link.hash(state); - self.ref_.hash(state); + self.reference.hash(state); self.heading.hash(state); self.list_item.hash(state); self.enum_item.hash(state); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index ae5f668a7..127c930fb 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -567,7 +567,9 @@ impl Eval for ast::Ref { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.ref_)(Label(self.get().into()))) + let label = Label(self.target().into()); + let supplement = self.supplement().map(|block| block.eval(vm)).transpose()?; + Ok((vm.items.reference)(label, supplement)) } } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index 7827b2c9b..6214328b5 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -129,6 +129,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Link => Some(Tag::Link), SyntaxKind::Label => Some(Tag::Label), SyntaxKind::Ref => Some(Tag::Ref), + SyntaxKind::RefMarker => None, SyntaxKind::Heading => Some(Tag::Heading), SyntaxKind::HeadingMarker => None, SyntaxKind::ListItem => None, diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 2fdedbcf6..8e48358da 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -101,7 +101,7 @@ pub enum Expr { Link(Link), /// A label: ``. Label(Label), - /// A reference: `@target`. + /// A reference: `@target`, `@target[..]`. Ref(Ref), /// A section heading: `= Introduction`. Heading(Heading), @@ -604,14 +604,23 @@ impl Label { } node! { - /// A reference: `@target`. + /// A reference: `@target`, `@target[..]`. Ref } impl Ref { /// Get the target. - pub fn get(&self) -> &str { - self.0.text().trim_start_matches('@') + pub fn target(&self) -> &str { + self.0 + .children() + .find(|node| node.kind() == SyntaxKind::RefMarker) + .map(|node| node.text().trim_start_matches('@')) + .unwrap_or_default() + } + + /// Get the supplement. + pub fn supplement(&self) -> Option { + self.0.cast_last_match() } } diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index 47b5da317..ce3ae7440 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -36,8 +36,10 @@ pub enum SyntaxKind { Link, /// A label: ``. Label, - /// A reference: `@target`. + /// A reference: `@target`, `@target[..]`. Ref, + /// Introduces a reference: `@target`. + RefMarker, /// A section heading: `= Introduction`. Heading, /// Introduces a section heading: `=`, `==`, ... @@ -324,12 +326,14 @@ impl SyntaxKind { Self::Parbreak => "paragraph break", Self::Escape => "escape sequence", Self::Shorthand => "shorthand", + Self::SmartQuote => "smart quote", Self::Strong => "strong content", Self::Emph => "emphasized content", Self::Raw => "raw block", Self::Link => "link", Self::Label => "label", Self::Ref => "reference", + Self::RefMarker => "reference marker", Self::Heading => "heading", Self::HeadingMarker => "heading marker", Self::ListItem => "list item", @@ -358,7 +362,6 @@ impl SyntaxKind { Self::Star => "star", Self::Underscore => "underscore", Self::Dollar => "dollar sign", - Self::SmartQuote => "smart quote", Self::Plus => "plus", Self::Minus => "minus", Self::Slash => "slash", diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs index 919cce69b..8e27d98d9 100644 --- a/src/syntax/lexer.rs +++ b/src/syntax/lexer.rs @@ -172,7 +172,7 @@ impl Lexer<'_> { 'h' if self.s.eat_if("ttps://") => self.link(), '0'..='9' => self.numbering(start), '<' if self.s.at(is_id_continue) => self.label(), - '@' if self.s.at(is_id_continue) => self.reference(), + '@' => self.ref_marker(), '.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '-' if self.s.eat_if("--") => SyntaxKind::Shorthand, @@ -297,6 +297,11 @@ impl Lexer<'_> { self.text() } + fn ref_marker(&mut self) -> SyntaxKind { + self.s.eat_while(is_id_continue); + SyntaxKind::RefMarker + } + fn label(&mut self) -> SyntaxKind { let label = self.s.eat_while(is_id_continue); if label.is_empty() { @@ -310,11 +315,6 @@ impl Lexer<'_> { SyntaxKind::Label } - fn reference(&mut self) -> SyntaxKind { - self.s.eat_while(is_id_continue); - SyntaxKind::Ref - } - fn text(&mut self) -> SyntaxKind { macro_rules! table { ($(|$c:literal)*) => { diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 201d78fa7..127a89d53 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -104,8 +104,7 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) { | SyntaxKind::SmartQuote | SyntaxKind::Raw | SyntaxKind::Link - | SyntaxKind::Label - | SyntaxKind::Ref => p.eat(), + | SyntaxKind::Label => p.eat(), SyntaxKind::Hashtag => embedded_code_expr(p), SyntaxKind::Star => strong(p), @@ -114,6 +113,7 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) { SyntaxKind::ListMarker if *at_start => list_item(p), SyntaxKind::EnumMarker if *at_start => enum_item(p), SyntaxKind::TermMarker if *at_start => term_item(p), + SyntaxKind::RefMarker => reference(p), SyntaxKind::Dollar => formula(p), SyntaxKind::LeftBracket @@ -198,6 +198,15 @@ fn term_item(p: &mut Parser) { p.wrap(m, SyntaxKind::TermItem); } +fn reference(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::RefMarker); + if p.directly_at(SyntaxKind::LeftBracket) { + content_block(p); + } + p.wrap(m, SyntaxKind::Ref); +} + fn whitespace_line(p: &mut Parser) { while !p.newline() && p.current().is_trivia() { p.eat();