diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 94d9ef405..7f182f48e 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -203,6 +203,8 @@ impl Eval for MarkupNode { Self::Heading(heading) => heading.eval(vm)?, Self::List(list) => list.eval(vm)?, Self::Enum(enum_) => enum_.eval(vm)?, + Self::Label(_) => Content::Empty, + Self::Ref(label) => Content::show(library::structure::RefNode(label.clone())), Self::Expr(expr) => expr.eval(vm)?.display(), }) } diff --git a/src/library/mod.rs b/src/library/mod.rs index bd34590a4..c1a645fb3 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -37,6 +37,7 @@ pub fn new() -> Scope { std.def_fn("smallcaps", text::smallcaps); // Structure. + std.def_node::("ref"); std.def_node::("heading"); std.def_node::("list"); std.def_node::("enum"); diff --git a/src/library/structure/mod.rs b/src/library/structure/mod.rs index a597211e7..088d1e6c1 100644 --- a/src/library/structure/mod.rs +++ b/src/library/structure/mod.rs @@ -3,9 +3,11 @@ mod doc; mod heading; mod list; +mod reference; mod table; pub use doc::*; pub use heading::*; pub use list::*; +pub use reference::*; pub use table::*; diff --git a/src/library/structure/reference.rs b/src/library/structure/reference.rs new file mode 100644 index 000000000..0eeb4bf57 --- /dev/null +++ b/src/library/structure/reference.rs @@ -0,0 +1,28 @@ +use crate::library::prelude::*; + +/// A reference to a label. +#[derive(Debug, Hash)] +pub struct RefNode(pub EcoString); + +#[node(showable)] +impl RefNode { + fn construct(_: &mut Machine, args: &mut Args) -> TypResult { + Ok(Content::show(Self(args.expect("label")?))) + } +} + +impl Show for RefNode { + fn unguard(&self, _: Selector) -> ShowNode { + Self(self.0.clone()).pack() + } + + fn encode(&self, _: StyleChain) -> Dict { + dict! { + "label" => Value::Str(self.0.clone().into()), + } + } + + fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult { + Ok(Content::Text(format_eco!("@{}", self.0))) + } +} diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index e64636f81..a09d791d5 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -169,6 +169,9 @@ pub static THEME: Lazy = Lazy::new(|| Theme { item("markup.raw", Some("#818181"), None), item("markup.list", Some("#8b41b1"), None), item("comment", Some("#8a8a8a"), None), + item("punctuation.shortcut", Some("#1d6c76"), None), + item("constant.character.escape", Some("#1d6c76"), None), + item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), item("keyword, constant.language, variable.language", Some("#d73a49"), None), item("storage.type, storage.modifier", Some("#d73a49"), None), item("entity.other", Some("#8b41b1"), None), @@ -177,8 +180,6 @@ pub static THEME: Lazy = Lazy::new(|| Theme { item("meta.annotation", Some("#301414"), None), item("constant", Some("#b60157"), None), item("string", Some("#298e0d"), None), - item("punctuation.shortcut", Some("#1d6c76"), None), - item("constant.character.escape", Some("#1d6c76"), None), item("invalid", Some("#ff0000"), None), ], }); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index a997421e4..0737d4caf 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -175,18 +175,24 @@ fn markup_indented(p: &mut Parser, min_indent: usize) { _ => false, }); + let marker = p.marker(); let mut at_start = false; - p.perform(NodeKind::Markup { min_indent }, |p| { - while !p.eof() { - if let Some(NodeKind::Space { newlines: (1 ..) }) = p.peek() { - if p.column(p.current_end()) < min_indent { - break; - } - } - markup_node(p, &mut at_start); + while !p.eof() { + match p.peek() { + Some(NodeKind::Space { newlines: (1 ..) }) + if p.column(p.current_end()) < min_indent => + { + break; + } + Some(NodeKind::Label(_)) => break, + _ => {} } - }); + + markup_node(p, &mut at_start); + } + + marker.end(p, NodeKind::Markup { min_indent }); } /// Parse a markup node. @@ -212,16 +218,18 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { // Text and markup. NodeKind::Text(_) + | NodeKind::Linebreak { .. } | NodeKind::NonBreakingSpace | NodeKind::Shy | NodeKind::EnDash | NodeKind::EmDash | NodeKind::Ellipsis | NodeKind::Quote { .. } - | NodeKind::Linebreak { .. } + | NodeKind::Escape(_) | NodeKind::Raw(_) | NodeKind::Math(_) - | NodeKind::Escape(_) => { + | NodeKind::Label(_) + | NodeKind::Ref(_) => { p.eat(); } diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index be107f3cb..e004dd37c 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -148,8 +148,10 @@ impl<'s> Tokens<'s> { '*' if !self.in_word() => NodeKind::Star, '_' if !self.in_word() => NodeKind::Underscore, '`' => self.raw(), - '$' => self.math(), '=' => NodeKind::Eq, + '$' => self.math(), + '<' => self.label(), + '@' => self.reference(), c if c == '.' || c.is_ascii_digit() => self.numbering(start, c), // Plain text. @@ -277,7 +279,9 @@ impl<'s> Tokens<'s> { // Parenthesis and hashtag. '[' | ']' | '{' | '}' | '#' | // Markup. - '~' | '\'' | '"' | '*' | '_' | '`' | '$' | '=' | '-' | '.' => { + '~' | '-' | '.' | ':' | + '\'' | '"' | '*' | '_' | '`' | '$' | '=' | + '<' | '>' | '@' => { self.s.expect(c); NodeKind::Escape(c) } @@ -453,6 +457,29 @@ impl<'s> Tokens<'s> { } } + fn label(&mut self) -> NodeKind { + let label = self.s.eat_while(is_id_continue); + if self.s.eat_if('>') { + if !label.is_empty() { + NodeKind::Label(label.into()) + } else { + NodeKind::Error(SpanPos::Full, "label cannot be empty".into()) + } + } else { + self.terminated = false; + NodeKind::Error(SpanPos::End, "expected closing angle bracket".into()) + } + } + + fn reference(&mut self) -> NodeKind { + let label = self.s.eat_while(is_id_continue); + if !label.is_empty() { + NodeKind::Ref(label.into()) + } else { + NodeKind::Error(SpanPos::Full, "label cannot be empty".into()) + } + } + fn ident(&mut self, start: usize) -> NodeKind { self.s.eat_while(is_id_continue); match self.s.from(start) { diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 67f9e0385..10bee4e82 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -84,6 +84,8 @@ impl Markup { NodeKind::Heading => node.cast().map(MarkupNode::Heading), NodeKind::List => node.cast().map(MarkupNode::List), NodeKind::Enum => node.cast().map(MarkupNode::Enum), + NodeKind::Label(v) => Some(MarkupNode::Label(v.clone())), + NodeKind::Ref(v) => Some(MarkupNode::Ref(v.clone())), _ => node.cast().map(MarkupNode::Expr), }) } @@ -116,6 +118,10 @@ pub enum MarkupNode { List(ListNode), /// An item in an enumeration (ordered list): `1. ...`. Enum(EnumNode), + /// A label. + Label(EcoString), + /// A reference. + Ref(EcoString), /// An expression. Expr(Expr), } diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index b52234d4e..ff02190a5 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -153,6 +153,10 @@ pub enum Category { Punctuation, /// A line or block comment. Comment, + /// An easily typable shortcut to a unicode codepoint. + Shortcut, + /// An escape sequence. + Escape, /// Strong text. Strong, /// Emphasized text. @@ -165,10 +169,10 @@ pub enum Category { Heading, /// A list or enumeration. List, - /// An easily typable shortcut to a unicode codepoint. - Shortcut, - /// An escape sequence. - Escape, + /// A label. + Label, + /// A reference. + Ref, /// A keyword. Keyword, /// An operator symbol. @@ -212,6 +216,13 @@ impl Category { NodeKind::Dot => Some(Category::Punctuation), NodeKind::LineComment => Some(Category::Comment), NodeKind::BlockComment => Some(Category::Comment), + NodeKind::Linebreak { .. } => Some(Category::Shortcut), + NodeKind::NonBreakingSpace => Some(Category::Shortcut), + NodeKind::Shy => Some(Category::Shortcut), + NodeKind::EnDash => Some(Category::Shortcut), + NodeKind::EmDash => Some(Category::Shortcut), + NodeKind::Ellipsis => Some(Category::Shortcut), + NodeKind::Escape(_) => Some(Category::Escape), NodeKind::Strong => Some(Category::Strong), NodeKind::Emph => Some(Category::Emph), NodeKind::Raw(_) => Some(Category::Raw), @@ -222,13 +233,8 @@ impl Category { _ => Some(Category::Operator), }, NodeKind::EnumNumbering(_) => Some(Category::List), - NodeKind::Linebreak { .. } => Some(Category::Shortcut), - NodeKind::NonBreakingSpace => Some(Category::Shortcut), - NodeKind::Shy => Some(Category::Shortcut), - NodeKind::EnDash => Some(Category::Shortcut), - NodeKind::EmDash => Some(Category::Shortcut), - NodeKind::Ellipsis => Some(Category::Shortcut), - NodeKind::Escape(_) => Some(Category::Escape), + NodeKind::Label(_) => Some(Category::Label), + NodeKind::Ref(_) => Some(Category::Ref), NodeKind::Not => Some(Category::Keyword), NodeKind::And => Some(Category::Keyword), NodeKind::Or => Some(Category::Keyword), @@ -344,14 +350,16 @@ impl Category { Self::Bracket => "punctuation.definition.typst", Self::Punctuation => "punctuation.typst", Self::Comment => "comment.typst", + Self::Shortcut => "punctuation.shortcut.typst", + Self::Escape => "constant.character.escape.content.typst", Self::Strong => "markup.bold.typst", Self::Emph => "markup.italic.typst", Self::Raw => "markup.raw.typst", Self::Math => "string.other.math.typst", Self::Heading => "markup.heading.typst", Self::List => "markup.list.typst", - Self::Shortcut => "punctuation.shortcut.typst", - Self::Escape => "constant.character.escape.content.typst", + Self::Label => "entity.name.label.typst", + Self::Ref => "markup.other.reference.typst", Self::Keyword => "keyword.typst", Self::Operator => "keyword.operator.typst", Self::None => "constant.language.none.typst", diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 4bae7a4b9..eb070a046 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -723,6 +723,10 @@ pub enum NodeKind { /// /// Can also exist without the number: `.`. EnumNumbering(Option), + /// A label: `