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 syntax_style(hl: Highlight) -> &'static str {
1249 match hl {
1250 Highlight::Keyword1 | Highlight::Keyword2 => "1;",
1251 Highlight::Comment => "3;",
1252 Highlight::String | Highlight::Number | Highlight::Normal => "",
1253 }
1254}
1255
1256fn keyword_len(kw: &str) -> (usize, bool) {
1257 if let Some(stripped) = kw.strip_suffix('|') {
1258 (stripped.len(), true)
1259 } else {
1260 (kw.len(), false)
1261 }
1262}
1263
1264fn bytes_eq_ci(a: &[u8], b: &[u8]) -> bool {
1265 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_ignore_ascii_case(y))
1266}
1267
1268fn match_keyword(syn: &Syntax, rest: &[u8]) -> Option<(usize, Highlight)> {
1269 for kw in syn.keywords {
1270 let (klen, secondary) = keyword_len(kw);
1271 if rest.len() < klen {
1272 continue;
1273 }
1274 let kbytes = &kw.as_bytes()[..klen];
1275 let matched = if syn.flags & SYNTAX_CASE_INSENSITIVE != 0 {
1276 bytes_eq_ci(&rest[..klen], kbytes)
1277 } else {
1278 &rest[..klen] == kbytes
1279 };
1280 if !matched {
1281 continue;
1282 }
1283 let follow = rest.get(klen).copied().unwrap_or(0);
1284 if !syntax_separator(follow) {
1285 continue;
1286 }
1287 let hl = if secondary {
1288 Highlight::Keyword2
1289 } else {
1290 Highlight::Keyword1
1291 };
1292 return Some((klen, hl));
1293 }
1294 None
1295}
1296
1297fn number_len(rest: &[u8]) -> usize {
1298 rest.iter()
1299 .take_while(|&&c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'.' | b'+' | b'-'))
1300 .count()
1301}
1302
1303#[derive(Debug, Clone, Copy, Default)]
1309pub struct RenderOptions {
1310 pub use_color: bool,
1312 pub format_thinking: bool,
1314 pub format_markdown: bool,
1316}
1317
1318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1319enum MdPending {
1320 None,
1321 Star,
1322 Backtick,
1323}
1324
1325const UPTO_MARKER: &[u8] = b"[upto]";
1326const FENCE_LANG_MAX: usize = 31;
1327
1328const THINK_GREY: &[u8] = b"\x1b[3;38;5;238m";
1331
1332fn is_unsafe_control(b: u8) -> bool {
1335 (b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r')) || b == 0x7f
1336}
1337
1338fn without_control_bytes(bytes: &[u8]) -> Cow<'_, [u8]> {
1349 if bytes.iter().copied().any(is_unsafe_control) {
1350 Cow::Owned(
1351 bytes
1352 .iter()
1353 .copied()
1354 .filter(|&b| !is_unsafe_control(b))
1355 .collect(),
1356 )
1357 } else {
1358 Cow::Borrowed(bytes)
1359 }
1360}
1361
1362#[allow(clippy::struct_excessive_bools)]
1368pub struct TokenRenderer<W: Write> {
1369 sink: W,
1370 opts: RenderOptions,
1371 capture: Option<TailCapture>,
1372
1373 in_think: bool,
1374 color_open: bool,
1375 last_output_newline: bool,
1376 wrote_visible_output: bool,
1377
1378 md_bold: bool,
1379 md_italic: bool,
1380 md_inline_code: bool,
1381 md_code_block: bool,
1382 md_fence_info: bool,
1383 md_code_line_start: bool,
1384 md_code_in_ml_comment: bool,
1385 md_syntax_silent: bool,
1386 md_syntax_has_highlight: bool,
1387 md_pending: MdPending,
1388 md_pending_len: usize,
1389 md_syntax: Option<&'static Syntax>,
1390 md_fence_lang: String,
1391 md_code_line_prefix: Option<String>,
1392 md_code_line_prefix_color: Option<String>,
1393 md_code_highlight_upto: bool,
1394 md_code_line: Vec<u8>,
1395
1396 pending: Vec<u8>,
1397 utf8_pending: [u8; 4],
1398 utf8_pending_len: usize,
1399 utf8_pending_need: usize,
1400}
1401
1402impl<W: Write> fmt::Debug for TokenRenderer<W> {
1403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1404 f.debug_struct("TokenRenderer")
1405 .field("opts", &self.opts)
1406 .field("in_think", &self.in_think)
1407 .field("md_code_block", &self.md_code_block)
1408 .field("wrote_visible_output", &self.wrote_visible_output)
1409 .field("last_output_newline", &self.last_output_newline)
1410 .finish_non_exhaustive()
1411 }
1412}
1413
1414impl<W: Write> TokenRenderer<W> {
1415 pub fn sink_mut(&mut self) -> &mut W {
1418 &mut self.sink
1419 }
1420
1421 pub fn new(sink: W, opts: RenderOptions) -> Self {
1423 Self {
1424 sink,
1425 opts,
1426 capture: None,
1427 in_think: false,
1428 color_open: false,
1429 last_output_newline: false,
1430 wrote_visible_output: false,
1431 md_bold: false,
1432 md_italic: false,
1433 md_inline_code: false,
1434 md_code_block: false,
1435 md_fence_info: false,
1436 md_code_line_start: false,
1437 md_code_in_ml_comment: false,
1438 md_syntax_silent: false,
1439 md_syntax_has_highlight: false,
1440 md_pending: MdPending::None,
1441 md_pending_len: 0,
1442 md_syntax: None,
1443 md_fence_lang: String::new(),
1444 md_code_line_prefix: None,
1445 md_code_line_prefix_color: None,
1446 md_code_highlight_upto: false,
1447 md_code_line: Vec::new(),
1448 pending: Vec::new(),
1449 utf8_pending: [0; 4],
1450 utf8_pending_len: 0,
1451 utf8_pending_need: 0,
1452 }
1453 }
1454
1455 pub fn set_capture(&mut self, capture: Option<TailCapture>) {
1457 self.capture = capture;
1458 }
1459
1460 pub fn take_capture(&mut self) -> Option<TailCapture> {
1462 self.capture.take()
1463 }
1464
1465 #[must_use]
1467 pub fn wrote_visible_output(&self) -> bool {
1468 self.wrote_visible_output
1469 }
1470
1471 #[must_use]
1473 pub fn last_output_newline(&self) -> bool {
1474 self.last_output_newline
1475 }
1476
1477 pub fn set_in_think(&mut self, in_think: bool) {
1479 self.in_think = in_think;
1480 }
1481
1482 pub fn write(&mut self, text: &str) {
1484 self.write_bytes(text.as_bytes());
1485 }
1486
1487 pub fn write_bytes(&mut self, bytes: &[u8]) {
1489 let bytes = without_control_bytes(bytes);
1490 if self.opts.format_thinking {
1491 self.process(&bytes, false);
1492 } else {
1493 for &b in bytes.iter() {
1494 self.write_char(b);
1495 }
1496 }
1497 }
1498
1499 pub fn finish(&mut self) {
1501 if self.opts.format_thinking {
1502 self.process(&[], true);
1503 }
1504 self.markdown_finish();
1505 self.flush_utf8();
1506 self.reset_color();
1507 if self.wrote_visible_output {
1508 if !self.last_output_newline {
1509 self.out(b"\n");
1510 }
1511 self.out(b"\n");
1512 self.last_output_newline = true;
1513 }
1514 let _ = self.sink.flush();
1515 }
1516
1517 pub fn into_sink(self) -> W {
1522 self.sink
1523 }
1524
1525 pub fn color(&mut self, seq: &str) {
1527 self.markdown_emit_pending_literals();
1528 self.flush_utf8();
1529 let reset = seq.is_empty() || seq == "\x1b[0m";
1530 if self.opts.use_color && !seq.is_empty() {
1531 self.out_str(seq);
1532 }
1533 self.color_open = self.opts.use_color && !reset;
1534 }
1535
1536 pub fn plain(&mut self, s: &str) {
1538 let s = without_control_bytes(s.as_bytes());
1539 self.markdown_emit_pending_literals();
1540 self.flush_utf8();
1541 self.out(&s);
1542 if !s.is_empty() {
1543 self.wrote_visible_output = true;
1544 self.last_output_newline = s.last() == Some(&b'\n');
1545 }
1546 }
1547
1548 pub fn restore_text_attrs(&mut self) {
1550 if !self.opts.use_color || !self.color_open || !self.has_text_attrs() {
1551 return;
1552 }
1553 self.set_text_attrs();
1554 }
1555
1556 pub fn code_stream_begin(&mut self, syntax: &'static Syntax) {
1558 self.reset_color();
1559 self.md_code_block = true;
1560 self.md_inline_code = false;
1561 self.md_fence_info = false;
1562 self.md_code_line_start = true;
1563 self.md_code_in_ml_comment = false;
1564 self.md_syntax = Some(syntax);
1565 self.md_fence_lang.clear();
1566 self.md_code_line_prefix = None;
1567 self.md_code_line_prefix_color = None;
1568 self.md_code_highlight_upto = false;
1569 self.md_code_line.clear();
1570 }
1571
1572 pub fn code_stream_set_prefix(&mut self, prefix: Option<&str>, color: Option<&str>) {
1574 self.md_code_line_prefix = prefix.map(str::to_owned);
1575 self.md_code_line_prefix_color = color.map(str::to_owned);
1576 }
1577
1578 pub fn code_stream_set_upto_marker(&mut self, enabled: bool) {
1580 self.md_code_highlight_upto = enabled;
1581 }
1582
1583 pub fn code_stream_end(&mut self) {
1585 self.code_end();
1586 }
1587
1588 fn out(&mut self, s: &[u8]) {
1591 if let Some(c) = self.capture.as_mut() {
1592 c.append(s);
1593 } else {
1594 let _ = self.sink.write_all(s);
1595 }
1596 }
1597
1598 fn out_str(&mut self, s: &str) {
1599 self.out(s.as_bytes());
1600 }
1601
1602 fn set_grey(&mut self) {
1603 if self.opts.use_color {
1604 self.out(THINK_GREY);
1607 }
1608 }
1609
1610 fn reset_color(&mut self) {
1611 if self.opts.use_color {
1612 self.out(b"\x1b[0m");
1613 }
1614 self.color_open = false;
1615 }
1616
1617 fn has_text_attrs(&self) -> bool {
1618 self.in_think || self.md_bold || self.md_italic || self.md_inline_code || self.md_code_block
1619 }
1620
1621 fn set_text_attrs(&mut self) {
1622 if !self.opts.use_color {
1623 return;
1624 }
1625 if self.in_think {
1626 self.set_grey();
1627 return;
1628 }
1629 if self.md_code_block {
1630 self.out(b"\x1b[38;5;75m");
1631 return;
1632 } else if self.md_inline_code {
1633 self.out(b"\x1b[36m");
1634 }
1635 if self.md_bold {
1636 self.out(b"\x1b[1m");
1637 }
1638 if self.md_italic {
1639 self.out(b"\x1b[3m");
1640 }
1641 }
1642
1643 fn write_complete_char_raw(&mut self, s: &[u8]) {
1644 let styled = self.opts.use_color && self.has_text_attrs();
1645 if styled && !self.color_open {
1646 self.set_text_attrs();
1647 self.color_open = true;
1648 } else if !styled && self.color_open {
1649 self.reset_color();
1650 }
1651 self.out(s);
1652 if !s.is_empty() {
1653 self.wrote_visible_output = true;
1654 }
1655 self.last_output_newline = s == b"\n";
1656 }
1657
1658 fn flush_utf8(&mut self) {
1659 if self.utf8_pending_len == 0 {
1660 return;
1661 }
1662 let buf: [u8; 4] = self.utf8_pending;
1663 let len = self.utf8_pending_len;
1664 self.write_complete_char_raw(&buf[..len]);
1665 self.utf8_pending_len = 0;
1666 self.utf8_pending_need = 0;
1667 }
1668
1669 fn utf8_need(c: u8) -> usize {
1670 match c {
1671 0xc2..=0xdf => 2,
1672 0xe0..=0xef => 3,
1673 0xf0..=0xf4 => 4,
1674 _ => 1,
1675 }
1676 }
1677
1678 fn write_char_raw(&mut self, c: u8) {
1679 if self.utf8_pending_len > 0 {
1680 if c & 0xc0 == 0x80 && self.utf8_pending_len < self.utf8_pending.len() {
1681 self.utf8_pending[self.utf8_pending_len] = c;
1682 self.utf8_pending_len += 1;
1683 if self.utf8_pending_len == self.utf8_pending_need {
1684 self.flush_utf8();
1685 }
1686 return;
1687 }
1688 self.flush_utf8();
1689 }
1690
1691 let need = Self::utf8_need(c);
1692 if need == 1 {
1693 self.write_complete_char_raw(&[c]);
1694 return;
1695 }
1696 self.utf8_pending[0] = c;
1697 self.utf8_pending_len = 1;
1698 self.utf8_pending_need = need;
1699 }
1700
1701 fn write_plain_byte(&mut self, c: u8) {
1703 let (bold, italic, inline, block) = (
1704 self.md_bold,
1705 self.md_italic,
1706 self.md_inline_code,
1707 self.md_code_block,
1708 );
1709 self.md_bold = false;
1710 self.md_italic = false;
1711 self.md_inline_code = false;
1712 self.md_code_block = false;
1713 self.write_char_raw(c);
1714 self.md_bold = bold;
1715 self.md_italic = italic;
1716 self.md_inline_code = inline;
1717 self.md_code_block = block;
1718 }
1719
1720 fn syntax_write(&mut self, hl: Highlight, s: &[u8]) {
1723 if s.is_empty() {
1724 return;
1725 }
1726 if hl != Highlight::Normal {
1727 self.md_syntax_has_highlight = true;
1728 }
1729 if self.md_syntax_silent {
1730 return;
1731 }
1732 if self.opts.use_color && hl != Highlight::Normal {
1733 let seq = format!("\x1b[{}38;5;{}m", syntax_style(hl), syntax_color(hl));
1734 self.out_str(&seq);
1735 }
1736 self.out(s);
1737 if self.opts.use_color && hl != Highlight::Normal {
1738 self.out(b"\x1b[0m");
1739 }
1740 self.wrote_visible_output = true;
1741 self.last_output_newline = false;
1742 }
1743
1744 fn syntax_write_upto_marker(&mut self) {
1745 self.md_syntax_has_highlight = true;
1746 if self.md_syntax_silent {
1747 return;
1748 }
1749 if self.opts.use_color {
1750 self.out(b"\x1b[38;5;244m[");
1751 self.out(b"\x1b[1;38;5;177mupto");
1752 self.out(b"\x1b[38;5;244m]\x1b[0m");
1753 } else {
1754 self.out(UPTO_MARKER);
1755 }
1756 self.wrote_visible_output = true;
1757 self.last_output_newline = false;
1758 }
1759
1760 #[allow(clippy::too_many_lines)]
1761 fn syntax_emit_line(&mut self, line: &[u8]) {
1762 let syn = self.md_syntax.unwrap_or_else(|| syntax_for_lang(""));
1763 let mut i = 0;
1764 let end = line.len();
1765 let mut prev_sep = true;
1766 let mut prev_hl = Highlight::Normal;
1767
1768 while i < end {
1769 let rest = &line[i..];
1770
1771 if self.md_code_highlight_upto && rest.starts_with(UPTO_MARKER) {
1772 self.syntax_write_upto_marker();
1773 i += UPTO_MARKER.len();
1774 prev_sep = true;
1775 prev_hl = Highlight::Normal;
1776 continue;
1777 }
1778
1779 if self.md_code_in_ml_comment {
1780 if let Some(mce) = syn.multiline_end
1781 && let Some(pos) = find_sub(rest, mce.as_bytes())
1782 {
1783 let take = pos + mce.len();
1784 let seg = &line[i..i + take];
1785 self.syntax_write(Highlight::Comment, seg);
1786 i += take;
1787 self.md_code_in_ml_comment = false;
1788 prev_sep = true;
1789 prev_hl = Highlight::Comment;
1790 continue;
1791 }
1792 let seg = &line[i..];
1793 self.syntax_write(Highlight::Comment, seg);
1794 return;
1795 }
1796
1797 if syn
1798 .singleline_comments
1799 .iter()
1800 .any(|m| rest.starts_with(m.as_bytes()))
1801 {
1802 let seg = &line[i..];
1803 self.syntax_write(Highlight::Comment, seg);
1804 return;
1805 }
1806
1807 if let (Some(mls), Some(mle)) = (syn.multiline_start, syn.multiline_end)
1808 && rest.starts_with(mls.as_bytes())
1809 {
1810 let body = &rest[mls.len()..];
1811 let take = if let Some(pos) = find_sub(body, mle.as_bytes()) {
1812 mls.len() + pos + mle.len()
1813 } else {
1814 self.md_code_in_ml_comment = true;
1815 rest.len()
1816 };
1817 let seg = &line[i..i + take];
1818 self.syntax_write(Highlight::Comment, seg);
1819 i += take;
1820 prev_sep = false;
1821 prev_hl = Highlight::Comment;
1822 continue;
1823 }
1824
1825 let c = rest[0];
1826 if syn.flags & SYNTAX_STRINGS != 0
1827 && (c == b'"'
1828 || c == b'\''
1829 || (syn.flags & SYNTAX_BACKTICK_STRINGS != 0 && c == b'`'))
1830 {
1831 let quote = c;
1832 let mut q = 1;
1833 while q < rest.len() {
1834 if rest[q] == b'\\' && q + 1 < rest.len() {
1835 q += 2;
1836 continue;
1837 }
1838 q += 1;
1839 if rest[q - 1] == quote {
1840 break;
1841 }
1842 }
1843 let seg = &line[i..i + q];
1844 self.syntax_write(Highlight::String, seg);
1845 i += q;
1846 prev_sep = false;
1847 prev_hl = Highlight::String;
1848 continue;
1849 }
1850
1851 let number_start = c.is_ascii_digit() && (prev_sep || prev_hl == Highlight::Number)
1852 || (c == b'.' && i > 0 && prev_hl == Highlight::Number);
1853 if syn.flags & SYNTAX_NUMBERS != 0 && number_start {
1854 let nlen = number_len(rest);
1855 let seg = &line[i..i + nlen];
1856 self.syntax_write(Highlight::Number, seg);
1857 i += nlen;
1858 prev_sep = false;
1859 prev_hl = Highlight::Number;
1860 continue;
1861 }
1862
1863 if prev_sep && let Some((klen, khl)) = match_keyword(syn, rest) {
1864 let seg = &line[i..i + klen];
1865 self.syntax_write(khl, seg);
1866 i += klen;
1867 prev_sep = false;
1868 prev_hl = khl;
1869 continue;
1870 }
1871
1872 let seg = &line[i..=i];
1873 self.syntax_write(Highlight::Normal, seg);
1874 prev_sep = syntax_separator(c);
1875 prev_hl = Highlight::Normal;
1876 i += 1;
1877 }
1878 }
1879
1880 fn terminal_cols() -> usize {
1883 80
1886 }
1887
1888 fn code_line_can_repaint(&self) -> bool {
1889 if !self.opts.use_color || self.capture.is_some() || self.md_code_line.is_empty() {
1890 return false;
1891 }
1892 let cols = Self::terminal_cols();
1893 let prefix_len = self.md_code_line_prefix.as_ref().map_or(0, String::len);
1894 if cols <= 1 || prefix_len + self.md_code_line.len() >= cols {
1895 return false;
1896 }
1897 self.md_code_line
1898 .iter()
1899 .all(|&c| c == b'\r' || (0x20..0x80).contains(&c) && c != 0x1b)
1900 }
1901
1902 fn code_write_line_prefix(&mut self) {
1903 let Some(prefix) = self.md_code_line_prefix.clone() else {
1904 return;
1905 };
1906 let color = self.md_code_line_prefix_color.clone();
1907 if self.opts.use_color
1908 && let Some(col) = &color
1909 {
1910 self.out_str(col);
1911 }
1912 self.out_str(&prefix);
1913 if self.opts.use_color && color.is_some() {
1914 self.out(b"\x1b[0m");
1915 }
1916 self.color_open = false;
1917 }
1918
1919 fn code_scan_line(&mut self) -> (bool, bool) {
1922 let old_silent = self.md_syntax_silent;
1923 let old_highlight = self.md_syntax_has_highlight;
1924 let old_ml = self.md_code_in_ml_comment;
1925
1926 self.md_syntax_silent = true;
1927 self.md_syntax_has_highlight = false;
1928 let line = std::mem::take(&mut self.md_code_line);
1929 self.syntax_emit_line(&line);
1930 self.md_code_line = line;
1931 let changed = self.md_syntax_has_highlight;
1932 let final_ml = self.md_code_in_ml_comment;
1933
1934 self.md_code_in_ml_comment = old_ml;
1935 self.md_syntax_silent = old_silent;
1936 self.md_syntax_has_highlight = old_highlight;
1937 (changed, final_ml)
1938 }
1939
1940 fn code_emit_buffered_line(&mut self, with_newline: bool) {
1941 let (changed, final_ml) = self.code_scan_line();
1942 let repaint = changed && self.code_line_can_repaint();
1943 if repaint {
1944 self.reset_color();
1945 self.out(b"\r\x1b[0K");
1946 self.code_write_line_prefix();
1947 let line = std::mem::take(&mut self.md_code_line);
1948 self.syntax_emit_line(&line);
1949 self.md_code_line = line;
1950 } else {
1951 self.md_code_in_ml_comment = final_ml;
1952 }
1953 self.md_code_line.clear();
1954 if with_newline {
1955 self.write_plain_byte(b'\n');
1956 self.wrote_visible_output = true;
1957 self.last_output_newline = true;
1958 self.md_code_line_start = true;
1959 }
1960 }
1961
1962 fn code_byte(&mut self, c: u8) {
1963 if c == b'\n' {
1964 self.code_emit_buffered_line(true);
1965 return;
1966 }
1967 self.md_code_line.push(c);
1968 self.write_plain_byte(c);
1969 if c != b' ' && c != b'\t' && c != b'\r' {
1970 self.md_code_line_start = false;
1971 }
1972 }
1973
1974 fn code_emit_backtick_literals(&mut self, count: usize) {
1975 for _ in 0..count {
1976 self.code_byte(b'`');
1977 }
1978 }
1979
1980 fn code_begin(&mut self) {
1981 self.reset_color();
1982 self.md_code_block = true;
1983 self.md_inline_code = false;
1984 self.md_fence_info = true;
1985 self.md_code_line_start = true;
1986 self.md_code_in_ml_comment = false;
1987 self.md_syntax = Some(syntax_for_lang(""));
1988 self.md_fence_lang.clear();
1989 self.md_code_line_prefix = None;
1990 self.md_code_line_prefix_color = None;
1991 self.md_code_highlight_upto = false;
1992 self.md_code_line.clear();
1993 }
1994
1995 fn code_end(&mut self) {
1996 let only_space = self
1997 .md_code_line
1998 .iter()
1999 .all(|&c| c == b' ' || c == b'\t' || c == b'\r');
2000 if !self.md_code_line.is_empty() && !only_space {
2001 self.code_emit_buffered_line(false);
2002 } else {
2003 self.md_code_line.clear();
2004 }
2005 self.md_code_block = false;
2006 self.md_inline_code = false;
2007 self.md_fence_info = false;
2008 self.md_code_line_start = true;
2009 self.md_code_in_ml_comment = false;
2010 self.md_syntax = None;
2011 self.md_fence_lang.clear();
2012 self.md_code_line_prefix = None;
2013 self.md_code_line_prefix_color = None;
2014 }
2015
2016 fn markdown_clear_pending(&mut self) {
2019 self.md_pending = MdPending::None;
2020 self.md_pending_len = 0;
2021 }
2022
2023 fn markdown_emit_pending_literals(&mut self) {
2024 let c = match self.md_pending {
2025 MdPending::Star => b'*',
2026 MdPending::Backtick => b'`',
2027 MdPending::None => return,
2028 };
2029 let count = self.md_pending_len;
2030 self.markdown_clear_pending();
2031 if self.md_code_block {
2032 if c == b'`' {
2033 self.code_emit_backtick_literals(count);
2034 } else {
2035 for _ in 0..count {
2036 self.code_byte(c);
2037 }
2038 }
2039 return;
2040 }
2041 for _ in 0..count {
2042 self.write_char_raw(c);
2043 }
2044 }
2045
2046 fn markdown_commit_backticks(&mut self) {
2047 let count = self.md_pending_len;
2048 self.markdown_clear_pending();
2049 if count >= 3 {
2050 for _ in 0..count {
2051 self.write_plain_byte(b'`');
2052 }
2053 if self.md_code_block {
2054 self.code_end();
2055 } else {
2056 self.code_begin();
2057 }
2058 return;
2059 }
2060 if self.md_code_block {
2061 self.code_emit_backtick_literals(count);
2062 return;
2063 }
2064 self.md_inline_code = !self.md_inline_code;
2066 }
2067
2068 fn markdown_feed(&mut self, c: u8) {
2069 if self.md_fence_info {
2070 if c == b'\n' {
2071 if self.md_code_block {
2072 self.md_syntax = Some(syntax_for_lang(&self.md_fence_lang.clone()));
2073 }
2074 self.write_plain_byte(b'\n');
2075 self.md_fence_info = false;
2076 } else if self.md_code_block {
2077 if self.md_fence_lang.len() < FENCE_LANG_MAX
2078 && (c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'+' | b'#'))
2079 {
2080 self.md_fence_lang.push(char::from(c));
2081 }
2082 self.write_plain_byte(c);
2083 }
2084 return;
2085 }
2086
2087 if self.md_pending == MdPending::Backtick {
2088 if c == b'`' {
2089 self.md_pending_len += 1;
2090 return;
2091 }
2092 self.markdown_commit_backticks();
2093 self.markdown_feed(c);
2094 return;
2095 }
2096
2097 if self.md_pending == MdPending::Star {
2098 self.markdown_clear_pending();
2099 if !self.md_inline_code && !self.md_code_block && c == b'*' {
2100 self.md_bold = !self.md_bold;
2101 return;
2102 }
2103 if !self.md_inline_code
2104 && !self.md_code_block
2105 && (self.md_italic || !matches!(c, b' ' | b'\t' | b'\r' | b'\n'))
2106 {
2107 self.md_italic = !self.md_italic;
2108 self.markdown_feed(c);
2109 return;
2110 }
2111 self.write_char_raw(b'*');
2112 self.markdown_feed(c);
2113 return;
2114 }
2115
2116 if c == b'`' && (!self.md_code_block || self.md_code_line_start) {
2117 self.md_pending = MdPending::Backtick;
2118 self.md_pending_len = 1;
2119 return;
2120 }
2121 if self.md_code_block {
2122 self.code_byte(c);
2123 return;
2124 }
2125 if !self.md_inline_code && c == b'*' {
2126 self.md_pending = MdPending::Star;
2127 self.md_pending_len = 1;
2128 return;
2129 }
2130 self.write_char_raw(c);
2131 }
2132
2133 fn markdown_finish(&mut self) {
2134 if self.md_pending == MdPending::Backtick && self.md_pending_len >= 3 {
2137 self.markdown_commit_backticks();
2138 } else {
2139 self.markdown_emit_pending_literals();
2140 }
2141 if self.md_code_block && !self.md_code_line.is_empty() {
2142 self.code_emit_buffered_line(false);
2143 }
2144 self.md_bold = false;
2145 self.md_italic = false;
2146 self.md_inline_code = false;
2147 self.md_code_block = false;
2148 self.md_fence_info = false;
2149 self.md_code_line_start = false;
2150 self.md_code_in_ml_comment = false;
2151 self.md_syntax = None;
2152 self.md_fence_lang.clear();
2153 self.md_code_line_prefix = None;
2154 self.md_code_line_prefix_color = None;
2155 self.md_code_highlight_upto = false;
2156 self.md_code_line = Vec::new();
2157 }
2158
2159 fn write_char(&mut self, c: u8) {
2160 if !self.opts.format_markdown || self.in_think {
2161 self.markdown_emit_pending_literals();
2162 self.write_char_raw(c);
2163 return;
2164 }
2165 self.markdown_feed(c);
2166 }
2167
2168 fn process(&mut self, text: &[u8], finish: bool) {
2173 const THINK_OPEN: &[u8] = b"<think>";
2174 const THINK_CLOSE: &[u8] = b"</think>";
2175
2176 let mut buf = std::mem::take(&mut self.pending);
2177 buf.extend_from_slice(text);
2178
2179 let mut i = 0;
2180 while i < buf.len() {
2181 let cur = &buf[i..];
2182 if cur.starts_with(THINK_OPEN) {
2183 self.in_think = true;
2184 i += THINK_OPEN.len();
2185 continue;
2186 }
2187 if cur.starts_with(THINK_CLOSE) {
2188 self.in_think = false;
2189 self.reset_color();
2190 if !self.last_output_newline {
2191 self.out(b"\n");
2192 }
2193 self.out(b"\n");
2194 self.last_output_newline = true;
2195 i += THINK_CLOSE.len();
2196 continue;
2197 }
2198 if !finish
2199 && cur[0] == b'<'
2200 && (is_partial_prefix(cur, THINK_OPEN) || is_partial_prefix(cur, THINK_CLOSE))
2201 {
2202 self.pending = cur.to_vec();
2203 return;
2204 }
2205 self.write_char(cur[0]);
2206 i += 1;
2207 }
2208 }
2209}
2210
2211fn is_partial_prefix(p: &[u8], prefix: &[u8]) -> bool {
2212 p.len() < prefix.len() && prefix.starts_with(p)
2213}
2214
2215fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2216 if needle.is_empty() || haystack.len() < needle.len() {
2217 return None;
2218 }
2219 (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
2220}
2221
2222#[cfg(test)]
2227mod tests {
2228 use super::*;
2229
2230 fn renderer(opts: RenderOptions) -> TokenRenderer<Vec<u8>> {
2231 TokenRenderer::new(Vec::new(), opts)
2232 }
2233
2234 fn output(r: TokenRenderer<Vec<u8>>) -> String {
2235 String::from_utf8(r.sink).unwrap()
2236 }
2237
2238 const COLOR_MD: RenderOptions = RenderOptions {
2239 use_color: true,
2240 format_thinking: false,
2241 format_markdown: true,
2242 };
2243
2244 #[test]
2245 fn plain_text_passthrough() {
2246 let mut r = renderer(RenderOptions::default());
2247 r.write("hello world\n");
2248 assert!(r.wrote_visible_output());
2249 assert!(r.last_output_newline());
2250 r.finish();
2251 let out = output(r);
2252 assert!(out.starts_with("hello world\n"));
2253 assert!(!out.contains('\x1b'));
2254 }
2255
2256 #[test]
2257 fn model_escape_sequences_never_reach_the_terminal() {
2258 let mut r = renderer(RenderOptions::default());
2260 r.write("before\x1b]52;c;cHduZWQ=\x07after");
2261 r.plain("banner \x1b[31mred\x1b[0m\n");
2262 r.finish();
2263 let out = output(r);
2264 assert!(!out.contains('\x1b'), "escape survived: {out:?}");
2265 assert!(!out.contains('\x07'), "bell survived: {out:?}");
2266 assert!(out.contains("before") && out.contains("after"));
2268 assert!(out.contains("banner "));
2269 }
2270
2271 #[test]
2272 fn bold_and_italic_markers() {
2273 let mut r = renderer(COLOR_MD);
2274 r.write("a **bold** and *ital* b");
2275 r.finish();
2276 let out = output(r);
2277 assert!(out.contains("\x1b[1mbold"), "bold SGR missing: {out:?}");
2278 assert!(out.contains("\x1b[3mital"), "italic SGR missing: {out:?}");
2279 assert!(!out.contains('*'), "markers leaked: {out:?}");
2280 }
2281
2282 #[test]
2283 fn inline_code_cyan() {
2284 let mut r = renderer(COLOR_MD);
2285 r.write("run `ls -la` now");
2286 r.finish();
2287 let out = output(r);
2288 assert!(out.contains("\x1b[36mls -la"), "cyan inline code: {out:?}");
2289 assert!(!out.contains('`'));
2290 }
2291
2292 #[test]
2293 fn fenced_rust_block_keyword_highlighting() {
2294 let mut r = renderer(COLOR_MD);
2295 r.write("```rust\nfn main() { let x = 42; }\n```\n");
2296 r.finish();
2297 let out = output(r);
2298 assert!(out.contains("\r\x1b[0K"), "repaint missing: {out:?}");
2300 assert!(out.contains("\x1b[1;38;5;214mfn"), "kw1 'fn' bold: {out:?}");
2301 assert!(out.contains("\x1b[1;38;5;214mlet"), "kw1 'let' bold: {out:?}");
2302 assert!(out.contains("\x1b[38;5;203m42"), "number: {out:?}");
2303 assert!(
2305 out.contains("```") && out.contains("rust\n"),
2306 "fence line: {out:?}"
2307 );
2308 }
2309
2310 #[test]
2311 fn fenced_block_bolds_keywords_and_italicizes_comments() {
2312 let mut r = renderer(COLOR_MD);
2313 r.write("```rust\nfn main() {} // note\n```\n");
2314 r.finish();
2315 let out = output(r);
2316 assert!(
2318 out.contains("\x1b[1;38;5;214mfn"),
2319 "keyword must be bold: {out:?}"
2320 );
2321 assert!(
2323 out.contains("\x1b[3;38;5;244m"),
2324 "comment must be italic: {out:?}"
2325 );
2326 }
2327
2328 #[test]
2329 fn thinking_rendered_grey() {
2330 let mut r = renderer(RenderOptions {
2331 use_color: true,
2332 format_thinking: true,
2333 format_markdown: true,
2334 });
2335 r.write("<think>pondering</think>answer");
2336 r.finish();
2337 let out = output(r);
2338 assert!(
2339 out.contains("\x1b[3;38;5;238mpondering"),
2340 "italic grey think: {out:?}"
2341 );
2342 assert!(!out.contains("<think>"));
2343 assert!(!out.contains("</think>"));
2344 assert!(out.contains("answer"));
2345 }
2346
2347 #[test]
2348 fn partial_think_tag_held_across_writes() {
2349 let mut r = renderer(RenderOptions {
2350 use_color: false,
2351 format_thinking: true,
2352 format_markdown: false,
2353 });
2354 r.write("<thi");
2355 r.write("nk>x</thi");
2356 r.write("nk>y");
2357 r.finish();
2358 let out = output(r);
2359 assert!(!out.contains('<'));
2360 assert!(out.contains('x'));
2361 assert!(out.contains('y'));
2362 }
2363
2364 #[test]
2365 fn utf8_split_across_writes() {
2366 let mut r = renderer(RenderOptions::default());
2367 let euro = "€".as_bytes(); r.write_bytes(&euro[..1]);
2369 r.write_bytes(&euro[1..2]);
2370 r.write_bytes(&euro[2..]);
2371 r.finish();
2372 let out = output(r);
2373 assert!(out.starts_with('€'), "utf-8 reassembly failed: {out:?}");
2374 }
2375
2376 #[test]
2377 fn tail_capture_records_last_bytes() {
2378 let mut cap = TailCapture::new(8);
2379 cap.append(b"0123456789abcdef");
2380 assert_eq!(cap.total(), 16);
2381 assert_eq!(cap.len(), 8);
2382 let taken = cap.take();
2383 assert_eq!(taken, b"89abcdef");
2384 assert!(cap.is_empty());
2385
2386 let mut r = renderer(RenderOptions::default());
2388 r.set_capture(Some(TailCapture::new(64)));
2389 r.write("captured text");
2390 let mut got = r.take_capture().unwrap();
2391 assert_eq!(got.take(), b"captured text");
2392 assert!(r.sink.is_empty());
2393 }
2394
2395 #[test]
2396 fn syntax_lookup_by_lang_and_path() {
2397 assert_eq!(syntax_for_lang("rs").name(), "rust");
2398 assert_eq!(syntax_for_lang("TypeScript").name(), "javascript");
2399 assert_eq!(syntax_for_lang("nosuchlang").name(), "generic");
2400 assert_eq!(syntax_for_path("src/main.rs").name(), "rust");
2401 assert_eq!(syntax_for_path("Dockerfile").name(), "shell");
2402 assert_eq!(syntax_for_path("a/b/Makefile").name(), "shell");
2403 assert_eq!(syntax_for_path("noext").name(), "generic");
2404 }
2405}