lognal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/core/filter.d.ts +29 -0
  4. package/dist/core/filter.js +82 -0
  5. package/dist/core/layout/entry-lines.d.ts +16 -0
  6. package/dist/core/layout/entry-lines.js +120 -0
  7. package/dist/core/layout/layout.d.ts +152 -0
  8. package/dist/core/layout/layout.js +784 -0
  9. package/dist/core/layout/row-index.d.ts +33 -0
  10. package/dist/core/layout/row-index.js +99 -0
  11. package/dist/core/layout/types.d.ts +128 -0
  12. package/dist/core/layout/types.js +8 -0
  13. package/dist/core/store.d.ts +96 -0
  14. package/dist/core/store.js +204 -0
  15. package/dist/core/text/ansi.d.ts +20 -0
  16. package/dist/core/text/ansi.js +187 -0
  17. package/dist/core/text/graphemes.d.ts +17 -0
  18. package/dist/core/text/graphemes.js +70 -0
  19. package/dist/core/text/line-splitter.d.ts +18 -0
  20. package/dist/core/text/line-splitter.js +56 -0
  21. package/dist/core/text/measure.d.ts +10 -0
  22. package/dist/core/text/measure.js +38 -0
  23. package/dist/core/text/shape.d.ts +24 -0
  24. package/dist/core/text/shape.js +220 -0
  25. package/dist/core/text/unicode-width-data.d.ts +54 -0
  26. package/dist/core/text/unicode-width-data.js +227 -0
  27. package/dist/core/text/width.d.ts +20 -0
  28. package/dist/core/text/width.js +157 -0
  29. package/dist/core/text/wrap.d.ts +14 -0
  30. package/dist/core/text/wrap.js +94 -0
  31. package/dist/core/time.d.ts +10 -0
  32. package/dist/core/time.js +24 -0
  33. package/dist/core/types.d.ts +131 -0
  34. package/dist/core/types.js +9 -0
  35. package/dist/core/value/preview.d.ts +16 -0
  36. package/dist/core/value/preview.js +207 -0
  37. package/dist/index.d.ts +27 -0
  38. package/dist/index.js +24 -0
  39. package/dist/lognal.css +589 -0
  40. package/dist/react/LogViewer.d.ts +32 -0
  41. package/dist/react/LogViewer.js +183 -0
  42. package/dist/react/index.d.ts +1 -0
  43. package/dist/react/index.js +2 -0
  44. package/dist/renderer/canvas/canvas-renderer.d.ts +45 -0
  45. package/dist/renderer/canvas/canvas-renderer.js +464 -0
  46. package/dist/renderer/canvas/palette.d.ts +6 -0
  47. package/dist/renderer/canvas/palette.js +25 -0
  48. package/dist/renderer/theme.d.ts +3 -0
  49. package/dist/renderer/theme.js +52 -0
  50. package/dist/renderer/types.d.ts +82 -0
  51. package/dist/renderer/types.js +1 -0
  52. package/dist/sources/console/format.d.ts +32 -0
  53. package/dist/sources/console/format.js +189 -0
  54. package/dist/sources/console/hook.d.ts +27 -0
  55. package/dist/sources/console/hook.js +94 -0
  56. package/dist/sources/console/recorder.d.ts +41 -0
  57. package/dist/sources/console/recorder.js +239 -0
  58. package/dist/sources/console/snapshot.d.ts +22 -0
  59. package/dist/sources/console/snapshot.js +547 -0
  60. package/dist/sources/console/table.d.ts +9 -0
  61. package/dist/sources/console/table.js +87 -0
  62. package/dist/sources/text/encoding.d.ts +11 -0
  63. package/dist/sources/text/encoding.js +59 -0
  64. package/dist/sources/text/follow-file.d.ts +49 -0
  65. package/dist/sources/text/follow-file.js +109 -0
  66. package/dist/sources/text/read-file.d.ts +63 -0
  67. package/dist/sources/text/read-file.js +82 -0
  68. package/dist/viewer/icons.d.ts +14 -0
  69. package/dist/viewer/icons.js +30 -0
  70. package/dist/viewer/input-line.d.ts +46 -0
  71. package/dist/viewer/input-line.js +144 -0
  72. package/dist/viewer/labels.d.ts +35 -0
  73. package/dist/viewer/labels.js +61 -0
  74. package/dist/viewer/scrollbar.d.ts +27 -0
  75. package/dist/viewer/scrollbar.js +143 -0
  76. package/dist/viewer/theme.d.ts +15 -0
  77. package/dist/viewer/theme.js +105 -0
  78. package/dist/viewer/viewer.d.ts +217 -0
  79. package/dist/viewer/viewer.js +1201 -0
  80. package/package.json +92 -0
@@ -0,0 +1,187 @@
1
+ const ESCAPE = '\x1b';
2
+ const BELL = '\x07';
3
+ /**
4
+ * Turns text with ANSI escape codes into styled parts.
5
+ *
6
+ * Select Graphic Rendition codes (colors, bold, italic, underline and so on) become styles.
7
+ * Every other escape sequence, such as cursor movement or an OSC hyperlink wrapper, is removed
8
+ * so it cannot show up as stray characters. The style carries over between calls, the way a
9
+ * terminal keeps it from one line to the next.
10
+ */
11
+ export class AnsiParser {
12
+ style = {};
13
+ /** Parses one piece of text, usually a line. */
14
+ parse(text) {
15
+ if (!text.includes(ESCAPE)) {
16
+ return [this.createPart(text)];
17
+ }
18
+ const parts = [];
19
+ let buffer = '';
20
+ let index = 0;
21
+ const flush = () => {
22
+ if (buffer) {
23
+ parts.push(this.createPart(buffer));
24
+ buffer = '';
25
+ }
26
+ };
27
+ while (index < text.length) {
28
+ const character = text[index];
29
+ if (character !== ESCAPE) {
30
+ buffer += character;
31
+ index++;
32
+ continue;
33
+ }
34
+ const next = text[index + 1];
35
+ if (next === '[') {
36
+ const end = findCsiEnd(text, index + 2);
37
+ if (end < 0) {
38
+ break;
39
+ }
40
+ if (text[end] === 'm') {
41
+ flush();
42
+ this.applySgr(text.slice(index + 2, end));
43
+ }
44
+ index = end + 1;
45
+ }
46
+ else if (next === ']') {
47
+ index = findOscEnd(text, index + 2);
48
+ }
49
+ else {
50
+ // A two-character escape such as `ESC c`, or a lone escape at the end.
51
+ index += next === undefined ? 1 : 2;
52
+ }
53
+ }
54
+ flush();
55
+ return parts.length ? parts : [this.createPart('')];
56
+ }
57
+ /** Forgets the current style. */
58
+ reset() {
59
+ this.style = {};
60
+ }
61
+ createPart(text) {
62
+ const part = { type: 'text', text };
63
+ if (Object.keys(this.style).length > 0) {
64
+ part.style = { ...this.style };
65
+ }
66
+ return part;
67
+ }
68
+ applySgr(sequence) {
69
+ const codes = sequence === '' ? [0] : sequence.split(/[;:]/).map((code) => Number(code) || 0);
70
+ const style = { ...this.style };
71
+ for (let index = 0; index < codes.length; index++) {
72
+ const code = codes[index];
73
+ if (code === 0) {
74
+ for (const key of Object.keys(style)) {
75
+ delete style[key];
76
+ }
77
+ }
78
+ else if (code === 1) {
79
+ style.bold = true;
80
+ }
81
+ else if (code === 2) {
82
+ style.dim = true;
83
+ }
84
+ else if (code === 3) {
85
+ style.italic = true;
86
+ }
87
+ else if (code === 4) {
88
+ style.underline = true;
89
+ }
90
+ else if (code === 9) {
91
+ style.strikethrough = true;
92
+ }
93
+ else if (code === 22) {
94
+ delete style.bold;
95
+ delete style.dim;
96
+ }
97
+ else if (code === 23) {
98
+ delete style.italic;
99
+ }
100
+ else if (code === 24) {
101
+ delete style.underline;
102
+ }
103
+ else if (code === 29) {
104
+ delete style.strikethrough;
105
+ }
106
+ else if (code >= 30 && code <= 37) {
107
+ style.color = code - 30;
108
+ }
109
+ else if (code >= 90 && code <= 97) {
110
+ style.color = code - 90 + 8;
111
+ }
112
+ else if (code >= 40 && code <= 47) {
113
+ style.background = code - 40;
114
+ }
115
+ else if (code >= 100 && code <= 107) {
116
+ style.background = code - 100 + 8;
117
+ }
118
+ else if (code === 39) {
119
+ delete style.color;
120
+ }
121
+ else if (code === 49) {
122
+ delete style.background;
123
+ }
124
+ else if (code === 38 || code === 48) {
125
+ const [color, used] = readExtendedColor(codes, index + 1);
126
+ if (color !== undefined) {
127
+ if (code === 38) {
128
+ style.color = color;
129
+ }
130
+ else {
131
+ style.background = color;
132
+ }
133
+ }
134
+ index += used;
135
+ }
136
+ }
137
+ this.style = style;
138
+ }
139
+ }
140
+ /** Reads a `5;n` or `2;r;g;b` color after code 38 or 48. Returns the color and codes consumed. */
141
+ const readExtendedColor = (codes, start) => {
142
+ const mode = codes[start];
143
+ if (mode === 5) {
144
+ const index = codes[start + 1];
145
+ return [index >= 0 && index <= 255 ? index : undefined, 2];
146
+ }
147
+ if (mode === 2) {
148
+ const [red, green, blue] = codes.slice(start + 1, start + 4).map((value) => clampByte(value));
149
+ return [`#${toHex(red)}${toHex(green)}${toHex(blue)}`, 4];
150
+ }
151
+ return [undefined, 0];
152
+ };
153
+ const clampByte = (value) => {
154
+ return Math.min(255, Math.max(0, value ?? 0));
155
+ };
156
+ const toHex = (value) => {
157
+ return value.toString(16).padStart(2, '0');
158
+ };
159
+ /** Returns the index of the final byte of a CSI sequence, or -1 if the text ends first. */
160
+ const findCsiEnd = (text, start) => {
161
+ for (let index = start; index < text.length; index++) {
162
+ const code = text.charCodeAt(index);
163
+ if (code >= 0x40 && code <= 0x7e) {
164
+ return index;
165
+ }
166
+ }
167
+ return -1;
168
+ };
169
+ /** Returns the index just after an OSC sequence, which ends with BEL or `ESC \`. */
170
+ const findOscEnd = (text, start) => {
171
+ for (let index = start; index < text.length; index++) {
172
+ if (text[index] === BELL) {
173
+ return index + 1;
174
+ }
175
+ if (text[index] === ESCAPE && text[index + 1] === '\\') {
176
+ return index + 2;
177
+ }
178
+ }
179
+ return text.length;
180
+ };
181
+ /** Removes every ANSI escape sequence from text. */
182
+ export const stripAnsi = (text) => {
183
+ return new AnsiParser()
184
+ .parse(text)
185
+ .map((part) => part.text)
186
+ .join('');
187
+ };
@@ -0,0 +1,17 @@
1
+ /** Splits text into user-perceived characters (grapheme clusters). */
2
+ export type GraphemeSplitter = (text: string) => string[];
3
+ /**
4
+ * A grapheme splitter that needs no platform support.
5
+ *
6
+ * It joins combining marks, variation selectors, emoji modifiers, zero width joiner sequences
7
+ * and regional indicator pairs to the character before them. That covers the cases a log line
8
+ * meets in practice; `Intl.Segmenter` is used instead wherever it exists.
9
+ */
10
+ export declare const splitGraphemesFallback: GraphemeSplitter;
11
+ /**
12
+ * Replaces the grapheme splitter used by the text layout. Pass `null` to go back to the
13
+ * default, which uses `Intl.Segmenter` when the platform has it.
14
+ */
15
+ export declare const setGraphemeSplitter: (splitter: GraphemeSplitter | null) => void;
16
+ /** Splits text into grapheme clusters with the active splitter. */
17
+ export declare const splitGraphemes: (text: string) => string[];
@@ -0,0 +1,70 @@
1
+ import { codePointWidth } from './width.js';
2
+ const ZERO_WIDTH_JOINER = 0x200d;
3
+ const REGIONAL_INDICATOR_FIRST = 0x1f1e6;
4
+ const REGIONAL_INDICATOR_LAST = 0x1f1ff;
5
+ const isRegionalIndicator = (codePoint) => {
6
+ return codePoint >= REGIONAL_INDICATOR_FIRST && codePoint <= REGIONAL_INDICATOR_LAST;
7
+ };
8
+ const isEmojiModifier = (codePoint) => {
9
+ return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
10
+ };
11
+ /**
12
+ * A grapheme splitter that needs no platform support.
13
+ *
14
+ * It joins combining marks, variation selectors, emoji modifiers, zero width joiner sequences
15
+ * and regional indicator pairs to the character before them. That covers the cases a log line
16
+ * meets in practice; `Intl.Segmenter` is used instead wherever it exists.
17
+ */
18
+ export const splitGraphemesFallback = (text) => {
19
+ const clusters = [];
20
+ let joinNext = false;
21
+ let regionalCount = 0;
22
+ for (const character of text) {
23
+ const codePoint = character.codePointAt(0) ?? 0;
24
+ const last = clusters.length - 1;
25
+ const attaches = last >= 0 &&
26
+ (joinNext ||
27
+ codePointWidth(codePoint) === 0 ||
28
+ isEmojiModifier(codePoint) ||
29
+ (isRegionalIndicator(codePoint) && regionalCount % 2 === 1));
30
+ if (attaches) {
31
+ clusters[last] += character;
32
+ }
33
+ else {
34
+ clusters.push(character);
35
+ }
36
+ regionalCount = isRegionalIndicator(codePoint) ? regionalCount + 1 : 0;
37
+ joinNext = codePoint === ZERO_WIDTH_JOINER;
38
+ }
39
+ return clusters;
40
+ };
41
+ let activeSplitter = null;
42
+ const createDefaultSplitter = () => {
43
+ const intl = globalThis
44
+ .Intl;
45
+ if (intl?.Segmenter) {
46
+ const segmenter = new intl.Segmenter(undefined, { granularity: 'grapheme' });
47
+ return (text) => {
48
+ const clusters = [];
49
+ for (const { segment } of segmenter.segment(text)) {
50
+ clusters.push(segment);
51
+ }
52
+ return clusters;
53
+ };
54
+ }
55
+ return splitGraphemesFallback;
56
+ };
57
+ /**
58
+ * Replaces the grapheme splitter used by the text layout. Pass `null` to go back to the
59
+ * default, which uses `Intl.Segmenter` when the platform has it.
60
+ */
61
+ export const setGraphemeSplitter = (splitter) => {
62
+ activeSplitter = splitter;
63
+ };
64
+ /** Splits text into grapheme clusters with the active splitter. */
65
+ export const splitGraphemes = (text) => {
66
+ if (!activeSplitter) {
67
+ activeSplitter = createDefaultSplitter();
68
+ }
69
+ return activeSplitter(text);
70
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Splits a stream of text chunks into lines.
3
+ *
4
+ * A line ends at `\n`, `\r\n` or a lone `\r`. A `\r\n` pair split across two chunks still
5
+ * counts as one line break.
6
+ */
7
+ export declare class LineSplitter {
8
+ private pending;
9
+ private skipLineFeed;
10
+ /** Adds a chunk and returns the lines it completed. */
11
+ push(chunk: string): string[];
12
+ /** Returns the unfinished last line, if any, and resets the splitter. */
13
+ flush(): string[];
14
+ /** Whether text is waiting for a line break. */
15
+ get hasPending(): boolean;
16
+ }
17
+ /** Splits a whole string into lines. A trailing line break does not add an empty line. */
18
+ export declare const splitLines: (text: string) => string[];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Splits a stream of text chunks into lines.
3
+ *
4
+ * A line ends at `\n`, `\r\n` or a lone `\r`. A `\r\n` pair split across two chunks still
5
+ * counts as one line break.
6
+ */
7
+ export class LineSplitter {
8
+ pending = '';
9
+ skipLineFeed = false;
10
+ /** Adds a chunk and returns the lines it completed. */
11
+ push(chunk) {
12
+ const lines = [];
13
+ let start = 0;
14
+ let index = 0;
15
+ if (this.skipLineFeed && chunk.charCodeAt(0) === 0x0a) {
16
+ start = 1;
17
+ index = 1;
18
+ }
19
+ this.skipLineFeed = false;
20
+ for (; index < chunk.length; index++) {
21
+ const code = chunk.charCodeAt(index);
22
+ if (code !== 0x0a && code !== 0x0d) {
23
+ continue;
24
+ }
25
+ lines.push(this.pending + chunk.slice(start, index));
26
+ this.pending = '';
27
+ if (code === 0x0d) {
28
+ if (index + 1 === chunk.length) {
29
+ this.skipLineFeed = true;
30
+ }
31
+ else if (chunk.charCodeAt(index + 1) === 0x0a) {
32
+ index++;
33
+ }
34
+ }
35
+ start = index + 1;
36
+ }
37
+ this.pending += chunk.slice(start);
38
+ return lines;
39
+ }
40
+ /** Returns the unfinished last line, if any, and resets the splitter. */
41
+ flush() {
42
+ const rest = this.pending;
43
+ this.pending = '';
44
+ this.skipLineFeed = false;
45
+ return rest ? [rest] : [];
46
+ }
47
+ /** Whether text is waiting for a line break. */
48
+ get hasPending() {
49
+ return this.pending.length > 0;
50
+ }
51
+ }
52
+ /** Splits a whole string into lines. A trailing line break does not add an empty line. */
53
+ export const splitLines = (text) => {
54
+ const splitter = new LineSplitter();
55
+ return [...splitter.push(text), ...splitter.flush()];
56
+ };
@@ -0,0 +1,10 @@
1
+ import { type AmbiguousWidth } from './width.js';
2
+ /** Returns how many cells a string takes on one row. */
3
+ export declare const measureCells: (text: string, ambiguousWidth?: AmbiguousWidth) => number;
4
+ /**
5
+ * Cuts a string to at most `maxCells` cells, ending it with `…` when anything was cut.
6
+ * A wide character that would cross the limit is left out rather than split.
7
+ */
8
+ export declare const truncateCells: (text: string, maxCells: number, ambiguousWidth?: AmbiguousWidth) => string;
9
+ /** Pads a string with spaces on the right to `cells` cells. */
10
+ export declare const padCells: (text: string, cells: number, ambiguousWidth?: AmbiguousWidth) => string;
@@ -0,0 +1,38 @@
1
+ import { splitGraphemes } from './graphemes.js';
2
+ import { clusterWidth } from './width.js';
3
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
4
+ /** Returns how many cells a string takes on one row. */
5
+ export const measureCells = (text, ambiguousWidth = 1) => {
6
+ if (PRINTABLE_ASCII.test(text)) {
7
+ return text.length;
8
+ }
9
+ let cells = 0;
10
+ for (const cluster of splitGraphemes(text)) {
11
+ cells += clusterWidth(cluster, ambiguousWidth);
12
+ }
13
+ return cells;
14
+ };
15
+ /**
16
+ * Cuts a string to at most `maxCells` cells, ending it with `…` when anything was cut.
17
+ * A wide character that would cross the limit is left out rather than split.
18
+ */
19
+ export const truncateCells = (text, maxCells, ambiguousWidth = 1) => {
20
+ if (measureCells(text, ambiguousWidth) <= maxCells) {
21
+ return text;
22
+ }
23
+ let result = '';
24
+ let cells = 0;
25
+ for (const cluster of splitGraphemes(text)) {
26
+ const width = clusterWidth(cluster, ambiguousWidth);
27
+ if (cells + width > maxCells - 1) {
28
+ break;
29
+ }
30
+ result += cluster;
31
+ cells += width;
32
+ }
33
+ return `${result}…`;
34
+ };
35
+ /** Pads a string with spaces on the right to `cells` cells. */
36
+ export const padCells = (text, cells, ambiguousWidth = 1) => {
37
+ return text + ' '.repeat(Math.max(0, cells - measureCells(text, ambiguousWidth)));
38
+ };
@@ -0,0 +1,24 @@
1
+ import { type LineSpan, type ShapedLine } from '../layout/types.js';
2
+ import { type AmbiguousWidth } from './width.js';
3
+ export interface ShapeOptions {
4
+ /** Cells between tab stops. */
5
+ tabSize: number;
6
+ /** Cells an East Asian Ambiguous character takes. */
7
+ ambiguousWidth: AmbiguousWidth;
8
+ /** The most clusters a line keeps. The rest is replaced with `…`. */
9
+ maxClusters: number;
10
+ }
11
+ export declare const DEFAULT_SHAPE_OPTIONS: ShapeOptions;
12
+ /**
13
+ * Splits the spans of a logical line into clusters and measures them.
14
+ *
15
+ * Tabs become spaces up to the next tab stop, and control and bidirectional formatting
16
+ * characters are replaced with a visible notation in the muted style.
17
+ */
18
+ export declare const shapeLine: (spans: readonly LineSpan[], indent: number, options?: ShapeOptions) => ShapedLine;
19
+ /** Returns the width of a cluster of a shaped line. */
20
+ export declare const widthAt: (line: ShapedLine, index: number) => number;
21
+ /** Returns the break class of a cluster of a shaped line. */
22
+ export declare const breakAt: (line: ShapedLine, index: number) => number;
23
+ /** Returns the text of the clusters from `start` to `end`. */
24
+ export declare const textBetween: (line: ShapedLine, start: number, end: number) => string;
@@ -0,0 +1,220 @@
1
+ import { BREAK_KEEP, BREAK_NORMAL, BREAK_SPACE, BREAK_WIDE } from '../layout/types.js';
2
+ import { splitGraphemes } from './graphemes.js';
3
+ import { clusterWidth, isHangul } from './width.js';
4
+ export const DEFAULT_SHAPE_OPTIONS = {
5
+ tabSize: 8,
6
+ ambiguousWidth: 1,
7
+ maxClusters: 10000
8
+ };
9
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
10
+ const SPECIAL_CHARACTER = /[\x00-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
11
+ const ELLIPSIS = '…';
12
+ /**
13
+ * Returns the visible notation for a character that must not be drawn as is: a control
14
+ * character, which would be invisible or move the cursor, or a bidirectional formatting
15
+ * character, which can make a line read differently from what it contains.
16
+ */
17
+ const notationOf = (code) => {
18
+ if (code < 0x20) {
19
+ return `^${String.fromCharCode(code + 0x40)}`;
20
+ }
21
+ if (code === 0x7f) {
22
+ return '^?';
23
+ }
24
+ return `<U+${code.toString(16).toUpperCase().padStart(4, '0')}>`;
25
+ };
26
+ class LineBuilder {
27
+ options;
28
+ spans = [];
29
+ spanStarts = [];
30
+ clusters = [];
31
+ widths = [];
32
+ breaks = [];
33
+ cells = 0;
34
+ simple = true;
35
+ full = false;
36
+ constructor(options) {
37
+ this.options = options;
38
+ }
39
+ openSpan(span) {
40
+ this.spans.push(span);
41
+ this.spanStarts.push(this.clusters.length);
42
+ }
43
+ addCluster(cluster, width, breakClass) {
44
+ if (this.clusters.length >= this.options.maxClusters) {
45
+ this.full = true;
46
+ return;
47
+ }
48
+ this.clusters.push(cluster);
49
+ this.widths.push(width);
50
+ this.breaks.push(breakClass);
51
+ this.cells += width;
52
+ }
53
+ /** Drops clusters past `count`, and spans that no longer have a cluster. */
54
+ truncate(count) {
55
+ this.clusters.length = count;
56
+ this.widths.length = count;
57
+ this.breaks.length = count;
58
+ this.cells = this.widths.reduce((sum, width) => sum + width, 0);
59
+ this.full = false;
60
+ while (this.spanStarts.length > 0 && this.spanStarts[this.spanStarts.length - 1] >= count) {
61
+ this.spanStarts.pop();
62
+ this.spans.pop();
63
+ }
64
+ }
65
+ addText(source, text) {
66
+ if (!text || this.full) {
67
+ return;
68
+ }
69
+ this.openSpan({ ...source, text });
70
+ if (PRINTABLE_ASCII.test(text)) {
71
+ for (let index = 0; index < text.length && !this.full; index++) {
72
+ const character = text[index];
73
+ this.addCluster(character, 1, character === ' ' ? BREAK_SPACE : BREAK_NORMAL);
74
+ }
75
+ return;
76
+ }
77
+ this.simple = false;
78
+ for (const cluster of splitGraphemes(text)) {
79
+ if (this.full) {
80
+ return;
81
+ }
82
+ const width = clusterWidth(cluster, this.options.ambiguousWidth);
83
+ let breakClass = BREAK_NORMAL;
84
+ if (cluster === ' ') {
85
+ breakClass = BREAK_SPACE;
86
+ }
87
+ else if (width === 2) {
88
+ breakClass = isHangul(cluster.codePointAt(0) ?? 0) ? BREAK_KEEP : BREAK_WIDE;
89
+ }
90
+ this.addCluster(cluster, width, breakClass);
91
+ }
92
+ }
93
+ addSpan(span) {
94
+ if ('icon' in span) {
95
+ this.openSpan({ text: '', icon: span.icon, expanded: span.expanded, action: span.action });
96
+ this.simple = false;
97
+ this.addCluster('', 2, BREAK_NORMAL);
98
+ return;
99
+ }
100
+ const source = {
101
+ text: '',
102
+ token: span.token,
103
+ style: span.style,
104
+ action: span.action
105
+ };
106
+ const { text } = span;
107
+ if (!SPECIAL_CHARACTER.test(text)) {
108
+ this.addText(source, text);
109
+ return;
110
+ }
111
+ let start = 0;
112
+ for (let index = 0; index < text.length; index++) {
113
+ if (!SPECIAL_CHARACTER.test(text[index])) {
114
+ continue;
115
+ }
116
+ const code = text.charCodeAt(index);
117
+ this.addText(source, text.slice(start, index));
118
+ start = index + 1;
119
+ if (code === 0x09) {
120
+ const size = this.options.tabSize;
121
+ this.addText(source, ' '.repeat(size - (this.cells % size)));
122
+ }
123
+ else {
124
+ this.addText({ ...source, token: 'muted', style: undefined }, notationOf(code));
125
+ }
126
+ }
127
+ this.addText(source, text.slice(start));
128
+ }
129
+ }
130
+ /** Builds a simple line without splitting it, for the common case of one ASCII span. */
131
+ const shapeSimple = (span, indent) => {
132
+ return {
133
+ indent,
134
+ spans: [span],
135
+ spanStarts: Uint32Array.of(0),
136
+ simple: true,
137
+ text: span.text,
138
+ clusters: null,
139
+ widths: null,
140
+ breaks: null,
141
+ length: span.text.length,
142
+ cells: span.text.length,
143
+ wrap: true
144
+ };
145
+ };
146
+ /**
147
+ * Splits the spans of a logical line into clusters and measures them.
148
+ *
149
+ * Tabs become spaces up to the next tab stop, and control and bidirectional formatting
150
+ * characters are replaced with a visible notation in the muted style.
151
+ */
152
+ export const shapeLine = (spans, indent, options = DEFAULT_SHAPE_OPTIONS) => {
153
+ if (spans.length === 1) {
154
+ const [span] = spans;
155
+ if (!('icon' in span) &&
156
+ span.text.length <= options.maxClusters &&
157
+ PRINTABLE_ASCII.test(span.text)) {
158
+ return shapeSimple({ text: span.text, token: span.token, style: span.style, action: span.action }, indent);
159
+ }
160
+ }
161
+ const builder = new LineBuilder(options);
162
+ for (const span of spans) {
163
+ if (builder.full) {
164
+ break;
165
+ }
166
+ builder.addSpan(span);
167
+ }
168
+ if (builder.full) {
169
+ builder.truncate(Math.max(0, builder.clusters.length - 2));
170
+ builder.openSpan({ text: ` ${ELLIPSIS}`, token: 'muted' });
171
+ builder.addCluster(' ', 1, BREAK_SPACE);
172
+ builder.addCluster(ELLIPSIS, 1, BREAK_NORMAL);
173
+ builder.simple = false;
174
+ }
175
+ const text = builder.clusters.join('');
176
+ const spanStarts = Uint32Array.from(builder.spanStarts);
177
+ if (builder.simple) {
178
+ return {
179
+ indent,
180
+ spans: builder.spans,
181
+ spanStarts,
182
+ simple: true,
183
+ text,
184
+ clusters: null,
185
+ widths: null,
186
+ breaks: null,
187
+ length: builder.clusters.length,
188
+ cells: builder.cells,
189
+ wrap: true
190
+ };
191
+ }
192
+ return {
193
+ indent,
194
+ spans: builder.spans,
195
+ spanStarts,
196
+ simple: false,
197
+ text,
198
+ clusters: builder.clusters,
199
+ widths: Uint8Array.from(builder.widths),
200
+ breaks: Uint8Array.from(builder.breaks),
201
+ length: builder.clusters.length,
202
+ cells: builder.cells,
203
+ wrap: true
204
+ };
205
+ };
206
+ /** Returns the width of a cluster of a shaped line. */
207
+ export const widthAt = (line, index) => {
208
+ return line.widths ? line.widths[index] : 1;
209
+ };
210
+ /** Returns the break class of a cluster of a shaped line. */
211
+ export const breakAt = (line, index) => {
212
+ if (line.breaks) {
213
+ return line.breaks[index];
214
+ }
215
+ return line.text.charCodeAt(index) === 0x20 ? BREAK_SPACE : BREAK_NORMAL;
216
+ };
217
+ /** Returns the text of the clusters from `start` to `end`. */
218
+ export const textBetween = (line, start, end) => {
219
+ return line.clusters ? line.clusters.slice(start, end).join('') : line.text.slice(start, end);
220
+ };