1use crate::{LinkedNode, SyntaxKind, SyntaxNode, ast};
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 MathGroupingParens,
36 Keyword,
38 Operator,
40 Number,
42 String,
44 Function,
46 Interpolated,
48 Error,
50}
51
52impl Tag {
53 pub const LIST: &'static [Tag] = &[
57 Self::Comment,
58 Self::Punctuation,
59 Self::Escape,
60 Self::Strong,
61 Self::Emph,
62 Self::Link,
63 Self::Raw,
64 Self::Label,
65 Self::Ref,
66 Self::Heading,
67 Self::ListMarker,
68 Self::ListTerm,
69 Self::MathDelimiter,
70 Self::MathOperator,
71 Self::MathGroupingParens,
72 Self::Keyword,
73 Self::Operator,
74 Self::Number,
75 Self::String,
76 Self::Function,
77 Self::Interpolated,
78 Self::Error,
79 ];
80
81 pub fn tm_scope(&self) -> &'static str {
84 match self {
85 Self::Comment => "comment.typst",
86 Self::Punctuation => "punctuation.typst",
87 Self::Escape => "constant.character.escape.typst",
88 Self::Strong => "markup.bold.typst",
89 Self::Emph => "markup.italic.typst",
90 Self::Link => "markup.underline.link.typst",
91 Self::Raw => "markup.raw.typst",
92 Self::MathDelimiter => "punctuation.definition.math.typst",
93 Self::MathOperator => "keyword.operator.math.typst",
94 Self::MathGroupingParens => "punctuation.math.typst",
95 Self::Heading => "markup.heading.typst",
96 Self::ListMarker => "punctuation.definition.list.typst",
97 Self::ListTerm => "markup.list.term.typst",
98 Self::Label => "entity.name.label.typst",
99 Self::Ref => "markup.other.reference.typst",
100 Self::Keyword => "keyword.typst",
101 Self::Operator => "keyword.operator.typst",
102 Self::Number => "constant.numeric.typst",
103 Self::String => "string.quoted.double.typst",
104 Self::Function => "entity.name.function.typst",
105 Self::Interpolated => "meta.interpolation.typst",
106 Self::Error => "invalid.typst",
107 }
108 }
109
110 pub fn css_class(self) -> &'static str {
112 match self {
113 Self::Comment => "typ-comment",
114 Self::Punctuation => "typ-punct",
115 Self::Escape => "typ-escape",
116 Self::Strong => "typ-strong",
117 Self::Emph => "typ-emph",
118 Self::Link => "typ-link",
119 Self::Raw => "typ-raw",
120 Self::Label => "typ-label",
121 Self::Ref => "typ-ref",
122 Self::Heading => "typ-heading",
123 Self::ListMarker => "typ-marker",
124 Self::ListTerm => "typ-term",
125 Self::MathDelimiter => "typ-math-delim",
126 Self::MathOperator => "typ-math-op",
127 Self::MathGroupingParens => "typ-math-group",
128 Self::Keyword => "typ-key",
129 Self::Operator => "typ-op",
130 Self::Number => "typ-num",
131 Self::String => "typ-str",
132 Self::Function => "typ-func",
133 Self::Interpolated => "typ-pol",
134 Self::Error => "typ-error",
135 }
136 }
137}
138
139pub fn highlight(node: &LinkedNode) -> Option<Tag> {
143 match node.kind() {
144 SyntaxKind::Markup
145 if node.parent_kind() == Some(SyntaxKind::TermItem)
146 && node.next_sibling_kind() == Some(SyntaxKind::Colon) =>
147 {
148 Some(Tag::ListTerm)
149 }
150 SyntaxKind::Markup => None,
151 SyntaxKind::Text => None,
152 SyntaxKind::Space => None,
153 SyntaxKind::Linebreak => Some(Tag::Escape),
154 SyntaxKind::Parbreak => None,
155 SyntaxKind::Escape => Some(Tag::Escape),
156 SyntaxKind::Shorthand => Some(Tag::Escape),
157 SyntaxKind::SmartQuote => None,
158 SyntaxKind::Strong => Some(Tag::Strong),
159 SyntaxKind::Emph => Some(Tag::Emph),
160 SyntaxKind::Raw => Some(Tag::Raw),
161 SyntaxKind::RawLang => None,
162 SyntaxKind::RawTrimmed => None,
163 SyntaxKind::RawDelim => None,
164 SyntaxKind::Link => Some(Tag::Link),
165 SyntaxKind::Label => Some(Tag::Label),
166 SyntaxKind::Ref => Some(Tag::Ref),
167 SyntaxKind::RefMarker => None,
168 SyntaxKind::Heading => Some(Tag::Heading),
169 SyntaxKind::HeadingMarker => None,
170 SyntaxKind::ListItem => None,
171 SyntaxKind::ListMarker => Some(Tag::ListMarker),
172 SyntaxKind::EnumItem => None,
173 SyntaxKind::EnumMarker => Some(Tag::ListMarker),
174 SyntaxKind::TermItem => None,
175 SyntaxKind::TermMarker => Some(Tag::ListMarker),
176 SyntaxKind::Equation => None,
177
178 SyntaxKind::Math => None,
179 SyntaxKind::MathText => None,
180 SyntaxKind::MathIdent => highlight_ident(node),
181 SyntaxKind::MathFieldAccess => None,
182 SyntaxKind::MathShorthand => Some(Tag::Escape),
183 SyntaxKind::MathAlignPoint => Some(Tag::MathOperator),
184 SyntaxKind::MathCall => None,
185 SyntaxKind::MathArgs => None,
186 SyntaxKind::MathDelimited => None,
187 SyntaxKind::MathAttach => None,
188 SyntaxKind::MathFrac => None,
189 SyntaxKind::MathRoot => None,
190 SyntaxKind::MathPrimes => Some(Tag::MathOperator),
191
192 SyntaxKind::Hash => highlight_hash(node),
193 SyntaxKind::LeftBrace => Some(Tag::Punctuation),
194 SyntaxKind::RightBrace => Some(Tag::Punctuation),
195 SyntaxKind::LeftBracket => Some(Tag::Punctuation),
196 SyntaxKind::RightBracket => Some(Tag::Punctuation),
197 SyntaxKind::LeftParen => match node.parent_kind() {
198 Some(SyntaxKind::Math) => Some(Tag::MathGroupingParens),
199 _ => Some(Tag::Punctuation),
200 },
201 SyntaxKind::RightParen => match node.parent_kind() {
202 Some(SyntaxKind::Math) => Some(Tag::MathGroupingParens),
203 _ => Some(Tag::Punctuation),
204 },
205 SyntaxKind::Comma => Some(Tag::Punctuation),
206 SyntaxKind::Semicolon => Some(Tag::Punctuation),
207 SyntaxKind::Colon => Some(Tag::Punctuation),
208 SyntaxKind::Star => match node.parent_kind() {
209 Some(SyntaxKind::Strong) => None,
210 _ => Some(Tag::Operator),
211 },
212 SyntaxKind::Underscore => match node.parent_kind() {
213 Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator),
214 _ => None,
215 },
216 SyntaxKind::Dollar => Some(Tag::MathDelimiter),
217 SyntaxKind::Plus => Some(Tag::Operator),
218 SyntaxKind::Minus => Some(Tag::Operator),
219 SyntaxKind::Slash => Some(match node.parent_kind() {
220 Some(SyntaxKind::MathFrac) => Tag::MathOperator,
221 _ => Tag::Operator,
222 }),
223 SyntaxKind::Hat => Some(Tag::MathOperator),
224 SyntaxKind::Dot => Some(Tag::Punctuation),
225 SyntaxKind::Eq => match node.parent_kind() {
226 Some(SyntaxKind::Heading) => None,
227 _ => Some(Tag::Operator),
228 },
229 SyntaxKind::EqEq => Some(Tag::Operator),
230 SyntaxKind::ExclEq => Some(Tag::Operator),
231 SyntaxKind::Lt => Some(Tag::Operator),
232 SyntaxKind::LtEq => Some(Tag::Operator),
233 SyntaxKind::Gt => Some(Tag::Operator),
234 SyntaxKind::GtEq => Some(Tag::Operator),
235 SyntaxKind::PlusEq => Some(Tag::Operator),
236 SyntaxKind::HyphEq => Some(Tag::Operator),
237 SyntaxKind::StarEq => Some(Tag::Operator),
238 SyntaxKind::SlashEq => Some(Tag::Operator),
239 SyntaxKind::Dots => Some(Tag::Operator),
240 SyntaxKind::Arrow => Some(Tag::Operator),
241 SyntaxKind::Root => Some(Tag::MathOperator),
242 SyntaxKind::Bang => None,
243
244 SyntaxKind::Not => Some(Tag::Keyword),
245 SyntaxKind::And => Some(Tag::Keyword),
246 SyntaxKind::Or => Some(Tag::Keyword),
247 SyntaxKind::None => Some(Tag::Keyword),
248 SyntaxKind::Auto => Some(Tag::Keyword),
249 SyntaxKind::Let => Some(Tag::Keyword),
250 SyntaxKind::Set => Some(Tag::Keyword),
251 SyntaxKind::Show => Some(Tag::Keyword),
252 SyntaxKind::Context => Some(Tag::Keyword),
253 SyntaxKind::If => Some(Tag::Keyword),
254 SyntaxKind::Else => Some(Tag::Keyword),
255 SyntaxKind::For => Some(Tag::Keyword),
256 SyntaxKind::In => Some(Tag::Keyword),
257 SyntaxKind::While => Some(Tag::Keyword),
258 SyntaxKind::Break => Some(Tag::Keyword),
259 SyntaxKind::Continue => Some(Tag::Keyword),
260 SyntaxKind::Return => Some(Tag::Keyword),
261 SyntaxKind::Import => Some(Tag::Keyword),
262 SyntaxKind::Include => Some(Tag::Keyword),
263 SyntaxKind::As => Some(Tag::Keyword),
264
265 SyntaxKind::Code => None,
266 SyntaxKind::Ident => highlight_ident(node),
267 SyntaxKind::Bool => Some(Tag::Keyword),
268 SyntaxKind::Int => Some(Tag::Number),
269 SyntaxKind::Float => Some(Tag::Number),
270 SyntaxKind::Numeric => Some(Tag::Number),
271 SyntaxKind::Str => Some(Tag::String),
272 SyntaxKind::CodeBlock => None,
273 SyntaxKind::ContentBlock => None,
274 SyntaxKind::Parenthesized => None,
275 SyntaxKind::Array => None,
276 SyntaxKind::Dict => None,
277 SyntaxKind::Named => None,
278 SyntaxKind::Keyed => None,
279 SyntaxKind::Unary => None,
280 SyntaxKind::Binary => None,
281 SyntaxKind::FieldAccess => None,
282 SyntaxKind::FuncCall => None,
283 SyntaxKind::Args => None,
284 SyntaxKind::Spread => None,
285 SyntaxKind::Closure => None,
286 SyntaxKind::Params => None,
287 SyntaxKind::LetBinding => None,
288 SyntaxKind::SetRule => None,
289 SyntaxKind::ShowRule => None,
290 SyntaxKind::Contextual => None,
291 SyntaxKind::Conditional => None,
292 SyntaxKind::WhileLoop => None,
293 SyntaxKind::ForLoop => None,
294 SyntaxKind::ModuleImport => None,
295 SyntaxKind::ImportItems => None,
296 SyntaxKind::ImportItemPath => None,
297 SyntaxKind::RenamedImportItem => None,
298 SyntaxKind::ModuleInclude => None,
299 SyntaxKind::LoopBreak => None,
300 SyntaxKind::LoopContinue => None,
301 SyntaxKind::FuncReturn => None,
302 SyntaxKind::Destructuring => None,
303 SyntaxKind::DestructAssignment => None,
304
305 SyntaxKind::Shebang => Some(Tag::Comment),
306 SyntaxKind::LineComment => Some(Tag::Comment),
307 SyntaxKind::BlockComment => Some(Tag::Comment),
308 SyntaxKind::Error => Some(Tag::Error),
309 SyntaxKind::End => None,
310 }
311}
312
313fn highlight_ident(node: &LinkedNode) -> Option<Tag> {
315 let next_leaf = node.next_leaf();
317 if let Some(next) = &next_leaf
318 && node.range().end == next.offset()
319 && ((
320 next.kind() == SyntaxKind::LeftParen
322 && matches!(
323 next.parent_kind(),
324 Some(SyntaxKind::Args | SyntaxKind::MathArgs | SyntaxKind::Params)
325 )
326 ) || (
327 next.kind() == SyntaxKind::LeftBracket
329 && next.parent_kind() == Some(SyntaxKind::ContentBlock)
330 ))
331 {
332 return Some(Tag::Function);
333 }
334
335 if matches!(node.kind(), SyntaxKind::MathIdent | SyntaxKind::MathFieldAccess) {
337 return Some(Tag::Interpolated);
338 }
339
340 let mut ancestor = node;
342 while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
343 ancestor = ancestor.parent()?;
344 }
345
346 if ancestor.parent_kind() == Some(SyntaxKind::ShowRule)
348 && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)
349 || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon))
350 {
351 return Some(Tag::Function);
352 }
353
354 if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hash) {
356 return Some(Tag::Interpolated);
357 }
358
359 let prev = node.prev_leaf()?;
361 if prev.kind() == SyntaxKind::Dot {
362 let prev_prev = prev.prev_leaf()?;
363 if is_ident(&prev_prev) {
364 return highlight_ident(&prev_prev);
365 }
366 }
367
368 None
369}
370
371fn highlight_hash(node: &LinkedNode) -> Option<Tag> {
373 let next = node.next_sibling()?;
374 let expr = next.cast::<ast::Expr>()?;
375 if !expr.hash() {
376 return None;
377 }
378 highlight(&next.leftmost_leaf()?)
379}
380
381fn is_ident(node: &LinkedNode) -> bool {
383 matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
384}
385
386pub fn highlight_html(root: &SyntaxNode) -> String {
390 let mut buf = String::from("<code>");
391 let node = LinkedNode::new(root);
392 highlight_html_impl(&mut buf, &node);
393 buf.push_str("</code>");
394 buf
395}
396
397fn highlight_html_impl(html: &mut String, node: &LinkedNode) {
399 let mut span = false;
400 if let Some(tag) = highlight(node)
401 && tag != Tag::Error
402 {
403 span = true;
404 html.push_str("<span class=\"");
405 html.push_str(tag.css_class());
406 html.push_str("\">");
407 }
408
409 let text = node.leaf_text();
410 if !text.is_empty() {
411 for c in text.chars() {
412 match c {
413 '<' => html.push_str("<"),
414 '>' => html.push_str(">"),
415 '&' => html.push_str("&"),
416 '\'' => html.push_str("'"),
417 '"' => html.push_str("""),
418 _ => html.push(c),
419 }
420 }
421 } else {
422 for child in node.children() {
423 highlight_html_impl(html, &child);
424 }
425 }
426
427 if span {
428 html.push_str("</span>");
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use std::ops::Range;
435
436 use super::*;
437
438 #[test]
439 fn test_highlighting() {
440 use Tag::*;
441
442 #[track_caller]
443 fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
444 let mut vec = vec![];
445 let root = crate::parse(text);
446 highlight_tree(&mut vec, &LinkedNode::new(&root));
447 assert_eq!(vec, goal);
448 }
449
450 fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) {
451 if let Some(tag) = highlight(node) {
452 tags.push((node.range(), tag));
453 }
454
455 for child in node.children() {
456 highlight_tree(tags, &child);
457 }
458 }
459
460 test("= *AB*", &[(0..6, Heading), (2..6, Strong)]);
461
462 test(
463 "#f(x + 1)",
464 &[
465 (0..1, Function),
466 (1..2, Function),
467 (2..3, Punctuation),
468 (5..6, Operator),
469 (7..8, Number),
470 (8..9, Punctuation),
471 ],
472 );
473
474 test(
475 "#let f(x) = x",
476 &[
477 (0..1, Keyword),
478 (1..4, Keyword),
479 (5..6, Function),
480 (6..7, Punctuation),
481 (8..9, Punctuation),
482 (10..11, Operator),
483 ],
484 );
485 }
486}