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