typst_syntax/
highlight.rs

1use crate::{ast, LinkedNode, SyntaxKind, SyntaxNode};
2
3/// A syntax highlighting tag.
4#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
5pub enum Tag {
6    /// A line or block comment.
7    Comment,
8    /// Punctuation in code.
9    Punctuation,
10    /// An escape sequence or shorthand.
11    Escape,
12    /// Strong markup.
13    Strong,
14    /// Emphasized markup.
15    Emph,
16    /// A hyperlink.
17    Link,
18    /// Raw text.
19    Raw,
20    /// A label.
21    Label,
22    /// A reference to a label.
23    Ref,
24    /// A section heading.
25    Heading,
26    /// A marker of a list, enumeration, or term list.
27    ListMarker,
28    /// A term in a term list.
29    ListTerm,
30    /// The delimiters of an equation.
31    MathDelimiter,
32    /// An operator with special meaning in an equation.
33    MathOperator,
34    /// A keyword.
35    Keyword,
36    /// An operator in code.
37    Operator,
38    /// A numeric literal.
39    Number,
40    /// A string literal.
41    String,
42    /// A function or method name.
43    Function,
44    /// An interpolated variable in markup or math.
45    Interpolated,
46    /// A syntax error.
47    Error,
48}
49
50impl Tag {
51    /// The list of all tags, in the same order as thy are defined.
52    ///
53    /// Can be used as the counter-part to `tag as usize`.
54    pub const LIST: &'static [Tag] = &[
55        Self::Comment,
56        Self::Punctuation,
57        Self::Escape,
58        Self::Strong,
59        Self::Emph,
60        Self::Link,
61        Self::Raw,
62        Self::Label,
63        Self::Ref,
64        Self::Heading,
65        Self::ListMarker,
66        Self::ListTerm,
67        Self::MathDelimiter,
68        Self::MathOperator,
69        Self::Keyword,
70        Self::Operator,
71        Self::Number,
72        Self::String,
73        Self::Function,
74        Self::Interpolated,
75        Self::Error,
76    ];
77
78    /// Return the recommended TextMate grammar scope for the given highlighting
79    /// tag.
80    pub fn tm_scope(&self) -> &'static str {
81        match self {
82            Self::Comment => "comment.typst",
83            Self::Punctuation => "punctuation.typst",
84            Self::Escape => "constant.character.escape.typst",
85            Self::Strong => "markup.bold.typst",
86            Self::Emph => "markup.italic.typst",
87            Self::Link => "markup.underline.link.typst",
88            Self::Raw => "markup.raw.typst",
89            Self::MathDelimiter => "punctuation.definition.math.typst",
90            Self::MathOperator => "keyword.operator.math.typst",
91            Self::Heading => "markup.heading.typst",
92            Self::ListMarker => "punctuation.definition.list.typst",
93            Self::ListTerm => "markup.list.term.typst",
94            Self::Label => "entity.name.label.typst",
95            Self::Ref => "markup.other.reference.typst",
96            Self::Keyword => "keyword.typst",
97            Self::Operator => "keyword.operator.typst",
98            Self::Number => "constant.numeric.typst",
99            Self::String => "string.quoted.double.typst",
100            Self::Function => "entity.name.function.typst",
101            Self::Interpolated => "meta.interpolation.typst",
102            Self::Error => "invalid.typst",
103        }
104    }
105
106    /// The recommended CSS class for the highlighting tag.
107    pub fn css_class(self) -> &'static str {
108        match self {
109            Self::Comment => "typ-comment",
110            Self::Punctuation => "typ-punct",
111            Self::Escape => "typ-escape",
112            Self::Strong => "typ-strong",
113            Self::Emph => "typ-emph",
114            Self::Link => "typ-link",
115            Self::Raw => "typ-raw",
116            Self::Label => "typ-label",
117            Self::Ref => "typ-ref",
118            Self::Heading => "typ-heading",
119            Self::ListMarker => "typ-marker",
120            Self::ListTerm => "typ-term",
121            Self::MathDelimiter => "typ-math-delim",
122            Self::MathOperator => "typ-math-op",
123            Self::Keyword => "typ-key",
124            Self::Operator => "typ-op",
125            Self::Number => "typ-num",
126            Self::String => "typ-str",
127            Self::Function => "typ-func",
128            Self::Interpolated => "typ-pol",
129            Self::Error => "typ-error",
130        }
131    }
132}
133
134/// Determine the highlight tag of a linked syntax node.
135///
136/// Returns `None` if the node should not be highlighted.
137pub fn highlight(node: &LinkedNode) -> Option<Tag> {
138    match node.kind() {
139        SyntaxKind::Markup
140            if node.parent_kind() == Some(SyntaxKind::TermItem)
141                && node.next_sibling_kind() == Some(SyntaxKind::Colon) =>
142        {
143            Some(Tag::ListTerm)
144        }
145        SyntaxKind::Markup => None,
146        SyntaxKind::Text => None,
147        SyntaxKind::Space => None,
148        SyntaxKind::Linebreak => Some(Tag::Escape),
149        SyntaxKind::Parbreak => None,
150        SyntaxKind::Escape => Some(Tag::Escape),
151        SyntaxKind::Shorthand => Some(Tag::Escape),
152        SyntaxKind::SmartQuote => None,
153        SyntaxKind::Strong => Some(Tag::Strong),
154        SyntaxKind::Emph => Some(Tag::Emph),
155        SyntaxKind::Raw => Some(Tag::Raw),
156        SyntaxKind::RawLang => None,
157        SyntaxKind::RawTrimmed => None,
158        SyntaxKind::RawDelim => None,
159        SyntaxKind::Link => Some(Tag::Link),
160        SyntaxKind::Label => Some(Tag::Label),
161        SyntaxKind::Ref => Some(Tag::Ref),
162        SyntaxKind::RefMarker => None,
163        SyntaxKind::Heading => Some(Tag::Heading),
164        SyntaxKind::HeadingMarker => None,
165        SyntaxKind::ListItem => None,
166        SyntaxKind::ListMarker => Some(Tag::ListMarker),
167        SyntaxKind::EnumItem => None,
168        SyntaxKind::EnumMarker => Some(Tag::ListMarker),
169        SyntaxKind::TermItem => None,
170        SyntaxKind::TermMarker => Some(Tag::ListMarker),
171        SyntaxKind::Equation => None,
172
173        SyntaxKind::Math => None,
174        SyntaxKind::MathText => None,
175        SyntaxKind::MathIdent => highlight_ident(node),
176        SyntaxKind::MathShorthand => Some(Tag::Escape),
177        SyntaxKind::MathAlignPoint => Some(Tag::MathOperator),
178        SyntaxKind::MathDelimited => None,
179        SyntaxKind::MathAttach => None,
180        SyntaxKind::MathFrac => None,
181        SyntaxKind::MathRoot => None,
182        SyntaxKind::MathPrimes => None,
183
184        SyntaxKind::Hash => highlight_hash(node),
185        SyntaxKind::LeftBrace => Some(Tag::Punctuation),
186        SyntaxKind::RightBrace => Some(Tag::Punctuation),
187        SyntaxKind::LeftBracket => Some(Tag::Punctuation),
188        SyntaxKind::RightBracket => Some(Tag::Punctuation),
189        SyntaxKind::LeftParen => Some(Tag::Punctuation),
190        SyntaxKind::RightParen => Some(Tag::Punctuation),
191        SyntaxKind::Comma => Some(Tag::Punctuation),
192        SyntaxKind::Semicolon => Some(Tag::Punctuation),
193        SyntaxKind::Colon => Some(Tag::Punctuation),
194        SyntaxKind::Star => match node.parent_kind() {
195            Some(SyntaxKind::Strong) => None,
196            _ => Some(Tag::Operator),
197        },
198        SyntaxKind::Underscore => match node.parent_kind() {
199            Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator),
200            _ => None,
201        },
202        SyntaxKind::Dollar => Some(Tag::MathDelimiter),
203        SyntaxKind::Plus => Some(Tag::Operator),
204        SyntaxKind::Minus => Some(Tag::Operator),
205        SyntaxKind::Slash => Some(match node.parent_kind() {
206            Some(SyntaxKind::MathFrac) => Tag::MathOperator,
207            _ => Tag::Operator,
208        }),
209        SyntaxKind::Hat => Some(Tag::MathOperator),
210        SyntaxKind::Prime => Some(Tag::MathOperator),
211        SyntaxKind::Dot => Some(Tag::Punctuation),
212        SyntaxKind::Eq => match node.parent_kind() {
213            Some(SyntaxKind::Heading) => None,
214            _ => Some(Tag::Operator),
215        },
216        SyntaxKind::EqEq => Some(Tag::Operator),
217        SyntaxKind::ExclEq => Some(Tag::Operator),
218        SyntaxKind::Lt => Some(Tag::Operator),
219        SyntaxKind::LtEq => Some(Tag::Operator),
220        SyntaxKind::Gt => Some(Tag::Operator),
221        SyntaxKind::GtEq => Some(Tag::Operator),
222        SyntaxKind::PlusEq => Some(Tag::Operator),
223        SyntaxKind::HyphEq => Some(Tag::Operator),
224        SyntaxKind::StarEq => Some(Tag::Operator),
225        SyntaxKind::SlashEq => Some(Tag::Operator),
226        SyntaxKind::Dots => Some(Tag::Operator),
227        SyntaxKind::Arrow => Some(Tag::Operator),
228        SyntaxKind::Root => Some(Tag::MathOperator),
229
230        SyntaxKind::Not => Some(Tag::Keyword),
231        SyntaxKind::And => Some(Tag::Keyword),
232        SyntaxKind::Or => Some(Tag::Keyword),
233        SyntaxKind::None => Some(Tag::Keyword),
234        SyntaxKind::Auto => Some(Tag::Keyword),
235        SyntaxKind::Let => Some(Tag::Keyword),
236        SyntaxKind::Set => Some(Tag::Keyword),
237        SyntaxKind::Show => Some(Tag::Keyword),
238        SyntaxKind::Context => Some(Tag::Keyword),
239        SyntaxKind::If => Some(Tag::Keyword),
240        SyntaxKind::Else => Some(Tag::Keyword),
241        SyntaxKind::For => Some(Tag::Keyword),
242        SyntaxKind::In => Some(Tag::Keyword),
243        SyntaxKind::While => Some(Tag::Keyword),
244        SyntaxKind::Break => Some(Tag::Keyword),
245        SyntaxKind::Continue => Some(Tag::Keyword),
246        SyntaxKind::Return => Some(Tag::Keyword),
247        SyntaxKind::Import => Some(Tag::Keyword),
248        SyntaxKind::Include => Some(Tag::Keyword),
249        SyntaxKind::As => Some(Tag::Keyword),
250
251        SyntaxKind::Code => None,
252        SyntaxKind::Ident => highlight_ident(node),
253        SyntaxKind::Bool => Some(Tag::Keyword),
254        SyntaxKind::Int => Some(Tag::Number),
255        SyntaxKind::Float => Some(Tag::Number),
256        SyntaxKind::Numeric => Some(Tag::Number),
257        SyntaxKind::Str => Some(Tag::String),
258        SyntaxKind::CodeBlock => None,
259        SyntaxKind::ContentBlock => None,
260        SyntaxKind::Parenthesized => None,
261        SyntaxKind::Array => None,
262        SyntaxKind::Dict => None,
263        SyntaxKind::Named => None,
264        SyntaxKind::Keyed => None,
265        SyntaxKind::Unary => None,
266        SyntaxKind::Binary => None,
267        SyntaxKind::FieldAccess => None,
268        SyntaxKind::FuncCall => None,
269        SyntaxKind::Args => None,
270        SyntaxKind::Spread => None,
271        SyntaxKind::Closure => None,
272        SyntaxKind::Params => None,
273        SyntaxKind::LetBinding => None,
274        SyntaxKind::SetRule => None,
275        SyntaxKind::ShowRule => None,
276        SyntaxKind::Contextual => None,
277        SyntaxKind::Conditional => None,
278        SyntaxKind::WhileLoop => None,
279        SyntaxKind::ForLoop => None,
280        SyntaxKind::ModuleImport => None,
281        SyntaxKind::ImportItems => None,
282        SyntaxKind::ImportItemPath => None,
283        SyntaxKind::RenamedImportItem => None,
284        SyntaxKind::ModuleInclude => None,
285        SyntaxKind::LoopBreak => None,
286        SyntaxKind::LoopContinue => None,
287        SyntaxKind::FuncReturn => None,
288        SyntaxKind::Destructuring => None,
289        SyntaxKind::DestructAssignment => None,
290
291        SyntaxKind::Shebang => Some(Tag::Comment),
292        SyntaxKind::LineComment => Some(Tag::Comment),
293        SyntaxKind::BlockComment => Some(Tag::Comment),
294        SyntaxKind::Error => Some(Tag::Error),
295        SyntaxKind::End => None,
296    }
297}
298
299/// Highlight an identifier based on context.
300fn highlight_ident(node: &LinkedNode) -> Option<Tag> {
301    // Are we directly before an argument list?
302    let next_leaf = node.next_leaf();
303    if let Some(next) = &next_leaf {
304        if node.range().end == next.offset()
305            && ((next.kind() == SyntaxKind::LeftParen
306                && matches!(
307                    next.parent_kind(),
308                    Some(SyntaxKind::Args | SyntaxKind::Params)
309                ))
310                || (next.kind() == SyntaxKind::LeftBracket
311                    && next.parent_kind() == Some(SyntaxKind::ContentBlock)))
312        {
313            return Some(Tag::Function);
314        }
315    }
316
317    // Are we in math?
318    if node.kind() == SyntaxKind::MathIdent {
319        return Some(Tag::Interpolated);
320    }
321
322    // Find the first non-field access ancestor.
323    let mut ancestor = node;
324    while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
325        ancestor = ancestor.parent()?;
326    }
327
328    // Are we directly before or behind a show rule colon?
329    if ancestor.parent_kind() == Some(SyntaxKind::ShowRule)
330        && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)
331            || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon))
332    {
333        return Some(Tag::Function);
334    }
335
336    // Are we (or an ancestor field access) directly after a hash.
337    if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hash) {
338        return Some(Tag::Interpolated);
339    }
340
341    // Are we behind a dot, that is behind another identifier?
342    let prev = node.prev_leaf()?;
343    if prev.kind() == SyntaxKind::Dot {
344        let prev_prev = prev.prev_leaf()?;
345        if is_ident(&prev_prev) {
346            return highlight_ident(&prev_prev);
347        }
348    }
349
350    None
351}
352
353/// Highlight a hash based on context.
354fn highlight_hash(node: &LinkedNode) -> Option<Tag> {
355    let next = node.next_sibling()?;
356    let expr = next.cast::<ast::Expr>()?;
357    if !expr.hash() {
358        return None;
359    }
360    highlight(&next.leftmost_leaf()?)
361}
362
363/// Whether the node is one of the two identifier nodes.
364fn is_ident(node: &LinkedNode) -> bool {
365    matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
366}
367
368/// Highlight a node to an HTML `code` element.
369///
370/// This uses these [CSS classes for categories](Tag::css_class).
371pub fn highlight_html(root: &SyntaxNode) -> String {
372    let mut buf = String::from("<code>");
373    let node = LinkedNode::new(root);
374    highlight_html_impl(&mut buf, &node);
375    buf.push_str("</code>");
376    buf
377}
378
379/// Highlight one source node, emitting HTML.
380fn highlight_html_impl(html: &mut String, node: &LinkedNode) {
381    let mut span = false;
382    if let Some(tag) = highlight(node) {
383        if tag != Tag::Error {
384            span = true;
385            html.push_str("<span class=\"");
386            html.push_str(tag.css_class());
387            html.push_str("\">");
388        }
389    }
390
391    let text = node.text();
392    if !text.is_empty() {
393        for c in text.chars() {
394            match c {
395                '<' => html.push_str("&lt;"),
396                '>' => html.push_str("&gt;"),
397                '&' => html.push_str("&amp;"),
398                '\'' => html.push_str("&#39;"),
399                '"' => html.push_str("&quot;"),
400                _ => html.push(c),
401            }
402        }
403    } else {
404        for child in node.children() {
405            highlight_html_impl(html, &child);
406        }
407    }
408
409    if span {
410        html.push_str("</span>");
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use std::ops::Range;
417
418    use super::*;
419
420    #[test]
421    fn test_highlighting() {
422        use Tag::*;
423
424        #[track_caller]
425        fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
426            let mut vec = vec![];
427            let root = crate::parse(text);
428            highlight_tree(&mut vec, &LinkedNode::new(&root));
429            assert_eq!(vec, goal);
430        }
431
432        fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) {
433            if let Some(tag) = highlight(node) {
434                tags.push((node.range(), tag));
435            }
436
437            for child in node.children() {
438                highlight_tree(tags, &child);
439            }
440        }
441
442        test("= *AB*", &[(0..6, Heading), (2..6, Strong)]);
443
444        test(
445            "#f(x + 1)",
446            &[
447                (0..1, Function),
448                (1..2, Function),
449                (2..3, Punctuation),
450                (5..6, Operator),
451                (7..8, Number),
452                (8..9, Punctuation),
453            ],
454        );
455
456        test(
457            "#let f(x) = x",
458            &[
459                (0..1, Keyword),
460                (1..4, Keyword),
461                (5..6, Function),
462                (6..7, Punctuation),
463                (8..9, Punctuation),
464                (10..11, Operator),
465            ],
466        );
467    }
468}