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