//! Syntax highlighting for Typst source code. use std::fmt::Write; use std::ops::Range; use syntect::highlighting::{Color, FontStyle, Highlighter, Style, Theme}; use syntect::parsing::Scope; use super::{parse, SyntaxKind, SyntaxNode}; /// Highlight source text into a standalone HTML document. pub fn highlight_html(text: &str, theme: &Theme) -> String { let mut buf = String::new(); buf.push_str("\n"); buf.push_str("\n"); buf.push_str("\n"); buf.push_str(" \n"); buf.push_str("\n"); buf.push_str("\n"); buf.push_str(&highlight_pre(text, theme)); buf.push_str("\n\n"); buf.push_str("\n"); buf } /// Highlight source text into an HTML pre element. pub fn highlight_pre(text: &str, theme: &Theme) -> String { let mut buf = String::new(); buf.push_str("
\n");

    let root = parse(text);
    highlight_themed(&root, theme, |range, style| {
        let styled = style != Style::default();
        if styled {
            buf.push_str("");
        }

        buf.push_str(&text[range]);

        if styled {
            buf.push_str("");
        }
    });

    buf.push_str("\n
"); buf } /// Highlight a syntax node in a theme by calling `f` with ranges and their /// styles. pub fn highlight_themed(root: &SyntaxNode, theme: &Theme, mut f: F) where F: FnMut(Range, Style), { fn process( mut offset: usize, node: &SyntaxNode, scopes: Vec, highlighter: &Highlighter, f: &mut F, ) where F: FnMut(Range, Style), { if node.children().len() == 0 { let range = offset..offset + node.len(); let style = highlighter.style_for_stack(&scopes); f(range, style); return; } for (i, child) in node.children().enumerate() { let mut scopes = scopes.clone(); if let Some(category) = Category::determine(child, node, i) { scopes.push(Scope::new(category.tm_scope()).unwrap()) } process(offset, child, scopes, highlighter, f); offset += child.len(); } } let highlighter = Highlighter::new(theme); process(0, root, vec![], &highlighter, &mut f); } /// Highlight a syntax node by calling `f` with ranges overlapping `within` and /// their categories. pub fn highlight_categories(root: &SyntaxNode, within: Range, mut f: F) where F: FnMut(Range, Category), { fn process(mut offset: usize, node: &SyntaxNode, range: Range, f: &mut F) where F: FnMut(Range, Category), { for (i, child) in node.children().enumerate() { let span = offset..offset + child.len(); if range.start <= span.end && range.end >= span.start { if let Some(category) = Category::determine(child, node, i) { f(span, category); } process(offset, child, range.clone(), f); } offset += child.len(); } } process(0, root, within, &mut f) } /// The syntax highlighting category of a node. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Category { /// A line or block comment. Comment, /// A square bracket, parenthesis or brace. Bracket, /// Punctuation in code. Punctuation, /// An escape sequence. Escape, /// An easily typable shortcut to a unicode codepoint. Shorthand, /// Symbol notation. Symbol, /// A smart quote. SmartQuote, /// Strong markup. Strong, /// Emphasized markup. Emph, /// A hyperlink. Link, /// Raw text. Raw, /// A label. Label, /// A reference. Ref, /// A section heading. Heading, /// A full item of a list, enumeration or description list. ListItem, /// A marker of a list, enumeration, or description list. ListMarker, /// A term in a description list. ListTerm, /// The delimiters of a math formula. MathDelimiter, /// An operator with special meaning in a math formula. MathOperator, /// A keyword. Keyword, /// A literal defined by a keyword like `none`, `auto` or a boolean. KeywordLiteral, /// An operator symbol. Operator, /// A numeric literal. Number, /// A string literal. String, /// A function or method name. Function, /// An interpolated variable in markup or math. Interpolated, /// A syntax error. Error, } impl Category { /// Determine the highlighting category of a node given its parent and its /// index in its siblings. pub fn determine( child: &SyntaxNode, parent: &SyntaxNode, i: usize, ) -> Option { match child.kind() { SyntaxKind::LineComment => Some(Category::Comment), SyntaxKind::BlockComment => Some(Category::Comment), SyntaxKind::Space { .. } => None, SyntaxKind::LeftBrace => Some(Category::Bracket), SyntaxKind::RightBrace => Some(Category::Bracket), SyntaxKind::LeftBracket => Some(Category::Bracket), SyntaxKind::RightBracket => Some(Category::Bracket), SyntaxKind::LeftParen => Some(Category::Bracket), SyntaxKind::RightParen => Some(Category::Bracket), SyntaxKind::Comma => Some(Category::Punctuation), SyntaxKind::Semicolon => Some(Category::Punctuation), SyntaxKind::Colon => Some(Category::Punctuation), SyntaxKind::Star => match parent.kind() { SyntaxKind::Strong => None, _ => Some(Category::Operator), }, SyntaxKind::Underscore => match parent.kind() { SyntaxKind::Script => Some(Category::MathOperator), _ => None, }, SyntaxKind::Dollar => Some(Category::MathDelimiter), SyntaxKind::Plus => Some(match parent.kind() { SyntaxKind::EnumItem => Category::ListMarker, _ => Category::Operator, }), SyntaxKind::Minus => Some(match parent.kind() { SyntaxKind::ListItem => Category::ListMarker, _ => Category::Operator, }), SyntaxKind::Slash => Some(match parent.kind() { SyntaxKind::DescItem => Category::ListMarker, SyntaxKind::Frac => Category::MathOperator, _ => Category::Operator, }), SyntaxKind::Hat => Some(Category::MathOperator), SyntaxKind::Amp => Some(Category::MathOperator), SyntaxKind::Dot => Some(Category::Punctuation), SyntaxKind::Eq => match parent.kind() { SyntaxKind::Heading => None, _ => Some(Category::Operator), }, SyntaxKind::EqEq => Some(Category::Operator), SyntaxKind::ExclEq => Some(Category::Operator), SyntaxKind::Lt => Some(Category::Operator), SyntaxKind::LtEq => Some(Category::Operator), SyntaxKind::Gt => Some(Category::Operator), SyntaxKind::GtEq => Some(Category::Operator), SyntaxKind::PlusEq => Some(Category::Operator), SyntaxKind::HyphEq => Some(Category::Operator), SyntaxKind::StarEq => Some(Category::Operator), SyntaxKind::SlashEq => Some(Category::Operator), SyntaxKind::Dots => Some(Category::Operator), SyntaxKind::Arrow => Some(Category::Operator), SyntaxKind::Not => Some(Category::Keyword), SyntaxKind::And => Some(Category::Keyword), SyntaxKind::Or => Some(Category::Keyword), SyntaxKind::None => Some(Category::KeywordLiteral), SyntaxKind::Auto => Some(Category::KeywordLiteral), SyntaxKind::Let => Some(Category::Keyword), SyntaxKind::Set => Some(Category::Keyword), SyntaxKind::Show => Some(Category::Keyword), SyntaxKind::If => Some(Category::Keyword), SyntaxKind::Else => Some(Category::Keyword), SyntaxKind::For => Some(Category::Keyword), SyntaxKind::In => Some(Category::Keyword), SyntaxKind::While => Some(Category::Keyword), SyntaxKind::Break => Some(Category::Keyword), SyntaxKind::Continue => Some(Category::Keyword), SyntaxKind::Return => Some(Category::Keyword), SyntaxKind::Import => Some(Category::Keyword), SyntaxKind::Include => Some(Category::Keyword), SyntaxKind::From => Some(Category::Keyword), SyntaxKind::Markup { .. } => match parent.kind() { SyntaxKind::DescItem if parent .children() .take_while(|child| child.kind() != &SyntaxKind::Colon) .find(|c| matches!(c.kind(), SyntaxKind::Markup { .. })) .map_or(false, |ident| std::ptr::eq(ident, child)) => { Some(Category::ListTerm) } _ => None, }, SyntaxKind::Text(_) => None, SyntaxKind::Linebreak => Some(Category::Escape), SyntaxKind::Escape(_) => Some(Category::Escape), SyntaxKind::Shorthand(_) => Some(Category::Shorthand), SyntaxKind::Symbol(_) => Some(Category::Symbol), SyntaxKind::SmartQuote { .. } => Some(Category::SmartQuote), SyntaxKind::Strong => Some(Category::Strong), SyntaxKind::Emph => Some(Category::Emph), SyntaxKind::Raw(_) => Some(Category::Raw), SyntaxKind::Link(_) => Some(Category::Link), SyntaxKind::Label(_) => Some(Category::Label), SyntaxKind::Ref(_) => Some(Category::Ref), SyntaxKind::Heading => Some(Category::Heading), SyntaxKind::ListItem => Some(Category::ListItem), SyntaxKind::EnumItem => Some(Category::ListItem), SyntaxKind::EnumNumbering(_) => Some(Category::ListMarker), SyntaxKind::DescItem => Some(Category::ListItem), SyntaxKind::Math => None, SyntaxKind::Atom(_) => None, SyntaxKind::Script => None, SyntaxKind::Frac => None, SyntaxKind::AlignPoint => None, SyntaxKind::Ident(_) => match parent.kind() { SyntaxKind::Markup { .. } | SyntaxKind::Math | SyntaxKind::Script | SyntaxKind::Frac => Some(Category::Interpolated), SyntaxKind::FuncCall => Some(Category::Function), SyntaxKind::MethodCall if i > 0 => Some(Category::Function), SyntaxKind::Closure if i == 0 => Some(Category::Function), SyntaxKind::SetRule => Some(Category::Function), SyntaxKind::ShowRule if parent .children() .rev() .skip_while(|child| child.kind() != &SyntaxKind::Colon) .find(|c| matches!(c.kind(), SyntaxKind::Ident(_))) .map_or(false, |ident| std::ptr::eq(ident, child)) => { Some(Category::Function) } _ => None, }, SyntaxKind::Bool(_) => Some(Category::KeywordLiteral), SyntaxKind::Int(_) => Some(Category::Number), SyntaxKind::Float(_) => Some(Category::Number), SyntaxKind::Numeric(_, _) => Some(Category::Number), SyntaxKind::Str(_) => Some(Category::String), SyntaxKind::CodeBlock => None, SyntaxKind::ContentBlock => None, SyntaxKind::Parenthesized => None, SyntaxKind::Array => None, SyntaxKind::Dict => None, SyntaxKind::Named => None, SyntaxKind::Keyed => None, SyntaxKind::Unary => None, SyntaxKind::Binary => None, SyntaxKind::FieldAccess => None, SyntaxKind::FuncCall => None, SyntaxKind::MethodCall => None, SyntaxKind::Args => None, SyntaxKind::Spread => None, SyntaxKind::Closure => None, SyntaxKind::Params => None, SyntaxKind::LetBinding => None, SyntaxKind::SetRule => None, SyntaxKind::ShowRule => None, SyntaxKind::Conditional => None, SyntaxKind::WhileLoop => None, SyntaxKind::ForLoop => None, SyntaxKind::ForPattern => None, SyntaxKind::ModuleImport => None, SyntaxKind::ImportItems => None, SyntaxKind::ModuleInclude => None, SyntaxKind::LoopBreak => None, SyntaxKind::LoopContinue => None, SyntaxKind::FuncReturn => None, SyntaxKind::Error(_, _) => Some(Category::Error), } } /// Return the TextMate grammar scope for the given highlighting category. pub fn tm_scope(&self) -> &'static str { match self { Self::Comment => "comment.typst", Self::Bracket => "punctuation.definition.bracket.typst", Self::Punctuation => "punctuation.typst", Self::Escape => "constant.character.escape.typst", Self::Shorthand => "constant.character.shorthand.typst", Self::Symbol => "constant.symbol.typst", Self::SmartQuote => "constant.character.quote.typst", Self::Strong => "markup.bold.typst", Self::Emph => "markup.italic.typst", Self::Link => "markup.underline.link.typst", Self::Raw => "markup.raw.typst", Self::MathDelimiter => "punctuation.definition.math.typst", Self::MathOperator => "keyword.operator.math.typst", Self::Heading => "markup.heading.typst", Self::ListItem => "markup.list.typst", Self::ListMarker => "punctuation.definition.list.typst", Self::ListTerm => "markup.list.term.typst", Self::Label => "entity.name.label.typst", Self::Ref => "markup.other.reference.typst", Self::Keyword => "keyword.typst", Self::Operator => "keyword.operator.typst", Self::KeywordLiteral => "constant.language.typst", Self::Number => "constant.numeric.typst", Self::String => "string.quoted.double.typst", Self::Function => "entity.name.function.typst", Self::Interpolated => "meta.interpolation.typst", Self::Error => "invalid.typst", } } } #[cfg(test)] mod tests { use super::super::Source; use super::*; #[test] fn test_highlighting() { use Category::*; #[track_caller] fn test(text: &str, goal: &[(Range, Category)]) { let mut vec = vec![]; let source = Source::detached(text); let full = 0..text.len(); highlight_categories(source.root(), full, &mut |range, category| { vec.push((range, category)); }); assert_eq!(vec, goal); } test("= *AB*", &[(0..6, Heading), (2..6, Strong)]); test( "#f(x + 1)", &[ (0..2, Function), (2..3, Bracket), (5..6, Operator), (7..8, Number), (8..9, Bracket), ], ); test( "#let f(x) = x", &[ (0..4, Keyword), (5..6, Function), (6..7, Bracket), (8..9, Bracket), (10..11, Operator), ], ); } }