1use std::borrow::Cow;
14use std::fmt;
15use std::io::Write;
16
17#[derive(Debug, Default)]
23pub struct TailCapture {
24 buf: Vec<u8>,
25 cap: usize,
26 start: usize,
27 len: usize,
28 total: u64,
29}
30
31impl TailCapture {
32 #[must_use]
34 pub fn new(cap: usize) -> Self {
35 Self {
36 buf: Vec::new(),
37 cap,
38 start: 0,
39 len: 0,
40 total: 0,
41 }
42 }
43
44 pub fn append(&mut self, s: &[u8]) {
46 if s.is_empty() || self.cap == 0 {
47 return;
48 }
49 if self.buf.is_empty() {
50 self.buf = vec![0; self.cap];
51 }
52 self.total += s.len() as u64;
53
54 if s.len() >= self.cap {
55 self.buf.copy_from_slice(&s[s.len() - self.cap..]);
56 self.start = 0;
57 self.len = self.cap;
58 return;
59 }
60
61 for &b in s {
62 if self.len < self.cap {
63 let pos = (self.start + self.len) % self.cap;
64 self.buf[pos] = b;
65 self.len += 1;
66 } else {
67 self.buf[self.start] = b;
68 self.start = (self.start + 1) % self.cap;
69 }
70 }
71 }
72
73 pub fn take(&mut self) -> Vec<u8> {
75 let mut out = Vec::with_capacity(self.len);
76 for i in 0..self.len {
77 out.push(self.buf[(self.start + i) % self.cap]);
78 }
79 let cap = self.cap;
80 *self = Self::new(cap);
81 out
82 }
83
84 #[must_use]
86 pub fn total(&self) -> u64 {
87 self.total
88 }
89
90 #[must_use]
92 pub fn len(&self) -> usize {
93 self.len
94 }
95
96 #[must_use]
98 pub fn is_empty(&self) -> bool {
99 self.len == 0
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109enum Highlight {
110 Normal,
111 Comment,
112 Keyword1,
113 Keyword2,
114 String,
115 Number,
116}
117
118const SYNTAX_NUMBERS: u8 = 1 << 0;
119const SYNTAX_STRINGS: u8 = 1 << 1;
120const SYNTAX_BACKTICK_STRINGS: u8 = 1 << 2;
121const SYNTAX_CASE_INSENSITIVE: u8 = 1 << 3;
122
123#[derive(Debug)]
128pub struct Syntax {
129 name: &'static str,
130 aliases: &'static str,
131 keywords: &'static [&'static str],
132 singleline_comments: &'static [&'static str],
133 multiline_start: Option<&'static str>,
134 multiline_end: Option<&'static str>,
135 flags: u8,
136}
137
138impl Syntax {
139 #[must_use]
141 pub fn name(&self) -> &'static str {
142 self.name
143 }
144}
145
146static KW_GENERIC: &[&str] = &[
147 "if",
148 "else",
149 "for",
150 "while",
151 "do",
152 "switch",
153 "case",
154 "default",
155 "break",
156 "continue",
157 "return",
158 "try",
159 "catch",
160 "finally",
161 "throw",
162 "throws",
163 "class",
164 "struct",
165 "enum",
166 "interface",
167 "trait",
168 "impl",
169 "fn",
170 "func",
171 "function",
172 "def",
173 "lambda",
174 "let",
175 "var",
176 "const",
177 "static",
178 "public",
179 "private",
180 "protected",
181 "import",
182 "include",
183 "from",
184 "export",
185 "package",
186 "module",
187 "namespace",
188 "new",
189 "delete",
190 "async",
191 "await",
192 "yield",
193 "match",
194 "type",
195 "true|",
196 "false|",
197 "null|",
198 "nil|",
199 "none|",
200 "None|",
201 "NULL|",
202 "void|",
203 "int|",
204 "long|",
205 "float|",
206 "double|",
207 "char|",
208 "bool|",
209 "string|",
210 "String|",
211 "usize|",
212 "isize|",
213 "u8|",
214 "u16|",
215 "u32|",
216 "u64|",
217 "i8|",
218 "i16|",
219 "i32|",
220 "i64|",
221];
222
223static KW_C: &[&str] = &[
224 "auto",
225 "break",
226 "case",
227 "continue",
228 "default",
229 "do",
230 "else",
231 "enum",
232 "extern",
233 "for",
234 "goto",
235 "if",
236 "register",
237 "return",
238 "sizeof",
239 "static",
240 "struct",
241 "switch",
242 "typedef",
243 "union",
244 "volatile",
245 "while",
246 "alignas",
247 "alignof",
248 "and",
249 "and_eq",
250 "asm",
251 "bitand",
252 "bitor",
253 "class",
254 "compl",
255 "constexpr",
256 "const_cast",
257 "decltype",
258 "delete",
259 "dynamic_cast",
260 "explicit",
261 "export",
262 "false",
263 "friend",
264 "inline",
265 "mutable",
266 "namespace",
267 "new",
268 "noexcept",
269 "not",
270 "not_eq",
271 "nullptr",
272 "operator",
273 "or",
274 "or_eq",
275 "private",
276 "protected",
277 "public",
278 "reinterpret_cast",
279 "static_assert",
280 "static_cast",
281 "template",
282 "this",
283 "thread_local",
284 "throw",
285 "true",
286 "try",
287 "typeid",
288 "typename",
289 "virtual",
290 "xor",
291 "xor_eq",
292 "NULL|",
293 "bool|",
294 "char|",
295 "const|",
296 "double|",
297 "float|",
298 "int|",
299 "long|",
300 "short|",
301 "signed|",
302 "size_t|",
303 "ssize_t|",
304 "uint8_t|",
305 "uint16_t|",
306 "uint32_t|",
307 "uint64_t|",
308 "unsigned|",
309 "void|",
310];
311
312static KW_PYTHON: &[&str] = &[
313 "and", "as", "assert", "async", "await", "break", "case", "class", "continue", "def", "del",
314 "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is",
315 "lambda", "match", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
316 "yield", "False|", "None|", "True|", "bool|", "bytes|", "dict|", "float|", "int|", "list|",
317 "object|", "set|", "str|", "tuple|",
318];
319
320static KW_JS: &[&str] = &[
321 "async",
322 "await",
323 "break",
324 "case",
325 "catch",
326 "class",
327 "const",
328 "continue",
329 "debugger",
330 "default",
331 "delete",
332 "do",
333 "else",
334 "export",
335 "extends",
336 "finally",
337 "for",
338 "from",
339 "function",
340 "get",
341 "if",
342 "import",
343 "in",
344 "instanceof",
345 "let",
346 "new",
347 "of",
348 "return",
349 "set",
350 "static",
351 "super",
352 "switch",
353 "this",
354 "throw",
355 "try",
356 "typeof",
357 "var",
358 "void",
359 "while",
360 "with",
361 "yield",
362 "abstract",
363 "as",
364 "declare",
365 "enum",
366 "implements",
367 "interface",
368 "keyof",
369 "namespace",
370 "private",
371 "protected",
372 "public",
373 "readonly",
374 "type",
375 "any|",
376 "boolean|",
377 "false|",
378 "never|",
379 "null|",
380 "number|",
381 "string|",
382 "symbol|",
383 "true|",
384 "undefined|",
385 "unknown|",
386 "void|",
387];
388
389static KW_JAVA: &[&str] = &[
390 "abstract",
391 "assert",
392 "break",
393 "case",
394 "catch",
395 "class",
396 "const",
397 "continue",
398 "default",
399 "do",
400 "else",
401 "enum",
402 "extends",
403 "final",
404 "finally",
405 "for",
406 "goto",
407 "if",
408 "implements",
409 "import",
410 "instanceof",
411 "interface",
412 "native",
413 "new",
414 "package",
415 "private",
416 "protected",
417 "public",
418 "return",
419 "static",
420 "strictfp",
421 "super",
422 "switch",
423 "synchronized",
424 "this",
425 "throw",
426 "throws",
427 "transient",
428 "try",
429 "volatile",
430 "while",
431 "boolean|",
432 "byte|",
433 "char|",
434 "double|",
435 "false|",
436 "float|",
437 "int|",
438 "long|",
439 "null|",
440 "short|",
441 "true|",
442 "void|",
443];
444
445static KW_CSHARP: &[&str] = &[
446 "abstract",
447 "as",
448 "base",
449 "break",
450 "case",
451 "catch",
452 "checked",
453 "class",
454 "const",
455 "continue",
456 "default",
457 "delegate",
458 "do",
459 "else",
460 "enum",
461 "event",
462 "explicit",
463 "extern",
464 "finally",
465 "fixed",
466 "for",
467 "foreach",
468 "goto",
469 "if",
470 "implicit",
471 "in",
472 "interface",
473 "internal",
474 "is",
475 "lock",
476 "namespace",
477 "new",
478 "operator",
479 "out",
480 "override",
481 "params",
482 "private",
483 "protected",
484 "public",
485 "readonly",
486 "ref",
487 "return",
488 "sealed",
489 "sizeof",
490 "stackalloc",
491 "static",
492 "struct",
493 "switch",
494 "this",
495 "throw",
496 "try",
497 "typeof",
498 "unchecked",
499 "unsafe",
500 "using",
501 "virtual",
502 "volatile",
503 "while",
504 "async",
505 "await",
506 "get",
507 "init",
508 "record",
509 "set",
510 "var",
511 "bool|",
512 "byte|",
513 "char|",
514 "decimal|",
515 "double|",
516 "false|",
517 "float|",
518 "int|",
519 "long|",
520 "null|",
521 "object|",
522 "sbyte|",
523 "short|",
524 "string|",
525 "true|",
526 "uint|",
527 "ulong|",
528 "ushort|",
529 "void|",
530];
531
532static KW_GO: &[&str] = &[
533 "break",
534 "case",
535 "chan",
536 "const",
537 "continue",
538 "default",
539 "defer",
540 "else",
541 "fallthrough",
542 "for",
543 "func",
544 "go",
545 "goto",
546 "if",
547 "import",
548 "interface",
549 "map",
550 "package",
551 "range",
552 "return",
553 "select",
554 "struct",
555 "switch",
556 "type",
557 "var",
558 "bool|",
559 "byte|",
560 "complex64|",
561 "complex128|",
562 "error|",
563 "false|",
564 "float32|",
565 "float64|",
566 "int|",
567 "int8|",
568 "int16|",
569 "int32|",
570 "int64|",
571 "nil|",
572 "rune|",
573 "string|",
574 "true|",
575 "uint|",
576 "uint8|",
577 "uint16|",
578 "uint32|",
579 "uint64|",
580 "uintptr|",
581];
582
583static KW_RUST: &[&str] = &[
584 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
585 "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
586 "return", "self", "Self", "static", "struct", "super", "trait", "type", "unsafe", "use",
587 "where", "while", "bool|", "char|", "false|", "f32|", "f64|", "i8|", "i16|", "i32|", "i64|",
588 "i128|", "isize|", "str|", "String|", "true|", "u8|", "u16|", "u32|", "u64|", "u128|",
589 "usize|",
590];
591
592static KW_SHELL: &[&str] = &[
593 "case", "do", "done", "elif", "else", "esac", "fi", "for", "function", "if", "in", "select",
594 "then", "time", "until", "while", "break", "continue", "return", "export", "local", "readonly",
595 "source", "test", "true|", "false|", "echo|", "printf|", "cd|", "pwd|", "read|", "set|",
596 "unset|", "shift|",
597];
598
599static KW_SQL: &[&str] = &[
600 "add",
601 "alter",
602 "and",
603 "as",
604 "asc",
605 "between",
606 "by",
607 "case",
608 "check",
609 "column",
610 "constraint",
611 "create",
612 "delete",
613 "desc",
614 "distinct",
615 "drop",
616 "else",
617 "end",
618 "exists",
619 "foreign",
620 "from",
621 "group",
622 "having",
623 "in",
624 "index",
625 "insert",
626 "into",
627 "is",
628 "join",
629 "key",
630 "left",
631 "like",
632 "limit",
633 "not",
634 "null",
635 "on",
636 "or",
637 "order",
638 "outer",
639 "primary",
640 "references",
641 "right",
642 "select",
643 "set",
644 "table",
645 "then",
646 "union",
647 "unique",
648 "update",
649 "values",
650 "view",
651 "where",
652 "bigint|",
653 "boolean|",
654 "date|",
655 "decimal|",
656 "false|",
657 "int|",
658 "integer|",
659 "numeric|",
660 "real|",
661 "text|",
662 "timestamp|",
663 "true|",
664 "varchar|",
665];
666
667static KW_RUBY: &[&str] = &[
668 "BEGIN", "END", "alias", "and", "begin", "break", "case", "class", "def", "defined?", "do",
669 "else", "elsif", "end", "ensure", "for", "if", "in", "module", "next", "not", "or", "redo",
670 "rescue", "retry", "return", "self", "super", "then", "undef", "unless", "until", "when",
671 "while", "yield", "false|", "nil|", "true|",
672];
673
674static KW_PHP: &[&str] = &[
675 "abstract",
676 "and",
677 "array",
678 "as",
679 "break",
680 "callable",
681 "case",
682 "catch",
683 "class",
684 "clone",
685 "const",
686 "continue",
687 "declare",
688 "default",
689 "die",
690 "do",
691 "echo",
692 "else",
693 "elseif",
694 "empty",
695 "enddeclare",
696 "endfor",
697 "endforeach",
698 "endif",
699 "endswitch",
700 "endwhile",
701 "eval",
702 "exit",
703 "extends",
704 "final",
705 "finally",
706 "fn",
707 "for",
708 "foreach",
709 "function",
710 "global",
711 "goto",
712 "if",
713 "implements",
714 "include",
715 "include_once",
716 "instanceof",
717 "insteadof",
718 "interface",
719 "isset",
720 "list",
721 "match",
722 "namespace",
723 "new",
724 "or",
725 "print",
726 "private",
727 "protected",
728 "public",
729 "readonly",
730 "require",
731 "require_once",
732 "return",
733 "static",
734 "switch",
735 "throw",
736 "trait",
737 "try",
738 "unset",
739 "use",
740 "var",
741 "while",
742 "xor",
743 "bool|",
744 "false|",
745 "float|",
746 "int|",
747 "null|",
748 "string|",
749 "true|",
750 "void|",
751];
752
753static KW_SWIFT: &[&str] = &[
754 "actor",
755 "as",
756 "associatedtype",
757 "async",
758 "await",
759 "break",
760 "case",
761 "catch",
762 "class",
763 "continue",
764 "default",
765 "defer",
766 "do",
767 "else",
768 "enum",
769 "extension",
770 "fallthrough",
771 "for",
772 "func",
773 "guard",
774 "if",
775 "import",
776 "in",
777 "init",
778 "inout",
779 "is",
780 "let",
781 "nonisolated",
782 "operator",
783 "private",
784 "protocol",
785 "public",
786 "repeat",
787 "return",
788 "self",
789 "Self",
790 "static",
791 "struct",
792 "subscript",
793 "super",
794 "switch",
795 "throw",
796 "throws",
797 "try",
798 "typealias",
799 "var",
800 "where",
801 "while",
802 "Any|",
803 "Bool|",
804 "Double|",
805 "false|",
806 "Float|",
807 "Int|",
808 "nil|",
809 "String|",
810 "true|",
811 "Void|",
812];
813
814static KW_KOTLIN: &[&str] = &[
815 "as",
816 "break",
817 "class",
818 "continue",
819 "do",
820 "else",
821 "false",
822 "for",
823 "fun",
824 "if",
825 "in",
826 "interface",
827 "is",
828 "null",
829 "object",
830 "package",
831 "return",
832 "super",
833 "this",
834 "throw",
835 "true",
836 "try",
837 "typealias",
838 "typeof",
839 "val",
840 "var",
841 "when",
842 "while",
843 "actual",
844 "annotation",
845 "by",
846 "catch",
847 "companion",
848 "const",
849 "constructor",
850 "crossinline",
851 "data",
852 "enum",
853 "expect",
854 "external",
855 "final",
856 "finally",
857 "import",
858 "infix",
859 "init",
860 "inline",
861 "inner",
862 "internal",
863 "lateinit",
864 "noinline",
865 "open",
866 "operator",
867 "out",
868 "override",
869 "private",
870 "protected",
871 "public",
872 "reified",
873 "sealed",
874 "suspend",
875 "tailrec",
876 "vararg",
877 "Any|",
878 "Boolean|",
879 "Byte|",
880 "Char|",
881 "Double|",
882 "Float|",
883 "Int|",
884 "Long|",
885 "Short|",
886 "String|",
887 "Unit|",
888];
889
890static KW_ZIG: &[&str] = &[
891 "addrspace",
892 "align",
893 "allowzero",
894 "and",
895 "anyframe",
896 "anytype",
897 "asm",
898 "async",
899 "await",
900 "break",
901 "callconv",
902 "catch",
903 "comptime",
904 "const",
905 "continue",
906 "defer",
907 "else",
908 "enum",
909 "errdefer",
910 "error",
911 "export",
912 "extern",
913 "fn",
914 "for",
915 "if",
916 "inline",
917 "linksection",
918 "noalias",
919 "noinline",
920 "nosuspend",
921 "opaque",
922 "or",
923 "orelse",
924 "packed",
925 "pub",
926 "resume",
927 "return",
928 "struct",
929 "suspend",
930 "switch",
931 "test",
932 "threadlocal",
933 "try",
934 "union",
935 "unreachable",
936 "usingnamespace",
937 "var",
938 "volatile",
939 "while",
940 "bool|",
941 "false|",
942 "f32|",
943 "f64|",
944 "i32|",
945 "i64|",
946 "null|",
947 "true|",
948 "u8|",
949 "u16|",
950 "u32|",
951 "u64|",
952 "usize|",
953 "void|",
954];
955
956static KW_LUA: &[&str] = &[
957 "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
958 "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
959];
960
961static KW_HTML: &[&str] = &[
962 "a", "body", "button", "div", "doctype", "form", "h1", "h2", "h3", "head", "html", "input",
963 "label", "li", "link", "main", "meta", "ol", "option", "p", "script", "section", "select",
964 "span", "style", "table", "tbody", "td", "th", "thead", "title", "tr", "ul", "class|", "href|",
965 "id|", "name|", "rel|", "src|", "type|", "value|",
966];
967
968static KW_CSS: &[&str] = &[
969 "align-items",
970 "background",
971 "border",
972 "bottom",
973 "color",
974 "display",
975 "flex",
976 "font",
977 "font-size",
978 "gap",
979 "grid",
980 "height",
981 "justify-content",
982 "left",
983 "margin",
984 "max-width",
985 "min-width",
986 "padding",
987 "position",
988 "right",
989 "top",
990 "transform",
991 "width",
992 "z-index",
993 "absolute|",
994 "auto|",
995 "block|",
996 "flex|",
997 "grid|",
998 "hidden|",
999 "inline|",
1000 "none|",
1001 "relative|",
1002 "solid|",
1003];
1004
1005static SYNTAXES: &[Syntax] = &[
1006 Syntax {
1007 name: "generic",
1008 aliases: "text txt",
1009 keywords: KW_GENERIC,
1010 singleline_comments: &["//", "#"],
1011 multiline_start: Some("/*"),
1012 multiline_end: Some("*/"),
1013 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS | SYNTAX_BACKTICK_STRINGS,
1014 },
1015 Syntax {
1016 name: "c",
1017 aliases: "c h cpp c++ cc cxx hpp hxx objc objective-c",
1018 keywords: KW_C,
1019 singleline_comments: &["//"],
1020 multiline_start: Some("/*"),
1021 multiline_end: Some("*/"),
1022 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1023 },
1024 Syntax {
1025 name: "python",
1026 aliases: "py python py3",
1027 keywords: KW_PYTHON,
1028 singleline_comments: &["#"],
1029 multiline_start: None,
1030 multiline_end: None,
1031 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1032 },
1033 Syntax {
1034 name: "javascript",
1035 aliases: "js jsx javascript typescript ts tsx node mjs cjs",
1036 keywords: KW_JS,
1037 singleline_comments: &["//"],
1038 multiline_start: Some("/*"),
1039 multiline_end: Some("*/"),
1040 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS | SYNTAX_BACKTICK_STRINGS,
1041 },
1042 Syntax {
1043 name: "java",
1044 aliases: "java",
1045 keywords: KW_JAVA,
1046 singleline_comments: &["//"],
1047 multiline_start: Some("/*"),
1048 multiline_end: Some("*/"),
1049 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1050 },
1051 Syntax {
1052 name: "csharp",
1053 aliases: "cs c# csharp dotnet",
1054 keywords: KW_CSHARP,
1055 singleline_comments: &["//"],
1056 multiline_start: Some("/*"),
1057 multiline_end: Some("*/"),
1058 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1059 },
1060 Syntax {
1061 name: "go",
1062 aliases: "go golang",
1063 keywords: KW_GO,
1064 singleline_comments: &["//"],
1065 multiline_start: Some("/*"),
1066 multiline_end: Some("*/"),
1067 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS | SYNTAX_BACKTICK_STRINGS,
1068 },
1069 Syntax {
1070 name: "rust",
1071 aliases: "rs rust",
1072 keywords: KW_RUST,
1073 singleline_comments: &["//"],
1074 multiline_start: Some("/*"),
1075 multiline_end: Some("*/"),
1076 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1077 },
1078 Syntax {
1079 name: "shell",
1080 aliases: "sh bash zsh shell fish ksh",
1081 keywords: KW_SHELL,
1082 singleline_comments: &["#"],
1083 multiline_start: None,
1084 multiline_end: None,
1085 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS | SYNTAX_BACKTICK_STRINGS,
1086 },
1087 Syntax {
1088 name: "sql",
1089 aliases: "sql postgres mysql sqlite",
1090 keywords: KW_SQL,
1091 singleline_comments: &["--"],
1092 multiline_start: Some("/*"),
1093 multiline_end: Some("*/"),
1094 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS | SYNTAX_CASE_INSENSITIVE,
1095 },
1096 Syntax {
1097 name: "ruby",
1098 aliases: "rb ruby",
1099 keywords: KW_RUBY,
1100 singleline_comments: &["#"],
1101 multiline_start: None,
1102 multiline_end: None,
1103 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1104 },
1105 Syntax {
1106 name: "php",
1107 aliases: "php",
1108 keywords: KW_PHP,
1109 singleline_comments: &["//", "#"],
1110 multiline_start: Some("/*"),
1111 multiline_end: Some("*/"),
1112 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1113 },
1114 Syntax {
1115 name: "swift",
1116 aliases: "swift",
1117 keywords: KW_SWIFT,
1118 singleline_comments: &["//"],
1119 multiline_start: Some("/*"),
1120 multiline_end: Some("*/"),
1121 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1122 },
1123 Syntax {
1124 name: "kotlin",
1125 aliases: "kt kts kotlin",
1126 keywords: KW_KOTLIN,
1127 singleline_comments: &["//"],
1128 multiline_start: Some("/*"),
1129 multiline_end: Some("*/"),
1130 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1131 },
1132 Syntax {
1133 name: "zig",
1134 aliases: "zig",
1135 keywords: KW_ZIG,
1136 singleline_comments: &["//"],
1137 multiline_start: None,
1138 multiline_end: None,
1139 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1140 },
1141 Syntax {
1142 name: "lua",
1143 aliases: "lua",
1144 keywords: KW_LUA,
1145 singleline_comments: &["--"],
1146 multiline_start: None,
1147 multiline_end: None,
1148 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1149 },
1150 Syntax {
1151 name: "html",
1152 aliases: "html htm xml svg",
1153 keywords: KW_HTML,
1154 singleline_comments: &[],
1155 multiline_start: Some("<!--"),
1156 multiline_end: Some("-->"),
1157 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1158 },
1159 Syntax {
1160 name: "css",
1161 aliases: "css scss sass",
1162 keywords: KW_CSS,
1163 singleline_comments: &[],
1164 multiline_start: Some("/*"),
1165 multiline_end: Some("*/"),
1166 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1167 },
1168 Syntax {
1169 name: "json",
1170 aliases: "json jsonc",
1171 keywords: &[],
1172 singleline_comments: &["//"],
1173 multiline_start: Some("/*"),
1174 multiline_end: Some("*/"),
1175 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1176 },
1177 Syntax {
1178 name: "yaml",
1179 aliases: "yaml yml toml ini",
1180 keywords: &[],
1181 singleline_comments: &["#"],
1182 multiline_start: None,
1183 multiline_end: None,
1184 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1185 },
1186 Syntax {
1187 name: "markdown",
1188 aliases: "md markdown",
1189 keywords: KW_GENERIC,
1190 singleline_comments: &[],
1191 multiline_start: Some("<!--"),
1192 multiline_end: Some("-->"),
1193 flags: SYNTAX_NUMBERS | SYNTAX_STRINGS,
1194 },
1195];
1196
1197#[must_use]
1199pub fn syntax_for_lang(lang: &str) -> &'static Syntax {
1200 if !lang.is_empty() {
1201 for s in SYNTAXES {
1202 if s.name.eq_ignore_ascii_case(lang)
1203 || s.aliases
1204 .split(' ')
1205 .any(|a| !a.is_empty() && a.eq_ignore_ascii_case(lang))
1206 {
1207 return s;
1208 }
1209 }
1210 }
1211 &SYNTAXES[0]
1212}
1213
1214#[must_use]
1216pub fn syntax_for_path(path: &str) -> &'static Syntax {
1217 if path.is_empty() {
1218 return syntax_for_lang("");
1219 }
1220 let base = path.rsplit('/').next().unwrap_or(path);
1221 if base.eq_ignore_ascii_case("Dockerfile") || base.eq_ignore_ascii_case("Makefile") {
1222 return syntax_for_lang("sh");
1223 }
1224 match base.rfind('.') {
1225 Some(dot) if dot + 1 < base.len() => syntax_for_lang(&base[dot + 1..]),
1226 _ => syntax_for_lang(""),
1227 }
1228}
1229
1230fn syntax_separator(c: u8) -> bool {
1231 c == 0 || c.is_ascii_whitespace() || b",.()+-/*=~%[]{}<>:;!&|^?".contains(&c)
1232}
1233
1234fn syntax_color(hl: Highlight) -> u16 {
1235 match hl {
1236 Highlight::Comment => 244,
1237 Highlight::Keyword1 => 214,
1238 Highlight::Keyword2 => 81,
1239 Highlight::String => 150,
1240 Highlight::Number => 203,
1241 Highlight::Normal => 252,
1242 }
1243}
1244
1245fn keyword_len(kw: &str) -> (usize, bool) {
1246 if let Some(stripped) = kw.strip_suffix('|') {
1247 (stripped.len(), true)
1248 } else {
1249 (kw.len(), false)
1250 }
1251}
1252
1253fn bytes_eq_ci(a: &[u8], b: &[u8]) -> bool {
1254 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_ignore_ascii_case(y))
1255}
1256
1257fn match_keyword(syn: &Syntax, rest: &[u8]) -> Option<(usize, Highlight)> {
1258 for kw in syn.keywords {
1259 let (klen, secondary) = keyword_len(kw);
1260 if rest.len() < klen {
1261 continue;
1262 }
1263 let kbytes = &kw.as_bytes()[..klen];
1264 let matched = if syn.flags & SYNTAX_CASE_INSENSITIVE != 0 {
1265 bytes_eq_ci(&rest[..klen], kbytes)
1266 } else {
1267 &rest[..klen] == kbytes
1268 };
1269 if !matched {
1270 continue;
1271 }
1272 let follow = rest.get(klen).copied().unwrap_or(0);
1273 if !syntax_separator(follow) {
1274 continue;
1275 }
1276 let hl = if secondary {
1277 Highlight::Keyword2
1278 } else {
1279 Highlight::Keyword1
1280 };
1281 return Some((klen, hl));
1282 }
1283 None
1284}
1285
1286fn number_len(rest: &[u8]) -> usize {
1287 rest.iter()
1288 .take_while(|&&c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'.' | b'+' | b'-'))
1289 .count()
1290}
1291
1292#[derive(Debug, Clone, Copy, Default)]
1298pub struct RenderOptions {
1299 pub use_color: bool,
1301 pub format_thinking: bool,
1303 pub format_markdown: bool,
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308enum MdPending {
1309 None,
1310 Star,
1311 Backtick,
1312}
1313
1314const UPTO_MARKER: &[u8] = b"[upto]";
1315const FENCE_LANG_MAX: usize = 31;
1316
1317const THINK_GREY: &[u8] = b"\x1b[3;38;5;238m";
1320
1321fn is_unsafe_control(b: u8) -> bool {
1324 (b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r')) || b == 0x7f
1325}
1326
1327fn without_control_bytes(bytes: &[u8]) -> Cow<'_, [u8]> {
1338 if bytes.iter().copied().any(is_unsafe_control) {
1339 Cow::Owned(
1340 bytes
1341 .iter()
1342 .copied()
1343 .filter(|&b| !is_unsafe_control(b))
1344 .collect(),
1345 )
1346 } else {
1347 Cow::Borrowed(bytes)
1348 }
1349}
1350
1351#[allow(clippy::struct_excessive_bools)]
1357pub struct TokenRenderer<W: Write> {
1358 sink: W,
1359 opts: RenderOptions,
1360 capture: Option<TailCapture>,
1361
1362 in_think: bool,
1363 color_open: bool,
1364 last_output_newline: bool,
1365 wrote_visible_output: bool,
1366
1367 md_bold: bool,
1368 md_italic: bool,
1369 md_inline_code: bool,
1370 md_code_block: bool,
1371 md_fence_info: bool,
1372 md_code_line_start: bool,
1373 md_code_in_ml_comment: bool,
1374 md_syntax_silent: bool,
1375 md_syntax_has_highlight: bool,
1376 md_pending: MdPending,
1377 md_pending_len: usize,
1378 md_syntax: Option<&'static Syntax>,
1379 md_fence_lang: String,
1380 md_code_line_prefix: Option<String>,
1381 md_code_line_prefix_color: Option<String>,
1382 md_code_highlight_upto: bool,
1383 md_code_line: Vec<u8>,
1384
1385 pending: Vec<u8>,
1386 utf8_pending: [u8; 4],
1387 utf8_pending_len: usize,
1388 utf8_pending_need: usize,
1389}
1390
1391impl<W: Write> fmt::Debug for TokenRenderer<W> {
1392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1393 f.debug_struct("TokenRenderer")
1394 .field("opts", &self.opts)
1395 .field("in_think", &self.in_think)
1396 .field("md_code_block", &self.md_code_block)
1397 .field("wrote_visible_output", &self.wrote_visible_output)
1398 .field("last_output_newline", &self.last_output_newline)
1399 .finish_non_exhaustive()
1400 }
1401}
1402
1403impl<W: Write> TokenRenderer<W> {
1404 pub fn sink_mut(&mut self) -> &mut W {
1407 &mut self.sink
1408 }
1409
1410 pub fn new(sink: W, opts: RenderOptions) -> Self {
1412 Self {
1413 sink,
1414 opts,
1415 capture: None,
1416 in_think: false,
1417 color_open: false,
1418 last_output_newline: false,
1419 wrote_visible_output: false,
1420 md_bold: false,
1421 md_italic: false,
1422 md_inline_code: false,
1423 md_code_block: false,
1424 md_fence_info: false,
1425 md_code_line_start: false,
1426 md_code_in_ml_comment: false,
1427 md_syntax_silent: false,
1428 md_syntax_has_highlight: false,
1429 md_pending: MdPending::None,
1430 md_pending_len: 0,
1431 md_syntax: None,
1432 md_fence_lang: String::new(),
1433 md_code_line_prefix: None,
1434 md_code_line_prefix_color: None,
1435 md_code_highlight_upto: false,
1436 md_code_line: Vec::new(),
1437 pending: Vec::new(),
1438 utf8_pending: [0; 4],
1439 utf8_pending_len: 0,
1440 utf8_pending_need: 0,
1441 }
1442 }
1443
1444 pub fn set_capture(&mut self, capture: Option<TailCapture>) {
1446 self.capture = capture;
1447 }
1448
1449 pub fn take_capture(&mut self) -> Option<TailCapture> {
1451 self.capture.take()
1452 }
1453
1454 #[must_use]
1456 pub fn wrote_visible_output(&self) -> bool {
1457 self.wrote_visible_output
1458 }
1459
1460 #[must_use]
1462 pub fn last_output_newline(&self) -> bool {
1463 self.last_output_newline
1464 }
1465
1466 pub fn set_in_think(&mut self, in_think: bool) {
1468 self.in_think = in_think;
1469 }
1470
1471 pub fn write(&mut self, text: &str) {
1473 self.write_bytes(text.as_bytes());
1474 }
1475
1476 pub fn write_bytes(&mut self, bytes: &[u8]) {
1478 let bytes = without_control_bytes(bytes);
1479 if self.opts.format_thinking {
1480 self.process(&bytes, false);
1481 } else {
1482 for &b in bytes.iter() {
1483 self.write_char(b);
1484 }
1485 }
1486 }
1487
1488 pub fn finish(&mut self) {
1490 if self.opts.format_thinking {
1491 self.process(&[], true);
1492 }
1493 self.markdown_finish();
1494 self.flush_utf8();
1495 self.reset_color();
1496 if self.wrote_visible_output {
1497 if !self.last_output_newline {
1498 self.out(b"\n");
1499 }
1500 self.out(b"\n");
1501 self.last_output_newline = true;
1502 }
1503 let _ = self.sink.flush();
1504 }
1505
1506 pub fn into_sink(self) -> W {
1511 self.sink
1512 }
1513
1514 pub fn color(&mut self, seq: &str) {
1516 self.markdown_emit_pending_literals();
1517 self.flush_utf8();
1518 let reset = seq.is_empty() || seq == "\x1b[0m";
1519 if self.opts.use_color && !seq.is_empty() {
1520 self.out_str(seq);
1521 }
1522 self.color_open = self.opts.use_color && !reset;
1523 }
1524
1525 pub fn plain(&mut self, s: &str) {
1527 let s = without_control_bytes(s.as_bytes());
1528 self.markdown_emit_pending_literals();
1529 self.flush_utf8();
1530 self.out(&s);
1531 if !s.is_empty() {
1532 self.wrote_visible_output = true;
1533 self.last_output_newline = s.last() == Some(&b'\n');
1534 }
1535 }
1536
1537 pub fn restore_text_attrs(&mut self) {
1539 if !self.opts.use_color || !self.color_open || !self.has_text_attrs() {
1540 return;
1541 }
1542 self.set_text_attrs();
1543 }
1544
1545 pub fn code_stream_begin(&mut self, syntax: &'static Syntax) {
1547 self.reset_color();
1548 self.md_code_block = true;
1549 self.md_inline_code = false;
1550 self.md_fence_info = false;
1551 self.md_code_line_start = true;
1552 self.md_code_in_ml_comment = false;
1553 self.md_syntax = Some(syntax);
1554 self.md_fence_lang.clear();
1555 self.md_code_line_prefix = None;
1556 self.md_code_line_prefix_color = None;
1557 self.md_code_highlight_upto = false;
1558 self.md_code_line.clear();
1559 }
1560
1561 pub fn code_stream_set_prefix(&mut self, prefix: Option<&str>, color: Option<&str>) {
1563 self.md_code_line_prefix = prefix.map(str::to_owned);
1564 self.md_code_line_prefix_color = color.map(str::to_owned);
1565 }
1566
1567 pub fn code_stream_set_upto_marker(&mut self, enabled: bool) {
1569 self.md_code_highlight_upto = enabled;
1570 }
1571
1572 pub fn code_stream_end(&mut self) {
1574 self.code_end();
1575 }
1576
1577 fn out(&mut self, s: &[u8]) {
1580 if let Some(c) = self.capture.as_mut() {
1581 c.append(s);
1582 } else {
1583 let _ = self.sink.write_all(s);
1584 }
1585 }
1586
1587 fn out_str(&mut self, s: &str) {
1588 self.out(s.as_bytes());
1589 }
1590
1591 fn set_grey(&mut self) {
1592 if self.opts.use_color {
1593 self.out(THINK_GREY);
1596 }
1597 }
1598
1599 fn reset_color(&mut self) {
1600 if self.opts.use_color {
1601 self.out(b"\x1b[0m");
1602 }
1603 self.color_open = false;
1604 }
1605
1606 fn has_text_attrs(&self) -> bool {
1607 self.in_think || self.md_bold || self.md_italic || self.md_inline_code || self.md_code_block
1608 }
1609
1610 fn set_text_attrs(&mut self) {
1611 if !self.opts.use_color {
1612 return;
1613 }
1614 if self.in_think {
1615 self.set_grey();
1616 return;
1617 }
1618 if self.md_code_block {
1619 self.out(b"\x1b[38;5;75m");
1620 return;
1621 } else if self.md_inline_code {
1622 self.out(b"\x1b[36m");
1623 }
1624 if self.md_bold {
1625 self.out(b"\x1b[1m");
1626 }
1627 if self.md_italic {
1628 self.out(b"\x1b[3m");
1629 }
1630 }
1631
1632 fn write_complete_char_raw(&mut self, s: &[u8]) {
1633 let styled = self.opts.use_color && self.has_text_attrs();
1634 if styled && !self.color_open {
1635 self.set_text_attrs();
1636 self.color_open = true;
1637 } else if !styled && self.color_open {
1638 self.reset_color();
1639 }
1640 self.out(s);
1641 if !s.is_empty() {
1642 self.wrote_visible_output = true;
1643 }
1644 self.last_output_newline = s == b"\n";
1645 }
1646
1647 fn flush_utf8(&mut self) {
1648 if self.utf8_pending_len == 0 {
1649 return;
1650 }
1651 let buf: [u8; 4] = self.utf8_pending;
1652 let len = self.utf8_pending_len;
1653 self.write_complete_char_raw(&buf[..len]);
1654 self.utf8_pending_len = 0;
1655 self.utf8_pending_need = 0;
1656 }
1657
1658 fn utf8_need(c: u8) -> usize {
1659 match c {
1660 0xc2..=0xdf => 2,
1661 0xe0..=0xef => 3,
1662 0xf0..=0xf4 => 4,
1663 _ => 1,
1664 }
1665 }
1666
1667 fn write_char_raw(&mut self, c: u8) {
1668 if self.utf8_pending_len > 0 {
1669 if c & 0xc0 == 0x80 && self.utf8_pending_len < self.utf8_pending.len() {
1670 self.utf8_pending[self.utf8_pending_len] = c;
1671 self.utf8_pending_len += 1;
1672 if self.utf8_pending_len == self.utf8_pending_need {
1673 self.flush_utf8();
1674 }
1675 return;
1676 }
1677 self.flush_utf8();
1678 }
1679
1680 let need = Self::utf8_need(c);
1681 if need == 1 {
1682 self.write_complete_char_raw(&[c]);
1683 return;
1684 }
1685 self.utf8_pending[0] = c;
1686 self.utf8_pending_len = 1;
1687 self.utf8_pending_need = need;
1688 }
1689
1690 fn write_plain_byte(&mut self, c: u8) {
1692 let (bold, italic, inline, block) = (
1693 self.md_bold,
1694 self.md_italic,
1695 self.md_inline_code,
1696 self.md_code_block,
1697 );
1698 self.md_bold = false;
1699 self.md_italic = false;
1700 self.md_inline_code = false;
1701 self.md_code_block = false;
1702 self.write_char_raw(c);
1703 self.md_bold = bold;
1704 self.md_italic = italic;
1705 self.md_inline_code = inline;
1706 self.md_code_block = block;
1707 }
1708
1709 fn syntax_write(&mut self, hl: Highlight, s: &[u8]) {
1712 if s.is_empty() {
1713 return;
1714 }
1715 if hl != Highlight::Normal {
1716 self.md_syntax_has_highlight = true;
1717 }
1718 if self.md_syntax_silent {
1719 return;
1720 }
1721 if self.opts.use_color && hl != Highlight::Normal {
1722 let seq = format!("\x1b[38;5;{}m", syntax_color(hl));
1723 self.out_str(&seq);
1724 }
1725 self.out(s);
1726 if self.opts.use_color && hl != Highlight::Normal {
1727 self.out(b"\x1b[0m");
1728 }
1729 self.wrote_visible_output = true;
1730 self.last_output_newline = false;
1731 }
1732
1733 fn syntax_write_upto_marker(&mut self) {
1734 self.md_syntax_has_highlight = true;
1735 if self.md_syntax_silent {
1736 return;
1737 }
1738 if self.opts.use_color {
1739 self.out(b"\x1b[38;5;244m[");
1740 self.out(b"\x1b[1;38;5;177mupto");
1741 self.out(b"\x1b[38;5;244m]\x1b[0m");
1742 } else {
1743 self.out(UPTO_MARKER);
1744 }
1745 self.wrote_visible_output = true;
1746 self.last_output_newline = false;
1747 }
1748
1749 #[allow(clippy::too_many_lines)]
1750 fn syntax_emit_line(&mut self, line: &[u8]) {
1751 let syn = self.md_syntax.unwrap_or_else(|| syntax_for_lang(""));
1752 let mut i = 0;
1753 let end = line.len();
1754 let mut prev_sep = true;
1755 let mut prev_hl = Highlight::Normal;
1756
1757 while i < end {
1758 let rest = &line[i..];
1759
1760 if self.md_code_highlight_upto && rest.starts_with(UPTO_MARKER) {
1761 self.syntax_write_upto_marker();
1762 i += UPTO_MARKER.len();
1763 prev_sep = true;
1764 prev_hl = Highlight::Normal;
1765 continue;
1766 }
1767
1768 if self.md_code_in_ml_comment {
1769 if let Some(mce) = syn.multiline_end
1770 && let Some(pos) = find_sub(rest, mce.as_bytes())
1771 {
1772 let take = pos + mce.len();
1773 let seg = &line[i..i + take];
1774 self.syntax_write(Highlight::Comment, seg);
1775 i += take;
1776 self.md_code_in_ml_comment = false;
1777 prev_sep = true;
1778 prev_hl = Highlight::Comment;
1779 continue;
1780 }
1781 let seg = &line[i..];
1782 self.syntax_write(Highlight::Comment, seg);
1783 return;
1784 }
1785
1786 if syn
1787 .singleline_comments
1788 .iter()
1789 .any(|m| rest.starts_with(m.as_bytes()))
1790 {
1791 let seg = &line[i..];
1792 self.syntax_write(Highlight::Comment, seg);
1793 return;
1794 }
1795
1796 if let (Some(mls), Some(mle)) = (syn.multiline_start, syn.multiline_end)
1797 && rest.starts_with(mls.as_bytes())
1798 {
1799 let body = &rest[mls.len()..];
1800 let take = if let Some(pos) = find_sub(body, mle.as_bytes()) {
1801 mls.len() + pos + mle.len()
1802 } else {
1803 self.md_code_in_ml_comment = true;
1804 rest.len()
1805 };
1806 let seg = &line[i..i + take];
1807 self.syntax_write(Highlight::Comment, seg);
1808 i += take;
1809 prev_sep = false;
1810 prev_hl = Highlight::Comment;
1811 continue;
1812 }
1813
1814 let c = rest[0];
1815 if syn.flags & SYNTAX_STRINGS != 0
1816 && (c == b'"'
1817 || c == b'\''
1818 || (syn.flags & SYNTAX_BACKTICK_STRINGS != 0 && c == b'`'))
1819 {
1820 let quote = c;
1821 let mut q = 1;
1822 while q < rest.len() {
1823 if rest[q] == b'\\' && q + 1 < rest.len() {
1824 q += 2;
1825 continue;
1826 }
1827 q += 1;
1828 if rest[q - 1] == quote {
1829 break;
1830 }
1831 }
1832 let seg = &line[i..i + q];
1833 self.syntax_write(Highlight::String, seg);
1834 i += q;
1835 prev_sep = false;
1836 prev_hl = Highlight::String;
1837 continue;
1838 }
1839
1840 let number_start = c.is_ascii_digit() && (prev_sep || prev_hl == Highlight::Number)
1841 || (c == b'.' && i > 0 && prev_hl == Highlight::Number);
1842 if syn.flags & SYNTAX_NUMBERS != 0 && number_start {
1843 let nlen = number_len(rest);
1844 let seg = &line[i..i + nlen];
1845 self.syntax_write(Highlight::Number, seg);
1846 i += nlen;
1847 prev_sep = false;
1848 prev_hl = Highlight::Number;
1849 continue;
1850 }
1851
1852 if prev_sep && let Some((klen, khl)) = match_keyword(syn, rest) {
1853 let seg = &line[i..i + klen];
1854 self.syntax_write(khl, seg);
1855 i += klen;
1856 prev_sep = false;
1857 prev_hl = khl;
1858 continue;
1859 }
1860
1861 let seg = &line[i..=i];
1862 self.syntax_write(Highlight::Normal, seg);
1863 prev_sep = syntax_separator(c);
1864 prev_hl = Highlight::Normal;
1865 i += 1;
1866 }
1867 }
1868
1869 fn terminal_cols() -> usize {
1872 80
1875 }
1876
1877 fn code_line_can_repaint(&self) -> bool {
1878 if !self.opts.use_color || self.capture.is_some() || self.md_code_line.is_empty() {
1879 return false;
1880 }
1881 let cols = Self::terminal_cols();
1882 let prefix_len = self.md_code_line_prefix.as_ref().map_or(0, String::len);
1883 if cols <= 1 || prefix_len + self.md_code_line.len() >= cols {
1884 return false;
1885 }
1886 self.md_code_line
1887 .iter()
1888 .all(|&c| c == b'\r' || (0x20..0x80).contains(&c) && c != 0x1b)
1889 }
1890
1891 fn code_write_line_prefix(&mut self) {
1892 let Some(prefix) = self.md_code_line_prefix.clone() else {
1893 return;
1894 };
1895 let color = self.md_code_line_prefix_color.clone();
1896 if self.opts.use_color
1897 && let Some(col) = &color
1898 {
1899 self.out_str(col);
1900 }
1901 self.out_str(&prefix);
1902 if self.opts.use_color && color.is_some() {
1903 self.out(b"\x1b[0m");
1904 }
1905 self.color_open = false;
1906 }
1907
1908 fn code_scan_line(&mut self) -> (bool, bool) {
1911 let old_silent = self.md_syntax_silent;
1912 let old_highlight = self.md_syntax_has_highlight;
1913 let old_ml = self.md_code_in_ml_comment;
1914
1915 self.md_syntax_silent = true;
1916 self.md_syntax_has_highlight = false;
1917 let line = std::mem::take(&mut self.md_code_line);
1918 self.syntax_emit_line(&line);
1919 self.md_code_line = line;
1920 let changed = self.md_syntax_has_highlight;
1921 let final_ml = self.md_code_in_ml_comment;
1922
1923 self.md_code_in_ml_comment = old_ml;
1924 self.md_syntax_silent = old_silent;
1925 self.md_syntax_has_highlight = old_highlight;
1926 (changed, final_ml)
1927 }
1928
1929 fn code_emit_buffered_line(&mut self, with_newline: bool) {
1930 let (changed, final_ml) = self.code_scan_line();
1931 let repaint = changed && self.code_line_can_repaint();
1932 if repaint {
1933 self.reset_color();
1934 self.out(b"\r\x1b[0K");
1935 self.code_write_line_prefix();
1936 let line = std::mem::take(&mut self.md_code_line);
1937 self.syntax_emit_line(&line);
1938 self.md_code_line = line;
1939 } else {
1940 self.md_code_in_ml_comment = final_ml;
1941 }
1942 self.md_code_line.clear();
1943 if with_newline {
1944 self.write_plain_byte(b'\n');
1945 self.wrote_visible_output = true;
1946 self.last_output_newline = true;
1947 self.md_code_line_start = true;
1948 }
1949 }
1950
1951 fn code_byte(&mut self, c: u8) {
1952 if c == b'\n' {
1953 self.code_emit_buffered_line(true);
1954 return;
1955 }
1956 self.md_code_line.push(c);
1957 self.write_plain_byte(c);
1958 if c != b' ' && c != b'\t' && c != b'\r' {
1959 self.md_code_line_start = false;
1960 }
1961 }
1962
1963 fn code_emit_backtick_literals(&mut self, count: usize) {
1964 for _ in 0..count {
1965 self.code_byte(b'`');
1966 }
1967 }
1968
1969 fn code_begin(&mut self) {
1970 self.reset_color();
1971 self.md_code_block = true;
1972 self.md_inline_code = false;
1973 self.md_fence_info = true;
1974 self.md_code_line_start = true;
1975 self.md_code_in_ml_comment = false;
1976 self.md_syntax = Some(syntax_for_lang(""));
1977 self.md_fence_lang.clear();
1978 self.md_code_line_prefix = None;
1979 self.md_code_line_prefix_color = None;
1980 self.md_code_highlight_upto = false;
1981 self.md_code_line.clear();
1982 }
1983
1984 fn code_end(&mut self) {
1985 let only_space = self
1986 .md_code_line
1987 .iter()
1988 .all(|&c| c == b' ' || c == b'\t' || c == b'\r');
1989 if !self.md_code_line.is_empty() && !only_space {
1990 self.code_emit_buffered_line(false);
1991 } else {
1992 self.md_code_line.clear();
1993 }
1994 self.md_code_block = false;
1995 self.md_inline_code = false;
1996 self.md_fence_info = false;
1997 self.md_code_line_start = true;
1998 self.md_code_in_ml_comment = false;
1999 self.md_syntax = None;
2000 self.md_fence_lang.clear();
2001 self.md_code_line_prefix = None;
2002 self.md_code_line_prefix_color = None;
2003 }
2004
2005 fn markdown_clear_pending(&mut self) {
2008 self.md_pending = MdPending::None;
2009 self.md_pending_len = 0;
2010 }
2011
2012 fn markdown_emit_pending_literals(&mut self) {
2013 let c = match self.md_pending {
2014 MdPending::Star => b'*',
2015 MdPending::Backtick => b'`',
2016 MdPending::None => return,
2017 };
2018 let count = self.md_pending_len;
2019 self.markdown_clear_pending();
2020 if self.md_code_block {
2021 if c == b'`' {
2022 self.code_emit_backtick_literals(count);
2023 } else {
2024 for _ in 0..count {
2025 self.code_byte(c);
2026 }
2027 }
2028 return;
2029 }
2030 for _ in 0..count {
2031 self.write_char_raw(c);
2032 }
2033 }
2034
2035 fn markdown_commit_backticks(&mut self) {
2036 let count = self.md_pending_len;
2037 self.markdown_clear_pending();
2038 if count >= 3 {
2039 for _ in 0..count {
2040 self.write_plain_byte(b'`');
2041 }
2042 if self.md_code_block {
2043 self.code_end();
2044 } else {
2045 self.code_begin();
2046 }
2047 return;
2048 }
2049 if self.md_code_block {
2050 self.code_emit_backtick_literals(count);
2051 return;
2052 }
2053 self.md_inline_code = !self.md_inline_code;
2055 }
2056
2057 fn markdown_feed(&mut self, c: u8) {
2058 if self.md_fence_info {
2059 if c == b'\n' {
2060 if self.md_code_block {
2061 self.md_syntax = Some(syntax_for_lang(&self.md_fence_lang.clone()));
2062 }
2063 self.write_plain_byte(b'\n');
2064 self.md_fence_info = false;
2065 } else if self.md_code_block {
2066 if self.md_fence_lang.len() < FENCE_LANG_MAX
2067 && (c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'+' | b'#'))
2068 {
2069 self.md_fence_lang.push(char::from(c));
2070 }
2071 self.write_plain_byte(c);
2072 }
2073 return;
2074 }
2075
2076 if self.md_pending == MdPending::Backtick {
2077 if c == b'`' {
2078 self.md_pending_len += 1;
2079 return;
2080 }
2081 self.markdown_commit_backticks();
2082 self.markdown_feed(c);
2083 return;
2084 }
2085
2086 if self.md_pending == MdPending::Star {
2087 self.markdown_clear_pending();
2088 if !self.md_inline_code && !self.md_code_block && c == b'*' {
2089 self.md_bold = !self.md_bold;
2090 return;
2091 }
2092 if !self.md_inline_code
2093 && !self.md_code_block
2094 && (self.md_italic || !matches!(c, b' ' | b'\t' | b'\r' | b'\n'))
2095 {
2096 self.md_italic = !self.md_italic;
2097 self.markdown_feed(c);
2098 return;
2099 }
2100 self.write_char_raw(b'*');
2101 self.markdown_feed(c);
2102 return;
2103 }
2104
2105 if c == b'`' && (!self.md_code_block || self.md_code_line_start) {
2106 self.md_pending = MdPending::Backtick;
2107 self.md_pending_len = 1;
2108 return;
2109 }
2110 if self.md_code_block {
2111 self.code_byte(c);
2112 return;
2113 }
2114 if !self.md_inline_code && c == b'*' {
2115 self.md_pending = MdPending::Star;
2116 self.md_pending_len = 1;
2117 return;
2118 }
2119 self.write_char_raw(c);
2120 }
2121
2122 fn markdown_finish(&mut self) {
2123 if self.md_pending == MdPending::Backtick && self.md_pending_len >= 3 {
2126 self.markdown_commit_backticks();
2127 } else {
2128 self.markdown_emit_pending_literals();
2129 }
2130 if self.md_code_block && !self.md_code_line.is_empty() {
2131 self.code_emit_buffered_line(false);
2132 }
2133 self.md_bold = false;
2134 self.md_italic = false;
2135 self.md_inline_code = false;
2136 self.md_code_block = false;
2137 self.md_fence_info = false;
2138 self.md_code_line_start = false;
2139 self.md_code_in_ml_comment = false;
2140 self.md_syntax = None;
2141 self.md_fence_lang.clear();
2142 self.md_code_line_prefix = None;
2143 self.md_code_line_prefix_color = None;
2144 self.md_code_highlight_upto = false;
2145 self.md_code_line = Vec::new();
2146 }
2147
2148 fn write_char(&mut self, c: u8) {
2149 if !self.opts.format_markdown || self.in_think {
2150 self.markdown_emit_pending_literals();
2151 self.write_char_raw(c);
2152 return;
2153 }
2154 self.markdown_feed(c);
2155 }
2156
2157 fn process(&mut self, text: &[u8], finish: bool) {
2162 const THINK_OPEN: &[u8] = b"<think>";
2163 const THINK_CLOSE: &[u8] = b"</think>";
2164
2165 let mut buf = std::mem::take(&mut self.pending);
2166 buf.extend_from_slice(text);
2167
2168 let mut i = 0;
2169 while i < buf.len() {
2170 let cur = &buf[i..];
2171 if cur.starts_with(THINK_OPEN) {
2172 self.in_think = true;
2173 i += THINK_OPEN.len();
2174 continue;
2175 }
2176 if cur.starts_with(THINK_CLOSE) {
2177 self.in_think = false;
2178 self.reset_color();
2179 if !self.last_output_newline {
2180 self.out(b"\n");
2181 }
2182 self.out(b"\n");
2183 self.last_output_newline = true;
2184 i += THINK_CLOSE.len();
2185 continue;
2186 }
2187 if !finish
2188 && cur[0] == b'<'
2189 && (is_partial_prefix(cur, THINK_OPEN) || is_partial_prefix(cur, THINK_CLOSE))
2190 {
2191 self.pending = cur.to_vec();
2192 return;
2193 }
2194 self.write_char(cur[0]);
2195 i += 1;
2196 }
2197 }
2198}
2199
2200fn is_partial_prefix(p: &[u8], prefix: &[u8]) -> bool {
2201 p.len() < prefix.len() && prefix.starts_with(p)
2202}
2203
2204fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2205 if needle.is_empty() || haystack.len() < needle.len() {
2206 return None;
2207 }
2208 (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
2209}
2210
2211#[cfg(test)]
2216mod tests {
2217 use super::*;
2218
2219 fn renderer(opts: RenderOptions) -> TokenRenderer<Vec<u8>> {
2220 TokenRenderer::new(Vec::new(), opts)
2221 }
2222
2223 fn output(r: TokenRenderer<Vec<u8>>) -> String {
2224 String::from_utf8(r.sink).unwrap()
2225 }
2226
2227 const COLOR_MD: RenderOptions = RenderOptions {
2228 use_color: true,
2229 format_thinking: false,
2230 format_markdown: true,
2231 };
2232
2233 #[test]
2234 fn plain_text_passthrough() {
2235 let mut r = renderer(RenderOptions::default());
2236 r.write("hello world\n");
2237 assert!(r.wrote_visible_output());
2238 assert!(r.last_output_newline());
2239 r.finish();
2240 let out = output(r);
2241 assert!(out.starts_with("hello world\n"));
2242 assert!(!out.contains('\x1b'));
2243 }
2244
2245 #[test]
2246 fn model_escape_sequences_never_reach_the_terminal() {
2247 let mut r = renderer(RenderOptions::default());
2249 r.write("before\x1b]52;c;cHduZWQ=\x07after");
2250 r.plain("banner \x1b[31mred\x1b[0m\n");
2251 r.finish();
2252 let out = output(r);
2253 assert!(!out.contains('\x1b'), "escape survived: {out:?}");
2254 assert!(!out.contains('\x07'), "bell survived: {out:?}");
2255 assert!(out.contains("before") && out.contains("after"));
2257 assert!(out.contains("banner "));
2258 }
2259
2260 #[test]
2261 fn bold_and_italic_markers() {
2262 let mut r = renderer(COLOR_MD);
2263 r.write("a **bold** and *ital* b");
2264 r.finish();
2265 let out = output(r);
2266 assert!(out.contains("\x1b[1mbold"), "bold SGR missing: {out:?}");
2267 assert!(out.contains("\x1b[3mital"), "italic SGR missing: {out:?}");
2268 assert!(!out.contains('*'), "markers leaked: {out:?}");
2269 }
2270
2271 #[test]
2272 fn inline_code_cyan() {
2273 let mut r = renderer(COLOR_MD);
2274 r.write("run `ls -la` now");
2275 r.finish();
2276 let out = output(r);
2277 assert!(out.contains("\x1b[36mls -la"), "cyan inline code: {out:?}");
2278 assert!(!out.contains('`'));
2279 }
2280
2281 #[test]
2282 fn fenced_rust_block_keyword_highlighting() {
2283 let mut r = renderer(COLOR_MD);
2284 r.write("```rust\nfn main() { let x = 42; }\n```\n");
2285 r.finish();
2286 let out = output(r);
2287 assert!(out.contains("\r\x1b[0K"), "repaint missing: {out:?}");
2289 assert!(out.contains("\x1b[38;5;214mfn"), "kw1 'fn': {out:?}");
2290 assert!(out.contains("\x1b[38;5;214mlet"), "kw1 'let': {out:?}");
2291 assert!(out.contains("\x1b[38;5;203m42"), "number: {out:?}");
2292 assert!(
2294 out.contains("```") && out.contains("rust\n"),
2295 "fence line: {out:?}"
2296 );
2297 }
2298
2299 #[test]
2300 fn thinking_rendered_grey() {
2301 let mut r = renderer(RenderOptions {
2302 use_color: true,
2303 format_thinking: true,
2304 format_markdown: true,
2305 });
2306 r.write("<think>pondering</think>answer");
2307 r.finish();
2308 let out = output(r);
2309 assert!(
2310 out.contains("\x1b[3;38;5;238mpondering"),
2311 "italic grey think: {out:?}"
2312 );
2313 assert!(!out.contains("<think>"));
2314 assert!(!out.contains("</think>"));
2315 assert!(out.contains("answer"));
2316 }
2317
2318 #[test]
2319 fn partial_think_tag_held_across_writes() {
2320 let mut r = renderer(RenderOptions {
2321 use_color: false,
2322 format_thinking: true,
2323 format_markdown: false,
2324 });
2325 r.write("<thi");
2326 r.write("nk>x</thi");
2327 r.write("nk>y");
2328 r.finish();
2329 let out = output(r);
2330 assert!(!out.contains('<'));
2331 assert!(out.contains('x'));
2332 assert!(out.contains('y'));
2333 }
2334
2335 #[test]
2336 fn utf8_split_across_writes() {
2337 let mut r = renderer(RenderOptions::default());
2338 let euro = "€".as_bytes(); r.write_bytes(&euro[..1]);
2340 r.write_bytes(&euro[1..2]);
2341 r.write_bytes(&euro[2..]);
2342 r.finish();
2343 let out = output(r);
2344 assert!(out.starts_with('€'), "utf-8 reassembly failed: {out:?}");
2345 }
2346
2347 #[test]
2348 fn tail_capture_records_last_bytes() {
2349 let mut cap = TailCapture::new(8);
2350 cap.append(b"0123456789abcdef");
2351 assert_eq!(cap.total(), 16);
2352 assert_eq!(cap.len(), 8);
2353 let taken = cap.take();
2354 assert_eq!(taken, b"89abcdef");
2355 assert!(cap.is_empty());
2356
2357 let mut r = renderer(RenderOptions::default());
2359 r.set_capture(Some(TailCapture::new(64)));
2360 r.write("captured text");
2361 let mut got = r.take_capture().unwrap();
2362 assert_eq!(got.take(), b"captured text");
2363 assert!(r.sink.is_empty());
2364 }
2365
2366 #[test]
2367 fn syntax_lookup_by_lang_and_path() {
2368 assert_eq!(syntax_for_lang("rs").name(), "rust");
2369 assert_eq!(syntax_for_lang("TypeScript").name(), "javascript");
2370 assert_eq!(syntax_for_lang("nosuchlang").name(), "generic");
2371 assert_eq!(syntax_for_path("src/main.rs").name(), "rust");
2372 assert_eq!(syntax_for_path("Dockerfile").name(), "shell");
2373 assert_eq!(syntax_for_path("a/b/Makefile").name(), "shell");
2374 assert_eq!(syntax_for_path("noext").name(), "generic");
2375 }
2376}