1use crate::parser::{Document, Node, NodeKind, Position, Span};
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct Highlight {
7 pub span: Span,
8 pub tag: HighlightTag,
9}
10
11#[derive(Debug, Clone, PartialEq)]
12pub enum HighlightTag {
13 Heading1,
14 Heading2,
15 Heading3,
16 Heading4,
17 Heading5,
18 Heading6,
19 Emphasis,
20 Strong,
21 Strikethrough,
22 Mark,
23 Superscript,
24 Subscript,
25 Link,
26 Image,
27 CodeSpan,
28 CodeBlock,
29 InlineHtml,
30 HardBreak,
31 SoftBreak,
32 ThematicBreak,
33 Blockquote,
34 Admonition,
35 HtmlBlock,
36 List,
37 ListItem,
38 TaskCheckboxChecked,
39 TaskCheckboxUnchecked,
40 Table,
41 TableRow,
42 TableRowHeader,
43 TableCell,
44 TableCellHeader,
45 LinkReference,
46 DefinitionList,
47 DefinitionTerm,
48 DefinitionDescription,
49 TabBlockContainer,
50 TabBlockHeader,
51 SliderDeckMarker,
52 SliderSeparatorHorizontal,
53 SliderSeparatorVertical,
54}
55
56pub fn compute_highlights(document: &Document) -> Vec<Highlight> {
57 let mut highlights = Vec::new();
58
59 for node in &document.children {
60 collect_highlights(node, &mut highlights);
61 }
62
63 finalize_highlights(highlights)
64}
65
66pub fn compute_highlights_with_source(document: &Document, source: &str) -> Vec<Highlight> {
67 let mut highlights = compute_highlights(document);
68 highlights.extend(compute_tab_block_marker_highlights(source));
69 highlights.extend(compute_slider_marker_highlights(source));
70 finalize_highlights(highlights)
71}
72
73fn finalize_highlights(mut highlights: Vec<Highlight>) -> Vec<Highlight> {
74 highlights.retain(|h| is_non_empty_span(&h.span));
75 highlights.sort_by(|a, b| {
76 let start_cmp =
77 (a.span.start.line, a.span.start.column).cmp(&(b.span.start.line, b.span.start.column));
78 if start_cmp != std::cmp::Ordering::Equal {
79 return start_cmp;
80 }
81
82 let end_cmp =
83 (b.span.end.line, b.span.end.column).cmp(&(a.span.end.line, a.span.end.column));
84 if end_cmp != std::cmp::Ordering::Equal {
85 return end_cmp;
86 }
87
88 tag_rank(&a.tag).cmp(&tag_rank(&b.tag))
89 });
90 highlights.dedup_by(|a, b| a.tag == b.tag && a.span == b.span);
91
92 highlights
93}
94
95fn is_non_empty_span(span: &Span) -> bool {
96 let start = (span.start.line, span.start.column);
97 let end = (span.end.line, span.end.column);
98 start < end
99}
100
101fn tag_rank(tag: &HighlightTag) -> u8 {
102 match tag {
103 HighlightTag::Heading1 => 1,
104 HighlightTag::Heading2 => 2,
105 HighlightTag::Heading3 => 3,
106 HighlightTag::Heading4 => 4,
107 HighlightTag::Heading5 => 5,
108 HighlightTag::Heading6 => 6,
109 HighlightTag::Emphasis => 10,
110 HighlightTag::Strong => 11,
111 HighlightTag::Strikethrough => 12,
112 HighlightTag::Mark => 13,
113 HighlightTag::Superscript => 14,
114 HighlightTag::Subscript => 15,
115 HighlightTag::Link => 16,
116 HighlightTag::Image => 17,
117 HighlightTag::CodeSpan => 20,
118 HighlightTag::CodeBlock => 21,
119 HighlightTag::InlineHtml => 30,
120 HighlightTag::HardBreak => 40,
121 HighlightTag::SoftBreak => 41,
122 HighlightTag::ThematicBreak => 42,
123 HighlightTag::Blockquote => 50,
124 HighlightTag::Admonition => 51,
125 HighlightTag::HtmlBlock => 52,
126 HighlightTag::List => 60,
127 HighlightTag::ListItem => 61,
128 HighlightTag::TaskCheckboxUnchecked => 62,
129 HighlightTag::TaskCheckboxChecked => 63,
130 HighlightTag::Table => 70,
131 HighlightTag::TableRowHeader => 71,
132 HighlightTag::TableRow => 72,
133 HighlightTag::TableCellHeader => 73,
134 HighlightTag::TableCell => 74,
135 HighlightTag::LinkReference => 80,
136 HighlightTag::DefinitionList => 90,
137 HighlightTag::DefinitionTerm => 91,
138 HighlightTag::DefinitionDescription => 92,
139 HighlightTag::TabBlockContainer => 100,
140 HighlightTag::TabBlockHeader => 101,
141 HighlightTag::SliderDeckMarker => 110,
142 HighlightTag::SliderSeparatorHorizontal => 111,
143 HighlightTag::SliderSeparatorVertical => 112,
144 }
145}
146
147fn compute_tab_block_marker_highlights(source: &str) -> Vec<Highlight> {
148 fn trim_upto_3_spaces(s: &str) -> (&str, usize) {
149 let bytes = s.as_bytes();
150 let mut i = 0usize;
151 for _ in 0..3 {
152 if bytes.get(i) == Some(&b' ') {
153 i += 1;
154 } else {
155 break;
156 }
157 }
158 (&s[i..], i)
159 }
160
161 fn fence_prefix(rest: &str) -> Option<(char, usize, &str)> {
162 let mut chars = rest.chars();
163 let ch = chars.next()?;
164 if ch != '`' && ch != '~' {
165 return None;
166 }
167 let mut count = 1usize;
168 for c in chars.clone() {
169 if c == ch {
170 count += 1;
171 } else {
172 break;
173 }
174 }
175 if count >= 3 {
176 Some((ch, count, &rest[count..]))
177 } else {
178 None
179 }
180 }
181
182 let mut highlights: Vec<Highlight> = Vec::new();
183 let mut in_tab_block = false;
184 let mut in_fence: Option<(char, usize)> = None;
185 let mut line_start_offset: usize = 0;
186 let mut line_no: usize = 1;
187
188 for seg in source.split_inclusive('\n') {
189 let seg_len = seg.len();
190
191 let line = seg
192 .strip_suffix('\n')
193 .unwrap_or(seg)
194 .strip_suffix('\r')
195 .unwrap_or(seg.strip_suffix('\n').unwrap_or(seg));
196
197 let (rest, _indent_len) = trim_upto_3_spaces(line);
198
199 if let Some((fch, fcount, after_fence)) = fence_prefix(rest) {
200 match in_fence {
201 None => in_fence = Some((fch, fcount)),
202 Some((open_ch, open_count)) => {
203 if fch == open_ch && fcount >= open_count && after_fence.trim().is_empty() {
204 in_fence = None;
205 }
206 }
207 }
208 }
209
210 if in_fence.is_none() {
211 if !in_tab_block {
212 if let Some(after) = rest.strip_prefix(":::tab") {
213 if after.is_empty()
214 || after
215 .chars()
216 .next()
217 .is_some_and(|ch| ch == ' ' || ch == '\t')
218 {
219 highlights.push(line_highlight(
220 line_no,
221 line_start_offset,
222 line.len(),
223 HighlightTag::TabBlockContainer,
224 ));
225 in_tab_block = true;
226 }
227 }
228 } else {
229 if let Some(after) = rest.strip_prefix("@tab") {
230 let after = after.strip_prefix(' ').or_else(|| after.strip_prefix('\t'));
231 if let Some(after_ws) = after {
232 if !after_ws.trim().is_empty() {
233 highlights.push(line_highlight(
234 line_no,
235 line_start_offset,
236 line.len(),
237 HighlightTag::TabBlockHeader,
238 ));
239 }
240 }
241 }
242
243 if let Some(after) = rest.strip_prefix(":::") {
244 if after.trim().is_empty() {
245 highlights.push(line_highlight(
246 line_no,
247 line_start_offset,
248 line.len(),
249 HighlightTag::TabBlockContainer,
250 ));
251 in_tab_block = false;
252 }
253 }
254 }
255 }
256
257 line_start_offset = line_start_offset.saturating_add(seg_len);
258 line_no = line_no.saturating_add(1);
259 }
260
261 highlights
262}
263
264fn compute_slider_marker_highlights(source: &str) -> Vec<Highlight> {
265 fn trim_upto_3_spaces(s: &str) -> (&str, usize) {
266 let bytes = s.as_bytes();
267 let mut i = 0usize;
268 for _ in 0..3 {
269 if bytes.get(i) == Some(&b' ') {
270 i += 1;
271 } else {
272 break;
273 }
274 }
275 (&s[i..], i)
276 }
277
278 fn fence_prefix(rest: &str) -> Option<(char, usize, &str)> {
279 let mut chars = rest.chars();
280 let ch = chars.next()?;
281 if ch != '`' && ch != '~' {
282 return None;
283 }
284 let mut count = 1usize;
285 for c in chars.clone() {
286 if c == ch {
287 count += 1;
288 } else {
289 break;
290 }
291 }
292 if count >= 3 {
293 Some((ch, count, &rest[count..]))
294 } else {
295 None
296 }
297 }
298
299 let mut highlights: Vec<Highlight> = Vec::new();
300 let mut in_slider_deck = false;
301 let mut in_fence: Option<(char, usize)> = None;
302 let mut line_start_offset: usize = 0;
303 let mut line_no: usize = 1;
304
305 for seg in source.split_inclusive('\n') {
306 let seg_len = seg.len();
307
308 let line = seg
309 .strip_suffix('\n')
310 .unwrap_or(seg)
311 .strip_suffix('\r')
312 .unwrap_or(seg.strip_suffix('\n').unwrap_or(seg));
313
314 let (rest, _indent_len) = trim_upto_3_spaces(line);
315
316 if let Some((fch, fcount, after_fence)) = fence_prefix(rest) {
317 match in_fence {
318 None => in_fence = Some((fch, fcount)),
319 Some((open_ch, open_count)) => {
320 if fch == open_ch && fcount >= open_count && after_fence.trim().is_empty() {
321 in_fence = None;
322 }
323 }
324 }
325 }
326
327 if in_fence.is_none() {
328 if !in_slider_deck {
329 if let Some(after) = rest.strip_prefix("@slidestart") {
330 let ok = after.is_empty()
331 || after
332 .chars()
333 .next()
334 .is_some_and(|ch| ch == ' ' || ch == '\t' || ch == ':');
335 if ok {
336 highlights.push(line_highlight(
337 line_no,
338 line_start_offset,
339 line.len(),
340 HighlightTag::SliderDeckMarker,
341 ));
342 in_slider_deck = true;
343 }
344 }
345 } else {
346 if let Some(after) = rest.strip_prefix("@slideend") {
347 if after.is_empty()
348 || after
349 .chars()
350 .next()
351 .is_some_and(|ch| ch == ' ' || ch == '\t')
352 {
353 highlights.push(line_highlight(
354 line_no,
355 line_start_offset,
356 line.len(),
357 HighlightTag::SliderDeckMarker,
358 ));
359 in_slider_deck = false;
360 }
361 }
362
363 if rest.trim() == "---" {
364 highlights.push(line_highlight(
365 line_no,
366 line_start_offset,
367 line.len(),
368 HighlightTag::SliderSeparatorHorizontal,
369 ));
370 } else if rest.trim() == "--" {
371 highlights.push(line_highlight(
372 line_no,
373 line_start_offset,
374 line.len(),
375 HighlightTag::SliderSeparatorVertical,
376 ));
377 }
378 }
379 }
380
381 line_start_offset = line_start_offset.saturating_add(seg_len);
382 line_no = line_no.saturating_add(1);
383 }
384
385 highlights
386}
387
388fn line_highlight(
389 line: usize,
390 line_start_offset: usize,
391 line_len_bytes: usize,
392 tag: HighlightTag,
393) -> Highlight {
394 let start = Position::new(line, 1, line_start_offset);
395 let end = Position::new(line, line_len_bytes + 1, line_start_offset + line_len_bytes);
396 Highlight {
397 span: Span::new(start, end),
398 tag,
399 }
400}
401
402fn collect_highlights(node: &Node, highlights: &mut Vec<Highlight>) {
403 if let Some(span) = &node.span {
404 match &node.kind {
405 NodeKind::Heading { level, .. } => {
406 let tag = match level {
407 1 => HighlightTag::Heading1,
408 2 => HighlightTag::Heading2,
409 3 => HighlightTag::Heading3,
410 4 => HighlightTag::Heading4,
411 5 => HighlightTag::Heading5,
412 6 => HighlightTag::Heading6,
413 _ => HighlightTag::Heading1,
414 };
415
416 let full_line_span = Span::new(
417 Position::new(span.start.line, 1, span.start_line_offset()),
418 span.end,
419 );
420
421 highlights.push(Highlight {
422 span: full_line_span,
423 tag,
424 });
425 }
426 NodeKind::Emphasis => highlights.push(Highlight {
427 span: *span,
428 tag: HighlightTag::Emphasis,
429 }),
430 NodeKind::Strong | NodeKind::StrongEmphasis => highlights.push(Highlight {
431 span: *span,
432 tag: HighlightTag::Strong,
433 }),
434 NodeKind::Strikethrough => highlights.push(Highlight {
435 span: *span,
436 tag: HighlightTag::Strikethrough,
437 }),
438 NodeKind::Mark => highlights.push(Highlight {
439 span: *span,
440 tag: HighlightTag::Mark,
441 }),
442 NodeKind::Superscript => highlights.push(Highlight {
443 span: *span,
444 tag: HighlightTag::Superscript,
445 }),
446 NodeKind::Subscript => highlights.push(Highlight {
447 span: *span,
448 tag: HighlightTag::Subscript,
449 }),
450 NodeKind::Link { .. }
451 | NodeKind::PlatformMention { .. }
452 | NodeKind::FootnoteReference { .. } => highlights.push(Highlight {
453 span: *span,
454 tag: HighlightTag::Link,
455 }),
456 NodeKind::Image { .. } => highlights.push(Highlight {
457 span: *span,
458 tag: HighlightTag::Image,
459 }),
460 NodeKind::CodeSpan(_) | NodeKind::InlineMath { .. } => highlights.push(Highlight {
461 span: *span,
462 tag: HighlightTag::CodeSpan,
463 }),
464 NodeKind::CodeBlock { .. }
465 | NodeKind::DisplayMath { .. }
466 | NodeKind::MermaidDiagram { .. } => highlights.push(Highlight {
467 span: *span,
468 tag: HighlightTag::CodeBlock,
469 }),
470 NodeKind::InlineHtml(_) => highlights.push(Highlight {
471 span: *span,
472 tag: HighlightTag::InlineHtml,
473 }),
474 NodeKind::ThematicBreak => highlights.push(Highlight {
475 span: *span,
476 tag: HighlightTag::ThematicBreak,
477 }),
478 NodeKind::HtmlBlock { .. } => highlights.push(Highlight {
479 span: *span,
480 tag: HighlightTag::HtmlBlock,
481 }),
482 NodeKind::Blockquote => highlights.push(Highlight {
483 span: *span,
484 tag: HighlightTag::Blockquote,
485 }),
486 NodeKind::Admonition { .. } => highlights.push(Highlight {
487 span: *span,
488 tag: HighlightTag::Admonition,
489 }),
490 NodeKind::List { .. } => highlights.push(Highlight {
491 span: *span,
492 tag: HighlightTag::List,
493 }),
494 NodeKind::ListItem => highlights.push(Highlight {
495 span: *span,
496 tag: HighlightTag::ListItem,
497 }),
498 NodeKind::TaskCheckbox { checked } | NodeKind::TaskCheckboxInline { checked } => {
499 highlights.push(Highlight {
500 span: *span,
501 tag: if *checked {
502 HighlightTag::TaskCheckboxChecked
503 } else {
504 HighlightTag::TaskCheckboxUnchecked
505 },
506 })
507 }
508 NodeKind::Table { .. } => highlights.push(Highlight {
509 span: *span,
510 tag: HighlightTag::Table,
511 }),
512 NodeKind::TableRow { header } => highlights.push(Highlight {
513 span: *span,
514 tag: if *header {
515 HighlightTag::TableRowHeader
516 } else {
517 HighlightTag::TableRow
518 },
519 }),
520 NodeKind::TableCell { header, .. } => highlights.push(Highlight {
521 span: *span,
522 tag: if *header {
523 HighlightTag::TableCellHeader
524 } else {
525 HighlightTag::TableCell
526 },
527 }),
528 NodeKind::LinkReference { .. } => highlights.push(Highlight {
529 span: *span,
530 tag: HighlightTag::LinkReference,
531 }),
532 NodeKind::DefinitionList => highlights.push(Highlight {
533 span: *span,
534 tag: HighlightTag::DefinitionList,
535 }),
536 NodeKind::DefinitionTerm => highlights.push(Highlight {
537 span: *span,
538 tag: HighlightTag::DefinitionTerm,
539 }),
540 NodeKind::DefinitionDescription => highlights.push(Highlight {
541 span: *span,
542 tag: HighlightTag::DefinitionDescription,
543 }),
544 NodeKind::Paragraph
545 | NodeKind::Text(_)
546 | NodeKind::HardBreak
547 | NodeKind::SoftBreak
548 | NodeKind::TabGroup
549 | NodeKind::TabItem { .. }
550 | NodeKind::SliderDeck { .. }
551 | NodeKind::Slide { .. }
552 | NodeKind::FootnoteDefinition { .. } => {}
553 }
554 }
555
556 for child in &node.children {
557 collect_highlights(child, highlights);
558 }
559}