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