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 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 && node.range().end == next.offset()
305 && ((next.kind() == SyntaxKind::LeftParen
306 && matches!(next.parent_kind(), Some(SyntaxKind::Args | SyntaxKind::Params)))
307 || (next.kind() == SyntaxKind::LeftBracket
308 && next.parent_kind() == Some(SyntaxKind::ContentBlock)))
309 {
310 return Some(Tag::Function);
311 }
312
313 if node.kind() == SyntaxKind::MathIdent {
315 return Some(Tag::Interpolated);
316 }
317
318 let mut ancestor = node;
320 while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
321 ancestor = ancestor.parent()?;
322 }
323
324 if ancestor.parent_kind() == Some(SyntaxKind::ShowRule)
326 && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)
327 || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon))
328 {
329 return Some(Tag::Function);
330 }
331
332 if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hash) {
334 return Some(Tag::Interpolated);
335 }
336
337 let prev = node.prev_leaf()?;
339 if prev.kind() == SyntaxKind::Dot {
340 let prev_prev = prev.prev_leaf()?;
341 if is_ident(&prev_prev) {
342 return highlight_ident(&prev_prev);
343 }
344 }
345
346 None
347}
348
349fn highlight_hash(node: &LinkedNode) -> Option<Tag> {
351 let next = node.next_sibling()?;
352 let expr = next.cast::<ast::Expr>()?;
353 if !expr.hash() {
354 return None;
355 }
356 highlight(&next.leftmost_leaf()?)
357}
358
359fn is_ident(node: &LinkedNode) -> bool {
361 matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
362}
363
364pub fn highlight_html(root: &SyntaxNode) -> String {
368 let mut buf = String::from("<code>");
369 let node = LinkedNode::new(root);
370 highlight_html_impl(&mut buf, &node);
371 buf.push_str("</code>");
372 buf
373}
374
375fn highlight_html_impl(html: &mut String, node: &LinkedNode) {
377 let mut span = false;
378 if let Some(tag) = highlight(node)
379 && tag != Tag::Error
380 {
381 span = true;
382 html.push_str("<span class=\"");
383 html.push_str(tag.css_class());
384 html.push_str("\">");
385 }
386
387 let text = node.text();
388 if !text.is_empty() {
389 for c in text.chars() {
390 match c {
391 '<' => html.push_str("<"),
392 '>' => html.push_str(">"),
393 '&' => html.push_str("&"),
394 '\'' => html.push_str("'"),
395 '"' => html.push_str("""),
396 _ => html.push(c),
397 }
398 }
399 } else {
400 for child in node.children() {
401 highlight_html_impl(html, &child);
402 }
403 }
404
405 if span {
406 html.push_str("</span>");
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use std::ops::Range;
413
414 use super::*;
415
416 #[test]
417 fn test_highlighting() {
418 use Tag::*;
419
420 #[track_caller]
421 fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
422 let mut vec = vec![];
423 let root = crate::parse(text);
424 highlight_tree(&mut vec, &LinkedNode::new(&root));
425 assert_eq!(vec, goal);
426 }
427
428 fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) {
429 if let Some(tag) = highlight(node) {
430 tags.push((node.range(), tag));
431 }
432
433 for child in node.children() {
434 highlight_tree(tags, &child);
435 }
436 }
437
438 test("= *AB*", &[(0..6, Heading), (2..6, Strong)]);
439
440 test(
441 "#f(x + 1)",
442 &[
443 (0..1, Function),
444 (1..2, Function),
445 (2..3, Punctuation),
446 (5..6, Operator),
447 (7..8, Number),
448 (8..9, Punctuation),
449 ],
450 );
451
452 test(
453 "#let f(x) = x",
454 &[
455 (0..1, Keyword),
456 (1..4, Keyword),
457 (5..6, Function),
458 (6..7, Punctuation),
459 (8..9, Punctuation),
460 (10..11, Operator),
461 ],
462 );
463 }
464}