1use super::render::{char_width, display_width, wrap_text};
2use super::theme::Theme;
3use ratatui::{
4 style::{Modifier, Style},
5 text::{Line, Span},
6};
7
8pub fn markdown_to_lines(md: &str, max_width: usize, theme: &Theme) -> Vec<Line<'static>> {
9 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
10
11 let content_width = max_width.saturating_sub(2);
13
14 let md_owned;
20 let md = if md.contains("**\u{201C}")
21 || md.contains("**\u{2018}")
22 || md.contains("\u{201D}**")
23 || md.contains("\u{2019}**")
24 {
25 md_owned = md
26 .replace("**\u{201C}", "**\u{200B}\u{201C}") .replace("**\u{2018}", "**\u{200B}\u{2018}") .replace("\u{201D}**", "\u{201D}\u{200B}**") .replace("\u{2019}**", "\u{2019}\u{200B}**"); &md_owned as &str
31 } else {
32 md
33 };
34
35 let options = Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES;
36 let parser = Parser::new_ext(md, options);
37
38 let mut lines: Vec<Line<'static>> = Vec::new();
39 let mut current_spans: Vec<Span<'static>> = Vec::new();
40 let mut style_stack: Vec<Style> = vec![Style::default().fg(theme.text_normal)];
41 let mut in_code_block = false;
42 let mut code_block_content = String::new();
43 let mut code_block_lang = String::new();
44 let mut list_depth: usize = 0;
45 let mut ordered_index: Option<u64> = None;
46 let mut heading_level: Option<u8> = None;
47 let mut in_blockquote = false;
49 let mut in_table = false;
51 let mut table_rows: Vec<Vec<String>> = Vec::new(); let mut current_row: Vec<String> = Vec::new();
53 let mut current_cell = String::new();
54 let mut table_alignments: Vec<pulldown_cmark::Alignment> = Vec::new();
55
56 let base_style = Style::default().fg(theme.text_normal);
57
58 let flush_line = |current_spans: &mut Vec<Span<'static>>, lines: &mut Vec<Line<'static>>| {
59 if !current_spans.is_empty() {
60 lines.push(Line::from(current_spans.drain(..).collect::<Vec<_>>()));
61 }
62 };
63
64 for event in parser {
65 match event {
66 Event::Start(Tag::Heading { level, .. }) => {
67 flush_line(&mut current_spans, &mut lines);
68 heading_level = Some(level as u8);
69 if !lines.is_empty() {
70 lines.push(Line::from(""));
71 }
72 let heading_style = match level as u8 {
74 1 => Style::default()
75 .fg(theme.md_h1)
76 .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
77 2 => Style::default()
78 .fg(theme.md_h2)
79 .add_modifier(Modifier::BOLD),
80 3 => Style::default()
81 .fg(theme.md_h3)
82 .add_modifier(Modifier::BOLD),
83 _ => Style::default()
84 .fg(theme.md_h4)
85 .add_modifier(Modifier::BOLD),
86 };
87 style_stack.push(heading_style);
88 }
89 Event::End(TagEnd::Heading(level)) => {
90 flush_line(&mut current_spans, &mut lines);
91 if (level as u8) <= 2 {
93 let sep_char = if (level as u8) == 1 { "━" } else { "─" };
94 lines.push(Line::from(Span::styled(
95 sep_char.repeat(content_width),
96 Style::default().fg(theme.md_heading_sep),
97 )));
98 }
99 style_stack.pop();
100 heading_level = None;
101 }
102 Event::Start(Tag::Strong) => {
103 let current = *style_stack.last().unwrap_or(&base_style);
104 style_stack.push(current.add_modifier(Modifier::BOLD).fg(theme.text_bold));
105 }
106 Event::End(TagEnd::Strong) => {
107 style_stack.pop();
108 }
109 Event::Start(Tag::Emphasis) => {
110 let current = *style_stack.last().unwrap_or(&base_style);
111 style_stack.push(current.add_modifier(Modifier::ITALIC));
112 }
113 Event::End(TagEnd::Emphasis) => {
114 style_stack.pop();
115 }
116 Event::Start(Tag::Strikethrough) => {
117 let current = *style_stack.last().unwrap_or(&base_style);
118 style_stack.push(current.add_modifier(Modifier::CROSSED_OUT));
119 }
120 Event::End(TagEnd::Strikethrough) => {
121 style_stack.pop();
122 }
123 Event::Start(Tag::CodeBlock(kind)) => {
124 flush_line(&mut current_spans, &mut lines);
125 in_code_block = true;
126 code_block_content.clear();
127 code_block_lang = match kind {
128 CodeBlockKind::Fenced(lang) => lang.to_string(),
129 CodeBlockKind::Indented => String::new(),
130 };
131 let label = if code_block_lang.is_empty() {
133 " code ".to_string()
134 } else {
135 format!(" {} ", code_block_lang)
136 };
137 let label_w = display_width(&label);
138 let border_fill = content_width.saturating_sub(2 + label_w);
139 let top_border = format!("┌─{}{}", label, "─".repeat(border_fill));
140 lines.push(Line::from(Span::styled(
141 top_border,
142 Style::default().fg(theme.code_border),
143 )));
144 }
145 Event::End(TagEnd::CodeBlock) => {
146 let code_inner_w = content_width.saturating_sub(4); let code_content_expanded = code_block_content.replace('\t', " ");
150 for code_line in code_content_expanded.lines() {
151 let wrapped = wrap_text(code_line, code_inner_w);
152 for wl in wrapped {
153 let highlighted = highlight_code_line(&wl, &code_block_lang, theme);
154 let text_w: usize =
155 highlighted.iter().map(|s| display_width(&s.content)).sum();
156 let fill = code_inner_w.saturating_sub(text_w);
157 let mut spans_vec = Vec::new();
158 spans_vec.push(Span::styled("│ ", Style::default().fg(theme.code_border)));
159 for hs in highlighted {
160 spans_vec.push(Span::styled(
161 hs.content.to_string(),
162 hs.style.bg(theme.code_bg),
163 ));
164 }
165 spans_vec.push(Span::styled(
166 format!("{} │", " ".repeat(fill)),
167 Style::default().fg(theme.code_border).bg(theme.code_bg),
168 ));
169 lines.push(Line::from(spans_vec));
170 }
171 }
172 let bottom_border = format!("└{}", "─".repeat(content_width.saturating_sub(1)));
173 lines.push(Line::from(Span::styled(
174 bottom_border,
175 Style::default().fg(theme.code_border),
176 )));
177 in_code_block = false;
178 code_block_content.clear();
179 code_block_lang.clear();
180 }
181 Event::Code(text) => {
182 if in_table {
183 current_cell.push('`');
185 current_cell.push_str(&text);
186 current_cell.push('`');
187 } else {
188 let code_str = format!(" {} ", text);
190 let code_w = display_width(&code_str);
191 let effective_prefix_w = if in_blockquote { 2 } else { 0 };
192 let full_line_w = content_width.saturating_sub(effective_prefix_w);
193 let existing_w: usize = current_spans
194 .iter()
195 .map(|s| display_width(&s.content))
196 .sum();
197 if existing_w + code_w > full_line_w && !current_spans.is_empty() {
198 flush_line(&mut current_spans, &mut lines);
199 if in_blockquote {
200 current_spans.push(Span::styled(
201 "| ".to_string(),
202 Style::default().fg(theme.md_blockquote_bar),
203 ));
204 }
205 }
206 current_spans.push(Span::styled(
207 code_str,
208 Style::default()
209 .fg(theme.md_inline_code_fg)
210 .bg(theme.md_inline_code_bg),
211 ));
212 }
213 }
214 Event::Start(Tag::List(start)) => {
215 flush_line(&mut current_spans, &mut lines);
216 list_depth += 1;
217 ordered_index = start;
218 }
219 Event::End(TagEnd::List(_)) => {
220 flush_line(&mut current_spans, &mut lines);
221 list_depth = list_depth.saturating_sub(1);
222 ordered_index = None;
223 }
224 Event::Start(Tag::Item) => {
225 flush_line(&mut current_spans, &mut lines);
226 let indent = " ".repeat(list_depth);
227 let bullet = if let Some(ref mut idx) = ordered_index {
228 let s = format!("{}{}. ", indent, idx);
229 *idx += 1;
230 s
231 } else {
232 format!("{}• ", indent)
233 };
234 current_spans.push(Span::styled(
235 bullet,
236 Style::default().fg(theme.md_list_bullet),
237 ));
238 }
239 Event::End(TagEnd::Item) => {
240 flush_line(&mut current_spans, &mut lines);
241 }
242 Event::Start(Tag::Paragraph) => {
243 if !lines.is_empty() && !in_code_block && heading_level.is_none() {
244 let last_empty = lines.last().map(|l| l.spans.is_empty()).unwrap_or(false);
245 if !last_empty {
246 lines.push(Line::from(""));
247 }
248 }
249 }
250 Event::End(TagEnd::Paragraph) => {
251 flush_line(&mut current_spans, &mut lines);
252 }
253 Event::Start(Tag::BlockQuote(_)) => {
254 flush_line(&mut current_spans, &mut lines);
255 in_blockquote = true;
256 style_stack.push(Style::default().fg(theme.md_blockquote_text));
257 }
258 Event::End(TagEnd::BlockQuote(_)) => {
259 flush_line(&mut current_spans, &mut lines);
260 in_blockquote = false;
261 style_stack.pop();
262 }
263 Event::Text(text) => {
264 if in_code_block {
265 code_block_content.push_str(&text);
266 } else if in_table {
267 current_cell.push_str(&text);
269 } else {
270 let style = *style_stack.last().unwrap_or(&base_style);
271 let text_str = text.to_string().replace('\u{200B}', "");
273
274 if let Some(level) = heading_level {
276 let (prefix, prefix_style) = match level {
277 1 => (
278 ">> ",
279 Style::default()
280 .fg(theme.md_h1)
281 .add_modifier(Modifier::BOLD),
282 ),
283 2 => (
284 ">> ",
285 Style::default()
286 .fg(theme.md_h2)
287 .add_modifier(Modifier::BOLD),
288 ),
289 3 => (
290 "> ",
291 Style::default()
292 .fg(theme.md_h3)
293 .add_modifier(Modifier::BOLD),
294 ),
295 _ => (
296 "> ",
297 Style::default()
298 .fg(theme.md_h4)
299 .add_modifier(Modifier::BOLD),
300 ),
301 };
302 current_spans.push(Span::styled(prefix.to_string(), prefix_style));
303 heading_level = None; }
305
306 let effective_prefix_w = if in_blockquote { 2 } else { 0 }; let full_line_w = content_width.saturating_sub(effective_prefix_w);
309
310 let existing_w: usize = current_spans
312 .iter()
313 .map(|s| display_width(&s.content))
314 .sum();
315
316 let wrap_w = full_line_w.saturating_sub(existing_w);
318
319 let min_useful_w = full_line_w / 4;
322 let wrap_w = if wrap_w < min_useful_w.max(4) && !current_spans.is_empty() {
323 flush_line(&mut current_spans, &mut lines);
324 if in_blockquote {
325 current_spans.push(Span::styled(
326 "| ".to_string(),
327 Style::default().fg(theme.md_blockquote_bar),
328 ));
329 }
330 full_line_w
332 } else {
333 wrap_w
334 };
335
336 for (i, line) in text_str.split('\n').enumerate() {
337 if i > 0 {
338 flush_line(&mut current_spans, &mut lines);
339 if in_blockquote {
340 current_spans.push(Span::styled(
341 "| ".to_string(),
342 Style::default().fg(theme.md_blockquote_bar),
343 ));
344 }
345 }
346 if !line.is_empty() {
347 let effective_wrap = if i == 0 {
349 wrap_w
350 } else {
351 content_width.saturating_sub(effective_prefix_w)
352 };
353 let wrapped = wrap_text(line, effective_wrap);
354 for (j, wl) in wrapped.iter().enumerate() {
355 if j > 0 {
356 flush_line(&mut current_spans, &mut lines);
357 if in_blockquote {
358 current_spans.push(Span::styled(
359 "| ".to_string(),
360 Style::default().fg(theme.md_blockquote_bar),
361 ));
362 }
363 }
364 current_spans.push(Span::styled(wl.clone(), style));
365 }
366 }
367 }
368 }
369 }
370 Event::SoftBreak => {
371 if in_table {
372 current_cell.push(' ');
373 } else {
374 current_spans.push(Span::raw(" "));
375 }
376 }
377 Event::HardBreak => {
378 if in_table {
379 current_cell.push(' ');
380 } else {
381 flush_line(&mut current_spans, &mut lines);
382 }
383 }
384 Event::Rule => {
385 flush_line(&mut current_spans, &mut lines);
386 lines.push(Line::from(Span::styled(
387 "─".repeat(content_width),
388 Style::default().fg(theme.md_rule),
389 )));
390 }
391 Event::Start(Tag::Table(alignments)) => {
393 flush_line(&mut current_spans, &mut lines);
394 in_table = true;
395 table_rows.clear();
396 table_alignments = alignments;
397 }
398 Event::End(TagEnd::Table) => {
399 flush_line(&mut current_spans, &mut lines);
401 in_table = false;
402
403 if !table_rows.is_empty() {
404 let num_cols = table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
405 if num_cols > 0 {
406 let mut col_widths: Vec<usize> = vec![0; num_cols];
408 for row in &table_rows {
409 for (i, cell) in row.iter().enumerate() {
410 let w = display_width(cell);
411 if w > col_widths[i] {
412 col_widths[i] = w;
413 }
414 }
415 }
416
417 let sep_w = num_cols + 1; let pad_w = num_cols * 2; let avail = content_width.saturating_sub(sep_w + pad_w);
421 let max_col_w = avail * 2 / 3;
423 for cw in col_widths.iter_mut() {
424 if *cw > max_col_w {
425 *cw = max_col_w;
426 }
427 }
428 let total_col_w: usize = col_widths.iter().sum();
429 if total_col_w > avail && total_col_w > 0 {
430 let mut remaining = avail;
432 for (i, cw) in col_widths.iter_mut().enumerate() {
433 if i == num_cols - 1 {
434 *cw = remaining.max(1);
436 } else {
437 *cw = ((*cw) * avail / total_col_w).max(1);
438 remaining = remaining.saturating_sub(*cw);
439 }
440 }
441 }
442
443 let table_style = Style::default().fg(theme.table_body);
444 let header_style = Style::default()
445 .fg(theme.table_header)
446 .add_modifier(Modifier::BOLD);
447 let border_style = Style::default().fg(theme.table_border);
448
449 let total_col_w_final: usize = col_widths.iter().sum();
452 let table_row_w = sep_w + pad_w + total_col_w_final;
453 let table_right_pad = content_width.saturating_sub(table_row_w);
455
456 let mut top = String::from("┌");
458 for (i, cw) in col_widths.iter().enumerate() {
459 top.push_str(&"─".repeat(cw + 2));
460 if i < num_cols - 1 {
461 top.push('┬');
462 }
463 }
464 top.push('┐');
465 let mut top_spans = vec![Span::styled(top, border_style)];
467 if table_right_pad > 0 {
468 top_spans.push(Span::raw(" ".repeat(table_right_pad)));
469 }
470 lines.push(Line::from(top_spans));
471
472 for (row_idx, row) in table_rows.iter().enumerate() {
473 let mut row_spans: Vec<Span> = Vec::new();
475 row_spans.push(Span::styled("│", border_style));
476 for (i, cw) in col_widths.iter().enumerate() {
477 let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
478 let cell_w = display_width(cell_text);
479 let text = if cell_w > *cw {
480 let mut t = String::new();
482 let mut w = 0;
483 for ch in cell_text.chars() {
484 let chw = char_width(ch);
485 if w + chw > *cw {
486 break;
487 }
488 t.push(ch);
489 w += chw;
490 }
491 let fill = cw.saturating_sub(w);
492 format!(" {}{} ", t, " ".repeat(fill))
493 } else {
494 let fill = cw.saturating_sub(cell_w);
496 let align = table_alignments
497 .get(i)
498 .copied()
499 .unwrap_or(pulldown_cmark::Alignment::None);
500 match align {
501 pulldown_cmark::Alignment::Center => {
502 let left = fill / 2;
503 let right = fill - left;
504 format!(
505 " {}{}{} ",
506 " ".repeat(left),
507 cell_text,
508 " ".repeat(right)
509 )
510 }
511 pulldown_cmark::Alignment::Right => {
512 format!(" {}{} ", " ".repeat(fill), cell_text)
513 }
514 _ => {
515 format!(" {}{} ", cell_text, " ".repeat(fill))
516 }
517 }
518 };
519 let style = if row_idx == 0 {
520 header_style
521 } else {
522 table_style
523 };
524 row_spans.push(Span::styled(text, style));
525 row_spans.push(Span::styled("│", border_style));
526 }
527 if table_right_pad > 0 {
529 row_spans.push(Span::raw(" ".repeat(table_right_pad)));
530 }
531 lines.push(Line::from(row_spans));
532
533 if row_idx == 0 {
535 let mut sep = String::from("├");
536 for (i, cw) in col_widths.iter().enumerate() {
537 sep.push_str(&"─".repeat(cw + 2));
538 if i < num_cols - 1 {
539 sep.push('┼');
540 }
541 }
542 sep.push('┤');
543 let mut sep_spans = vec![Span::styled(sep, border_style)];
544 if table_right_pad > 0 {
545 sep_spans.push(Span::raw(" ".repeat(table_right_pad)));
546 }
547 lines.push(Line::from(sep_spans));
548 }
549 }
550
551 let mut bottom = String::from("└");
553 for (i, cw) in col_widths.iter().enumerate() {
554 bottom.push_str(&"─".repeat(cw + 2));
555 if i < num_cols - 1 {
556 bottom.push('┴');
557 }
558 }
559 bottom.push('┘');
560 let mut bottom_spans = vec![Span::styled(bottom, border_style)];
561 if table_right_pad > 0 {
562 bottom_spans.push(Span::raw(" ".repeat(table_right_pad)));
563 }
564 lines.push(Line::from(bottom_spans));
565 }
566 }
567 table_rows.clear();
568 table_alignments.clear();
569 }
570 Event::Start(Tag::TableHead) => {
571 current_row.clear();
572 }
573 Event::End(TagEnd::TableHead) => {
574 table_rows.push(current_row.clone());
575 current_row.clear();
576 }
577 Event::Start(Tag::TableRow) => {
578 current_row.clear();
579 }
580 Event::End(TagEnd::TableRow) => {
581 table_rows.push(current_row.clone());
582 current_row.clear();
583 }
584 Event::Start(Tag::TableCell) => {
585 current_cell.clear();
586 }
587 Event::End(TagEnd::TableCell) => {
588 current_row.push(current_cell.clone());
589 current_cell.clear();
590 }
591 _ => {}
592 }
593 }
594
595 if !current_spans.is_empty() {
597 lines.push(Line::from(current_spans));
598 }
599
600 if lines.is_empty() {
602 let wrapped = wrap_text(md, content_width);
603 for wl in wrapped {
604 lines.push(Line::from(Span::styled(wl, base_style)));
605 }
606 }
607
608 lines
609}
610
611pub fn highlight_code_line<'a>(line: &'a str, lang: &str, theme: &Theme) -> Vec<Span<'static>> {
614 let lang_lower = lang.to_lowercase();
615 let keywords: &[&str] = match lang_lower.as_str() {
621 "rust" | "rs" => &[
622 "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
624 "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
625 "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
626 "true", "false", "unsafe", "extern", "dyn", "abstract", "become", "box", "do", "final",
627 "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "union", "break",
628 "continue",
629 ],
630 "python" | "py" => &[
631 "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
632 "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
633 "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
634 "nonlocal", "assert", "del", "async", "await", "self", "print",
635 ],
636 "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
637 "function",
638 "const",
639 "let",
640 "var",
641 "return",
642 "if",
643 "else",
644 "for",
645 "while",
646 "class",
647 "new",
648 "this",
649 "import",
650 "export",
651 "from",
652 "default",
653 "async",
654 "await",
655 "try",
656 "catch",
657 "finally",
658 "throw",
659 "typeof",
660 "instanceof",
661 "true",
662 "false",
663 "null",
664 "undefined",
665 "of",
666 "in",
667 "switch",
668 "case",
669 ],
670 "go" | "golang" => &[
671 "func",
672 "package",
673 "import",
674 "return",
675 "if",
676 "else",
677 "for",
678 "range",
679 "struct",
680 "interface",
681 "type",
682 "var",
683 "const",
684 "defer",
685 "go",
686 "chan",
687 "select",
688 "case",
689 "switch",
690 "default",
691 "break",
692 "continue",
693 "map",
694 "true",
695 "false",
696 "nil",
697 "make",
698 "append",
699 "len",
700 "cap",
701 ],
702 "java" | "kotlin" | "kt" => &[
703 "public",
704 "private",
705 "protected",
706 "class",
707 "interface",
708 "extends",
709 "implements",
710 "return",
711 "if",
712 "else",
713 "for",
714 "while",
715 "new",
716 "this",
717 "import",
718 "package",
719 "static",
720 "final",
721 "void",
722 "int",
723 "String",
724 "boolean",
725 "true",
726 "false",
727 "null",
728 "try",
729 "catch",
730 "throw",
731 "throws",
732 "fun",
733 "val",
734 "var",
735 "when",
736 "object",
737 "companion",
738 ],
739 "sh" | "bash" | "zsh" | "shell" => &[
740 "if",
741 "then",
742 "else",
743 "elif",
744 "fi",
745 "for",
746 "while",
747 "do",
748 "done",
749 "case",
750 "esac",
751 "function",
752 "return",
753 "exit",
754 "echo",
755 "export",
756 "local",
757 "readonly",
758 "set",
759 "unset",
760 "shift",
761 "source",
762 "in",
763 "true",
764 "false",
765 "read",
766 "declare",
767 "typeset",
768 "trap",
769 "eval",
770 "exec",
771 "test",
772 "select",
773 "until",
774 "break",
775 "continue",
776 "printf",
777 "go",
779 "build",
780 "run",
781 "test",
782 "fmt",
783 "vet",
784 "mod",
785 "get",
786 "install",
787 "clean",
788 "doc",
789 "list",
790 "version",
791 "env",
792 "generate",
793 "tool",
794 "proxy",
795 "GOPATH",
796 "GOROOT",
797 "GOBIN",
798 "GOMODCACHE",
799 "GOPROXY",
800 "GOSUMDB",
801 "cargo",
803 "new",
804 "init",
805 "add",
806 "remove",
807 "update",
808 "check",
809 "clippy",
810 "rustfmt",
811 "rustc",
812 "rustup",
813 "publish",
814 "install",
815 "uninstall",
816 "search",
817 "tree",
818 "locate_project",
819 "metadata",
820 "audit",
821 "watch",
822 "expand",
823 ],
824 "c" | "cpp" | "c++" | "h" | "hpp" => &[
825 "int",
826 "char",
827 "float",
828 "double",
829 "void",
830 "long",
831 "short",
832 "unsigned",
833 "signed",
834 "const",
835 "static",
836 "extern",
837 "struct",
838 "union",
839 "enum",
840 "typedef",
841 "sizeof",
842 "return",
843 "if",
844 "else",
845 "for",
846 "while",
847 "do",
848 "switch",
849 "case",
850 "break",
851 "continue",
852 "default",
853 "goto",
854 "auto",
855 "register",
856 "volatile",
857 "class",
858 "public",
859 "private",
860 "protected",
861 "virtual",
862 "override",
863 "template",
864 "namespace",
865 "using",
866 "new",
867 "delete",
868 "try",
869 "catch",
870 "throw",
871 "nullptr",
872 "true",
873 "false",
874 "this",
875 "include",
876 "define",
877 "ifdef",
878 "ifndef",
879 "endif",
880 ],
881 "sql" => &[
882 "SELECT",
883 "FROM",
884 "WHERE",
885 "INSERT",
886 "UPDATE",
887 "DELETE",
888 "CREATE",
889 "DROP",
890 "ALTER",
891 "TABLE",
892 "INDEX",
893 "INTO",
894 "VALUES",
895 "SET",
896 "AND",
897 "OR",
898 "NOT",
899 "NULL",
900 "JOIN",
901 "LEFT",
902 "RIGHT",
903 "INNER",
904 "OUTER",
905 "ON",
906 "GROUP",
907 "BY",
908 "ORDER",
909 "ASC",
910 "DESC",
911 "HAVING",
912 "LIMIT",
913 "OFFSET",
914 "UNION",
915 "AS",
916 "DISTINCT",
917 "COUNT",
918 "SUM",
919 "AVG",
920 "MIN",
921 "MAX",
922 "LIKE",
923 "IN",
924 "BETWEEN",
925 "EXISTS",
926 "CASE",
927 "WHEN",
928 "THEN",
929 "ELSE",
930 "END",
931 "BEGIN",
932 "COMMIT",
933 "ROLLBACK",
934 "PRIMARY",
935 "KEY",
936 "FOREIGN",
937 "REFERENCES",
938 "select",
939 "from",
940 "where",
941 "insert",
942 "update",
943 "delete",
944 "create",
945 "drop",
946 "alter",
947 "table",
948 "index",
949 "into",
950 "values",
951 "set",
952 "and",
953 "or",
954 "not",
955 "null",
956 "join",
957 "left",
958 "right",
959 "inner",
960 "outer",
961 "on",
962 "group",
963 "by",
964 "order",
965 "asc",
966 "desc",
967 "having",
968 "limit",
969 "offset",
970 "union",
971 "as",
972 "distinct",
973 "count",
974 "sum",
975 "avg",
976 "min",
977 "max",
978 "like",
979 "in",
980 "between",
981 "exists",
982 "case",
983 "when",
984 "then",
985 "else",
986 "end",
987 "begin",
988 "commit",
989 "rollback",
990 "primary",
991 "key",
992 "foreign",
993 "references",
994 ],
995 "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
996 "toml" => &[
997 "true",
998 "false",
999 "true",
1000 "false",
1001 "name",
1003 "version",
1004 "edition",
1005 "authors",
1006 "dependencies",
1007 "dev-dependencies",
1008 "build-dependencies",
1009 "features",
1010 "workspace",
1011 "members",
1012 "exclude",
1013 "include",
1014 "path",
1015 "git",
1016 "branch",
1017 "tag",
1018 "rev",
1019 "package",
1020 "lib",
1021 "bin",
1022 "example",
1023 "test",
1024 "bench",
1025 "doc",
1026 "profile",
1027 "release",
1028 "debug",
1029 "opt-level",
1030 "lto",
1031 "codegen-units",
1032 "panic",
1033 "strip",
1034 "default",
1035 "features",
1036 "optional",
1037 "repository",
1039 "homepage",
1040 "documentation",
1041 "license",
1042 "license-file",
1043 "keywords",
1044 "categories",
1045 "readme",
1046 "description",
1047 "resolver",
1048 ],
1049 "css" | "scss" | "less" => &[
1050 "color",
1051 "background",
1052 "border",
1053 "margin",
1054 "padding",
1055 "display",
1056 "position",
1057 "width",
1058 "height",
1059 "font",
1060 "text",
1061 "flex",
1062 "grid",
1063 "align",
1064 "justify",
1065 "important",
1066 "none",
1067 "auto",
1068 "inherit",
1069 "initial",
1070 "unset",
1071 ],
1072 "dockerfile" | "docker" => &[
1073 "FROM",
1074 "RUN",
1075 "CMD",
1076 "LABEL",
1077 "EXPOSE",
1078 "ENV",
1079 "ADD",
1080 "COPY",
1081 "ENTRYPOINT",
1082 "VOLUME",
1083 "USER",
1084 "WORKDIR",
1085 "ARG",
1086 "ONBUILD",
1087 "STOPSIGNAL",
1088 "HEALTHCHECK",
1089 "SHELL",
1090 "AS",
1091 ],
1092 "ruby" | "rb" => &[
1093 "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
1094 "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
1095 "include", "attr", "self", "true", "false", "nil", "puts", "print",
1096 ],
1097 _ => &[
1098 "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
1099 "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
1100 "this",
1101 ],
1102 };
1103
1104 let primitive_types: &[&str] = match lang_lower.as_str() {
1106 "rust" | "rs" => &[
1107 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
1108 "f32", "f64", "bool", "char", "str",
1109 ],
1110 "go" | "golang" => &[
1111 "int",
1113 "int8",
1114 "int16",
1115 "int32",
1116 "int64",
1117 "uint",
1118 "uint8",
1119 "uint16",
1120 "uint32",
1121 "uint64",
1122 "uintptr",
1123 "float32",
1124 "float64",
1125 "complex64",
1126 "complex128",
1127 "bool",
1128 "byte",
1129 "rune",
1130 "string",
1131 "error",
1132 "any",
1133 ],
1134 _ => &[],
1135 };
1136
1137 let go_type_names: &[&str] = match lang_lower.as_str() {
1139 "go" | "golang" => &[
1140 "Reader",
1142 "Writer",
1143 "Closer",
1144 "ReadWriter",
1145 "ReadCloser",
1146 "WriteCloser",
1147 "ReadWriteCloser",
1148 "Seeker",
1149 "Context",
1150 "Error",
1151 "Stringer",
1152 "Mutex",
1153 "RWMutex",
1154 "WaitGroup",
1155 "Once",
1156 "Pool",
1157 "Map",
1158 "Duration",
1159 "Time",
1160 "Timer",
1161 "Ticker",
1162 "Buffer",
1163 "Builder",
1164 "Request",
1165 "Response",
1166 "ResponseWriter",
1167 "Handler",
1168 "HandlerFunc",
1169 "Server",
1170 "Client",
1171 "Transport",
1172 "File",
1173 "FileInfo",
1174 "FileMode",
1175 "Decoder",
1176 "Encoder",
1177 "Marshaler",
1178 "Unmarshaler",
1179 "Logger",
1180 "Flag",
1181 "Regexp",
1182 "Conn",
1183 "Listener",
1184 "Addr",
1185 "Scanner",
1186 "Token",
1187 "Type",
1188 "Value",
1189 "Kind",
1190 "Cmd",
1191 "Signal",
1192 ],
1193 _ => &[],
1194 };
1195
1196 let comment_prefix = match lang_lower.as_str() {
1197 "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
1198 | "toml" | "dockerfile" | "docker" => "#",
1199 "sql" => "--",
1200 "css" | "scss" | "less" => "/*",
1201 _ => "//",
1202 };
1203
1204 let code_style = Style::default().fg(theme.code_default);
1206 let kw_style = Style::default().fg(theme.code_keyword);
1207 let str_style = Style::default().fg(theme.code_string);
1208 let comment_style = Style::default()
1209 .fg(theme.code_comment)
1210 .add_modifier(Modifier::ITALIC);
1211 let num_style = Style::default().fg(theme.code_number);
1212 let type_style = Style::default().fg(theme.code_type);
1213 let primitive_style = Style::default().fg(theme.code_primitive);
1214 let macro_style = Style::default().fg(theme.code_macro);
1215
1216 let trimmed = line.trim_start();
1217
1218 if trimmed.starts_with(comment_prefix) {
1220 return vec![Span::styled(line.to_string(), comment_style)];
1221 }
1222
1223 let mut spans = Vec::new();
1225 let mut chars = line.chars().peekable();
1226 let mut buf = String::new();
1227
1228 while let Some(&ch) = chars.peek() {
1229 if ch == '"' {
1231 if !buf.is_empty() {
1232 spans.extend(colorize_tokens(
1233 &buf,
1234 keywords,
1235 primitive_types,
1236 go_type_names,
1237 code_style,
1238 kw_style,
1239 num_style,
1240 type_style,
1241 primitive_style,
1242 macro_style,
1243 &lang_lower,
1244 ));
1245 buf.clear();
1246 }
1247 let mut s = String::new();
1248 s.push(ch);
1249 chars.next();
1250 let mut escaped = false;
1251 while let Some(&c) = chars.peek() {
1252 s.push(c);
1253 chars.next();
1254 if escaped {
1255 escaped = false;
1256 continue;
1257 }
1258 if c == '\\' {
1259 escaped = true;
1260 continue;
1261 }
1262 if c == '"' {
1263 break;
1264 }
1265 }
1266 spans.push(Span::styled(s, str_style));
1267 continue;
1268 }
1269 if ch == '`' {
1272 if !buf.is_empty() {
1273 spans.extend(colorize_tokens(
1274 &buf,
1275 keywords,
1276 primitive_types,
1277 go_type_names,
1278 code_style,
1279 kw_style,
1280 num_style,
1281 type_style,
1282 primitive_style,
1283 macro_style,
1284 &lang_lower,
1285 ));
1286 buf.clear();
1287 }
1288 let mut s = String::new();
1289 s.push(ch);
1290 chars.next();
1291 while let Some(&c) = chars.peek() {
1292 s.push(c);
1293 chars.next();
1294 if c == '`' {
1295 break; }
1297 }
1298 spans.push(Span::styled(s, str_style));
1299 continue;
1300 }
1301 if ch == '\'' && matches!(lang_lower.as_str(), "rust" | "rs") {
1303 if !buf.is_empty() {
1304 spans.extend(colorize_tokens(
1305 &buf,
1306 keywords,
1307 primitive_types,
1308 go_type_names,
1309 code_style,
1310 kw_style,
1311 num_style,
1312 type_style,
1313 primitive_style,
1314 macro_style,
1315 &lang_lower,
1316 ));
1317 buf.clear();
1318 }
1319 let mut s = String::new();
1320 s.push(ch);
1321 chars.next();
1322 let mut is_lifetime = false;
1323 while let Some(&c) = chars.peek() {
1325 if c.is_alphanumeric() || c == '_' {
1326 s.push(c);
1327 chars.next();
1328 } else if c == '\'' && s.len() == 2 {
1329 s.push(c);
1331 chars.next();
1332 break;
1333 } else {
1334 is_lifetime = true;
1336 break;
1337 }
1338 }
1339 if is_lifetime || (s.len() > 1 && !s.ends_with('\'')) {
1340 let lifetime_style = Style::default().fg(theme.code_lifetime);
1342 spans.push(Span::styled(s, lifetime_style));
1343 } else {
1344 spans.push(Span::styled(s, str_style));
1346 }
1347 continue;
1348 }
1349 if ch == '\'' && !matches!(lang_lower.as_str(), "rust" | "rs") {
1351 if !buf.is_empty() {
1353 spans.extend(colorize_tokens(
1354 &buf,
1355 keywords,
1356 primitive_types,
1357 go_type_names,
1358 code_style,
1359 kw_style,
1360 num_style,
1361 type_style,
1362 primitive_style,
1363 macro_style,
1364 &lang_lower,
1365 ));
1366 buf.clear();
1367 }
1368 let mut s = String::new();
1369 s.push(ch);
1370 chars.next();
1371 let mut escaped = false;
1372 while let Some(&c) = chars.peek() {
1373 s.push(c);
1374 chars.next();
1375 if escaped {
1376 escaped = false;
1377 continue;
1378 }
1379 if c == '\\' {
1380 escaped = true;
1381 continue;
1382 }
1383 if c == '\'' {
1384 break;
1385 }
1386 }
1387 spans.push(Span::styled(s, str_style));
1388 continue;
1389 }
1390 if ch == '#' && matches!(lang_lower.as_str(), "rust" | "rs") {
1392 let mut lookahead = chars.clone();
1393 if let Some(next) = lookahead.next() {
1394 if next == '[' {
1395 if !buf.is_empty() {
1396 spans.extend(colorize_tokens(
1397 &buf,
1398 keywords,
1399 primitive_types,
1400 go_type_names,
1401 code_style,
1402 kw_style,
1403 num_style,
1404 type_style,
1405 primitive_style,
1406 macro_style,
1407 &lang_lower,
1408 ));
1409 buf.clear();
1410 }
1411 let mut attr = String::new();
1412 attr.push(ch);
1413 chars.next();
1414 let mut depth = 0;
1415 while let Some(&c) = chars.peek() {
1416 attr.push(c);
1417 chars.next();
1418 if c == '[' {
1419 depth += 1;
1420 } else if c == ']' {
1421 depth -= 1;
1422 if depth == 0 {
1423 break;
1424 }
1425 }
1426 }
1427 let attr_style = Style::default().fg(theme.code_attribute);
1429 spans.push(Span::styled(attr, attr_style));
1430 continue;
1431 }
1432 }
1433 }
1434 if ch == '$'
1436 && matches!(
1437 lang_lower.as_str(),
1438 "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
1439 )
1440 {
1441 if !buf.is_empty() {
1442 spans.extend(colorize_tokens(
1443 &buf,
1444 keywords,
1445 primitive_types,
1446 go_type_names,
1447 code_style,
1448 kw_style,
1449 num_style,
1450 type_style,
1451 primitive_style,
1452 macro_style,
1453 &lang_lower,
1454 ));
1455 buf.clear();
1456 }
1457 let var_style = Style::default().fg(theme.code_shell_var);
1459 let mut var = String::new();
1460 var.push(ch);
1461 chars.next();
1462 if let Some(&next_ch) = chars.peek() {
1463 if next_ch == '{' {
1464 var.push(next_ch);
1466 chars.next();
1467 while let Some(&c) = chars.peek() {
1468 var.push(c);
1469 chars.next();
1470 if c == '}' {
1471 break;
1472 }
1473 }
1474 } else if next_ch == '(' {
1475 var.push(next_ch);
1477 chars.next();
1478 let mut depth = 1;
1479 while let Some(&c) = chars.peek() {
1480 var.push(c);
1481 chars.next();
1482 if c == '(' {
1483 depth += 1;
1484 }
1485 if c == ')' {
1486 depth -= 1;
1487 if depth == 0 {
1488 break;
1489 }
1490 }
1491 }
1492 } else if next_ch.is_alphanumeric()
1493 || next_ch == '_'
1494 || next_ch == '@'
1495 || next_ch == '#'
1496 || next_ch == '?'
1497 || next_ch == '!'
1498 {
1499 while let Some(&c) = chars.peek() {
1501 if c.is_alphanumeric() || c == '_' {
1502 var.push(c);
1503 chars.next();
1504 } else {
1505 break;
1506 }
1507 }
1508 }
1509 }
1510 spans.push(Span::styled(var, var_style));
1511 continue;
1512 }
1513 if ch == '/' || ch == '#' || ch == '-' {
1516 let rest: String = chars.clone().collect();
1517 if rest.starts_with(comment_prefix) {
1518 if !buf.is_empty() {
1519 spans.extend(colorize_tokens(
1520 &buf,
1521 keywords,
1522 primitive_types,
1523 go_type_names,
1524 code_style,
1525 kw_style,
1526 num_style,
1527 type_style,
1528 primitive_style,
1529 macro_style,
1530 &lang_lower,
1531 ));
1532 buf.clear();
1533 }
1534 while chars.peek().is_some() {
1536 chars.next();
1537 }
1538 spans.push(Span::styled(rest, comment_style));
1539 break;
1540 }
1541 }
1542 buf.push(ch);
1543 chars.next();
1544 }
1545
1546 if !buf.is_empty() {
1547 spans.extend(colorize_tokens(
1548 &buf,
1549 keywords,
1550 primitive_types,
1551 go_type_names,
1552 code_style,
1553 kw_style,
1554 num_style,
1555 type_style,
1556 primitive_style,
1557 macro_style,
1558 &lang_lower,
1559 ));
1560 }
1561
1562 if spans.is_empty() {
1563 spans.push(Span::styled(line.to_string(), code_style));
1564 }
1565
1566 spans
1567}
1568
1569pub fn colorize_tokens<'a>(
1571 text: &str,
1572 keywords: &[&str],
1573 primitive_types: &[&str],
1574 go_type_names: &[&str],
1575 default_style: Style,
1576 kw_style: Style,
1577 num_style: Style,
1578 type_style: Style,
1579 primitive_style: Style,
1580 macro_style: Style,
1581 lang: &str,
1582) -> Vec<Span<'static>> {
1583 let mut spans = Vec::new();
1584 let mut current_word = String::new();
1585 let mut current_non_word = String::new();
1586 let mut chars = text.chars().peekable();
1587
1588 while let Some(ch) = chars.next() {
1589 if ch.is_alphanumeric() || ch == '_' {
1590 if !current_non_word.is_empty() {
1591 spans.push(Span::styled(current_non_word.clone(), default_style));
1592 current_non_word.clear();
1593 }
1594 current_word.push(ch);
1595 } else {
1596 if ch == '!' && matches!(lang, "rust" | "rs") && !current_word.is_empty() {
1598 let is_macro = chars
1600 .peek()
1601 .map(|&c| c == '(' || c == '{' || c == '[' || c.is_whitespace())
1602 .unwrap_or(true);
1603 if is_macro {
1604 spans.push(Span::styled(current_word.clone(), macro_style));
1606 current_word.clear();
1607 spans.push(Span::styled("!".to_string(), macro_style));
1608 continue;
1609 }
1610 }
1611 if !current_word.is_empty() {
1612 let style = classify_word(
1613 ¤t_word,
1614 keywords,
1615 primitive_types,
1616 go_type_names,
1617 kw_style,
1618 primitive_style,
1619 num_style,
1620 type_style,
1621 default_style,
1622 lang,
1623 );
1624 spans.push(Span::styled(current_word.clone(), style));
1625 current_word.clear();
1626 }
1627 current_non_word.push(ch);
1628 }
1629 }
1630
1631 if !current_non_word.is_empty() {
1633 spans.push(Span::styled(current_non_word, default_style));
1634 }
1635 if !current_word.is_empty() {
1636 let style = classify_word(
1637 ¤t_word,
1638 keywords,
1639 primitive_types,
1640 go_type_names,
1641 kw_style,
1642 primitive_style,
1643 num_style,
1644 type_style,
1645 default_style,
1646 lang,
1647 );
1648 spans.push(Span::styled(current_word, style));
1649 }
1650
1651 spans
1652}
1653
1654pub fn classify_word(
1656 word: &str,
1657 keywords: &[&str],
1658 primitive_types: &[&str],
1659 go_type_names: &[&str],
1660 kw_style: Style,
1661 primitive_style: Style,
1662 num_style: Style,
1663 type_style: Style,
1664 default_style: Style,
1665 lang: &str,
1666) -> Style {
1667 if keywords.contains(&word) {
1668 kw_style
1669 } else if primitive_types.contains(&word) {
1670 primitive_style
1671 } else if word
1672 .chars()
1673 .next()
1674 .map(|c| c.is_ascii_digit())
1675 .unwrap_or(false)
1676 {
1677 num_style
1678 } else if matches!(lang, "go" | "golang") {
1679 if go_type_names.contains(&word) {
1681 type_style
1682 } else {
1683 default_style
1684 }
1685 } else if word
1686 .chars()
1687 .next()
1688 .map(|c| c.is_uppercase())
1689 .unwrap_or(false)
1690 {
1691 type_style
1693 } else {
1694 default_style
1695 }
1696}