1use crate::{ast, LinkedNode, SyntaxKind, SyntaxNode};
2
3#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
5pub enum Tag {
6 Comment,
8 Punctuation,
10 Escape,
12 Strong,
14 Emph,
16 Link,
18 Raw,
20 Label,
22 Ref,
24 Heading,
26 ListMarker,
28 ListTerm,
30 MathDelimiter,
32 MathOperator,
34 Keyword,
36 Operator,
38 Number,
40 String,
42 Function,
44 Interpolated,
46 Error,
48}
49
50impl Tag {
51 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 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 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
134pub 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
299fn highlight_ident(node: &LinkedNode) -> Option<Tag> {
301 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 if node.kind() == SyntaxKind::MathIdent {
319 return Some(Tag::Interpolated);
320 }
321
322 let mut ancestor = node;
324 while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
325 ancestor = ancestor.parent()?;
326 }
327
328 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 if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hash) {
338 return Some(Tag::Interpolated);
339 }
340
341 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
353fn 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
363fn is_ident(node: &LinkedNode) -> bool {
365 matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
366}
367
368pub 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
379fn 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("<"),
396 '>' => html.push_str(">"),
397 '&' => html.push_str("&"),
398 '\'' => html.push_str("'"),
399 '"' => html.push_str("""),
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}