@loomcli/core 0.1.1 → 0.3.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 (57) hide show
  1. package/dist/application.d.ts +73 -28
  2. package/dist/application.js +334 -99
  3. package/dist/chain.d.ts +68 -0
  4. package/dist/chain.js +372 -0
  5. package/dist/command.d.ts +201 -46
  6. package/dist/command.js +713 -57
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -51
  10. package/dist/errors.js +58 -90
  11. package/dist/extension.d.ts +99 -0
  12. package/dist/extension.js +330 -0
  13. package/dist/facts.d.ts +39 -0
  14. package/dist/facts.js +95 -0
  15. package/dist/globals.d.ts +49 -28
  16. package/dist/globals.js +104 -58
  17. package/dist/glyphs.generated.d.ts +464 -0
  18. package/dist/glyphs.generated.js +491 -0
  19. package/dist/host.js +2 -1
  20. package/dist/index.d.ts +21 -6
  21. package/dist/index.js +7 -2
  22. package/dist/inspect.d.ts +69 -12
  23. package/dist/inspect.js +83 -26
  24. package/dist/lanes.d.ts +26 -0
  25. package/dist/lanes.js +45 -0
  26. package/dist/options.d.ts +7 -0
  27. package/dist/options.js +9 -0
  28. package/dist/output.d.ts +93 -15
  29. package/dist/output.js +307 -34
  30. package/dist/plugin.d.ts +132 -0
  31. package/dist/plugin.js +278 -0
  32. package/dist/rendering.d.ts +21 -0
  33. package/dist/rendering.js +72 -0
  34. package/dist/sequence.d.ts +41 -0
  35. package/dist/sequence.js +225 -0
  36. package/dist/signals.d.ts +52 -0
  37. package/dist/signals.js +85 -0
  38. package/dist/style-ansi.d.ts +13 -0
  39. package/dist/style-ansi.js +306 -0
  40. package/dist/style-layout.d.ts +29 -0
  41. package/dist/style-layout.js +228 -0
  42. package/dist/style-resolve.d.ts +6 -0
  43. package/dist/style-resolve.js +26 -0
  44. package/dist/style-state.d.ts +14 -0
  45. package/dist/style-state.js +179 -0
  46. package/dist/style-wire.d.ts +31 -0
  47. package/dist/style-wire.js +201 -0
  48. package/dist/style.d.ts +86 -0
  49. package/dist/style.js +201 -0
  50. package/dist/theme.d.ts +3 -0
  51. package/dist/theme.js +22 -0
  52. package/dist/types.d.ts +222 -26
  53. package/dist/validation.d.ts +12 -3
  54. package/dist/validation.js +34 -17
  55. package/dist/view.d.ts +180 -0
  56. package/dist/view.js +307 -0
  57. package/package.json +2 -1
@@ -0,0 +1,228 @@
1
+ import { graphemeWidths } from '@rockorager/uucode/width';
2
+ const offsets = [0, 1, 2, 3, 4, 5, 6, 7];
3
+ const empty = {
4
+ advance: offsets.map(() => 0),
5
+ children: [],
6
+ kind: 'join',
7
+ tabs: false,
8
+ text: false,
9
+ };
10
+ function leaf(unit, width, tab = false) {
11
+ return {
12
+ advance: offsets.map((offset) => (tab ? 8 - offset : width)),
13
+ kind: 'leaf',
14
+ tab,
15
+ tabs: tab,
16
+ text: unit.kind === 'text' && unit.text.length > 0,
17
+ unit,
18
+ };
19
+ }
20
+ function join(children) {
21
+ children = children.filter((child) => child !== empty);
22
+ if (children.length === 0) {
23
+ return empty;
24
+ }
25
+ if (children.length === 1 && children[0]) {
26
+ return children[0];
27
+ }
28
+ const advance = offsets.map((offset) => children.reduce((column, child) => column + (child.advance[column % 8] ?? 0), offset) -
29
+ offset);
30
+ return {
31
+ advance,
32
+ children,
33
+ kind: 'join',
34
+ tabs: children.some((child) => child.tabs),
35
+ text: children.some((child) => child.text),
36
+ };
37
+ }
38
+ function* leaves(root) {
39
+ const stack = [{ index: 0, node: root }];
40
+ while (stack.length > 0) {
41
+ const frame = stack.at(-1);
42
+ if (!frame) {
43
+ break;
44
+ }
45
+ if (frame.node.kind === 'leaf') {
46
+ yield frame.node;
47
+ stack.pop();
48
+ continue;
49
+ }
50
+ const child = frame.node.children[frame.index];
51
+ if (child === undefined) {
52
+ stack.pop();
53
+ continue;
54
+ }
55
+ frame.index += 1;
56
+ stack.push({ index: 0, node: child });
57
+ }
58
+ }
59
+ /** Grapheme segmentation ignores attribute boundaries and terminal-control envelopes. */
60
+ function textBlock(units) {
61
+ const visible = units
62
+ .filter((unit) => unit.kind === 'text')
63
+ .map((unit) => unit.text)
64
+ .join('');
65
+ const widths = new Map();
66
+ for (const segment of graphemeWidths(visible)) {
67
+ widths.set(segment.start, segment.width);
68
+ }
69
+ const lines = [];
70
+ let parts = [];
71
+ let offset = 0;
72
+ for (const unit of units) {
73
+ if (unit.kind !== 'text') {
74
+ parts.push(leaf(unit, 0));
75
+ continue;
76
+ }
77
+ if (unit.text === '\n' || unit.text === '\r\n') {
78
+ lines.push({ content: join(parts), ending: [unit] });
79
+ parts = [];
80
+ }
81
+ else {
82
+ let width = 0;
83
+ for (let at = offset; at < offset + unit.text.length; at += 1) {
84
+ width += widths.get(at) ?? 0;
85
+ }
86
+ parts.push(leaf(unit, width, unit.text === '\t'));
87
+ }
88
+ offset += unit.text.length;
89
+ }
90
+ lines.push({ content: join(parts), ending: [] });
91
+ return measured(lines);
92
+ }
93
+ function measured(lines) {
94
+ let minimum = Infinity;
95
+ for (let index = 0; index < lines.length; index += 1) {
96
+ const line = lines[index];
97
+ if (line && !(index > 0 && index === lines.length - 1 && !line.content.text)) {
98
+ minimum = Math.min(minimum, line.content.advance[0] ?? 0);
99
+ }
100
+ }
101
+ return { lines, minimum, tabs: lines.some((line) => line.content.tabs) };
102
+ }
103
+ function expandTabs(root) {
104
+ if (!root.tabs) {
105
+ return root;
106
+ }
107
+ const parts = [];
108
+ let column = 0;
109
+ for (const part of leaves(root)) {
110
+ const width = part.advance[column % 8] ?? 0;
111
+ parts.push(part.tab ? leaf({ ...part.unit, text: ' '.repeat(width) }, width) : part);
112
+ column += width;
113
+ }
114
+ return join(parts);
115
+ }
116
+ function padBlock(block, padding, attributes) {
117
+ if (!block.tabs && block.minimum >= padding.width) {
118
+ return block;
119
+ }
120
+ const lines = block.lines.map((line, index) => {
121
+ // A terminal newline has no extra line to align; interior blank lines do.
122
+ if (index > 0 && index === block.lines.length - 1 && !line.content.text) {
123
+ return line;
124
+ }
125
+ const content = expandTabs(line.content);
126
+ const missing = Math.max(0, padding.width - (content.advance[0] ?? 0));
127
+ if (missing === 0) {
128
+ return content === line.content ? line : { ...line, content };
129
+ }
130
+ let before = 0;
131
+ if (padding.align === 'right') {
132
+ before = missing;
133
+ }
134
+ else if (padding.align === 'center') {
135
+ before = Math.floor(missing / 2);
136
+ }
137
+ const space = (count) => leaf({ attributes, kind: 'text', text: ' '.repeat(count) }, count);
138
+ // Re-segment inserted spaces too: a trailing Unicode Prepend character can absorb one.
139
+ const spaced = join([space(before), content, space(missing - before)]);
140
+ const row = measured([{ content: spaced, ending: [] }]);
141
+ let padded = textBlock([...renderedUnits(row)]);
142
+ const shortfall = padding.width - padded.minimum;
143
+ if (shortfall > 0) {
144
+ padded = textBlock([
145
+ ...renderedUnits(padded),
146
+ { attributes, kind: 'text', text: ' '.repeat(shortfall) },
147
+ ]);
148
+ }
149
+ return { ...line, content: padded.lines[0]?.content ?? empty };
150
+ });
151
+ return measured(lines);
152
+ }
153
+ function textRope(parts) {
154
+ if (parts.length === 1 && parts[0] && 'parts' in parts[0]) {
155
+ return parts[0];
156
+ }
157
+ return {
158
+ parts,
159
+ tabs: parts.some((part) => 'parts' in part ? part.tabs : part.kind === 'text' && part.text.includes('\t')),
160
+ };
161
+ }
162
+ function* textUnits(root) {
163
+ const stack = [{ index: 0, rope: root }];
164
+ while (stack.length > 0) {
165
+ const frame = stack.at(-1);
166
+ if (!frame) {
167
+ break;
168
+ }
169
+ // Analysis is owned by the composed root; old subtree snapshots need not stay live.
170
+ if (frame.rope !== root) {
171
+ delete frame.rope.analysis;
172
+ }
173
+ const part = frame.rope.parts[frame.index++];
174
+ if (!part) {
175
+ stack.pop();
176
+ }
177
+ else if ('parts' in part) {
178
+ stack.push({ index: 0, rope: part });
179
+ }
180
+ else {
181
+ yield part;
182
+ }
183
+ }
184
+ }
185
+ function analyze(rope) {
186
+ rope.analysis ??= textBlock([...textUnits(rope)]);
187
+ return rope.analysis;
188
+ }
189
+ /** Nested padding resolves inside outward; unchanged content retains its rope and analysis. */
190
+ function layout(units) {
191
+ let current = { padding: undefined, parts: [] };
192
+ const stack = [];
193
+ for (const unit of units) {
194
+ if (unit.kind === 'open') {
195
+ stack.push(current);
196
+ current = { padding: { attributes: unit.attributes, value: unit.padding }, parts: [] };
197
+ }
198
+ else if (unit.kind === 'close') {
199
+ let rope = textRope(current.parts);
200
+ const padding = current.padding;
201
+ if (padding && (padding.value.width > 0 || rope.tabs)) {
202
+ const original = analyze(rope);
203
+ const padded = padBlock(original, padding.value, padding.attributes);
204
+ if (padded !== original) {
205
+ rope = { analysis: padded, parts: [...renderedUnits(padded)], tabs: false };
206
+ }
207
+ }
208
+ current = stack.pop() ?? { padding: undefined, parts: [] };
209
+ current.parts.push(rope);
210
+ }
211
+ else {
212
+ current.parts.push(unit);
213
+ }
214
+ }
215
+ return analyze(textRope(current.parts));
216
+ }
217
+ function* renderedUnits(block) {
218
+ for (const line of block.lines) {
219
+ for (const part of leaves(line.content)) {
220
+ yield part.unit;
221
+ }
222
+ yield* line.ending;
223
+ }
224
+ }
225
+ function blockWidth(block) {
226
+ return block.lines.reduce((widest, line) => Math.max(widest, line.content.advance[0] ?? 0), 0);
227
+ }
228
+ export { blockWidth, layout, renderedUnits };
@@ -0,0 +1,6 @@
1
+ import type { Capabilities } from './rendering.js';
2
+ import type { Palette } from './style-state.js';
3
+ /** A complete string is one state boundary; no terminal attributes survive the call. */
4
+ declare function resolveText(text: string, palette: Palette, caps: Capabilities): string;
5
+ declare function width(text: string, palette: Palette, caps: Capabilities): number;
6
+ export { resolveText, width };
@@ -0,0 +1,26 @@
1
+ import { scanAnsi } from './style-ansi.js';
2
+ import { blockWidth, layout, renderedUnits } from './style-layout.js';
3
+ import { emptyAttributes, transition } from './style-state.js';
4
+ import { parseText } from './style-wire.js';
5
+ /** A complete string is one state boundary; no terminal attributes survive the call. */
6
+ function resolveText(text, palette, caps) {
7
+ const parsed = parseText(text, palette, caps.mainGlyphs);
8
+ const result = [];
9
+ let previous = emptyAttributes;
10
+ let preservedSgr = false;
11
+ for (const unit of renderedUnits(layout(scanAnsi(parsed, caps)))) {
12
+ preservedSgr ||= unit.kind === 'sgr';
13
+ result.push(transition(previous, unit.attributes, caps), unit.text);
14
+ previous = unit.attributes;
15
+ }
16
+ result.push(transition(previous, emptyAttributes, caps));
17
+ if (preservedSgr) {
18
+ result.push('\u001b[0m');
19
+ }
20
+ return result.join('');
21
+ }
22
+ function width(text, palette, caps) {
23
+ const parsed = parseText(text, palette, caps.mainGlyphs);
24
+ return blockWidth(layout(scanAnsi(parsed, caps)));
25
+ }
26
+ export { resolveText, width };
@@ -0,0 +1,14 @@
1
+ import type { Capabilities } from './rendering.js';
2
+ import type { Color, Modifier, Operation } from './style.js';
3
+ interface Attributes {
4
+ foreground: Color | undefined;
5
+ background: Color | undefined;
6
+ modifiers: ReadonlySet<Modifier>;
7
+ }
8
+ type Palette = ReadonlyMap<string, readonly Operation[]>;
9
+ declare const emptyAttributes: Attributes;
10
+ declare function applyOperations(base: Attributes, operations: readonly Operation[], palette: Palette): Attributes;
11
+ /** Diffs effective attributes, including the shared reset for bold and faint. */
12
+ declare function transition(previous: Attributes, next: Attributes, caps: Capabilities): string;
13
+ export type { Attributes, Palette };
14
+ export { applyOperations, emptyAttributes, transition };
@@ -0,0 +1,179 @@
1
+ import { colors } from './style.js';
2
+ const emptyAttributes = {
3
+ background: undefined,
4
+ foreground: undefined,
5
+ modifiers: new Set(),
6
+ };
7
+ function applyOperations(base, operations, palette) {
8
+ let foreground = base.foreground;
9
+ let background = base.background;
10
+ let active = new Set(base.modifiers);
11
+ const expanded = operations.flatMap((operation) => operation[0] === 'token' ? (palette.get(operation[1]) ?? []) : [operation]);
12
+ for (const operation of expanded) {
13
+ switch (operation[0]) {
14
+ case 'foreground': {
15
+ foreground = operation[1];
16
+ break;
17
+ }
18
+ case 'background': {
19
+ background = operation[1];
20
+ break;
21
+ }
22
+ case 'modifier': {
23
+ active.add(operation[1]);
24
+ break;
25
+ }
26
+ case 'reset': {
27
+ foreground = undefined;
28
+ background = undefined;
29
+ active = new Set();
30
+ break;
31
+ }
32
+ case 'resetForeground': {
33
+ foreground = undefined;
34
+ break;
35
+ }
36
+ case 'resetBackground': {
37
+ background = undefined;
38
+ break;
39
+ }
40
+ case 'token': {
41
+ break;
42
+ }
43
+ }
44
+ }
45
+ return { background, foreground, modifiers: active };
46
+ }
47
+ const ansiModifiers = {
48
+ bold: 1,
49
+ faint: 2,
50
+ hidden: 8,
51
+ inverse: 7,
52
+ italic: 3,
53
+ overline: 53,
54
+ strikethrough: 9,
55
+ underline: 4,
56
+ };
57
+ const resetModifiers = {
58
+ bold: 22,
59
+ faint: 22,
60
+ hidden: 28,
61
+ inverse: 27,
62
+ italic: 23,
63
+ overline: 55,
64
+ strikethrough: 29,
65
+ underline: 24,
66
+ };
67
+ const basicPalette = [
68
+ [0, 0, 0],
69
+ [128, 0, 0],
70
+ [0, 128, 0],
71
+ [128, 128, 0],
72
+ [0, 0, 128],
73
+ [128, 0, 128],
74
+ [0, 128, 128],
75
+ [192, 192, 192],
76
+ [128, 128, 128],
77
+ [255, 0, 0],
78
+ [0, 255, 0],
79
+ [255, 255, 0],
80
+ [0, 0, 255],
81
+ [255, 0, 255],
82
+ [0, 255, 255],
83
+ [255, 255, 255],
84
+ ];
85
+ function channel(value) {
86
+ return value === 0 ? 0 : 55 + value * 40;
87
+ }
88
+ function paletteRgb(index) {
89
+ const basic = basicPalette[index];
90
+ if (basic) {
91
+ return basic;
92
+ }
93
+ if (index >= 232) {
94
+ const value = 8 + (index - 232) * 10;
95
+ return [value, value, value];
96
+ }
97
+ const cube = index - 16;
98
+ return [channel(Math.floor(cube / 36)), channel(Math.floor(cube / 6) % 6), channel(cube % 6)];
99
+ }
100
+ function nearest(rgb, limit) {
101
+ let closest = 0;
102
+ let distance = Infinity;
103
+ for (let index = 0; index < limit; index += 1) {
104
+ const candidate = paletteRgb(index);
105
+ const next = (rgb[0] - candidate[0]) ** 2 + (rgb[1] - candidate[1]) ** 2 + (rgb[2] - candidate[2]) ** 2;
106
+ if (next < distance) {
107
+ distance = next;
108
+ closest = index;
109
+ }
110
+ }
111
+ return closest;
112
+ }
113
+ function colorCode(color, background, depth) {
114
+ const offset = background ? 10 : 0;
115
+ if (color === undefined) {
116
+ return String(39 + offset);
117
+ }
118
+ let index = 0;
119
+ if (typeof color === 'string') {
120
+ index = colors[color];
121
+ }
122
+ else if (color[0] === 'rgb') {
123
+ if (depth === 'truecolor') {
124
+ return `${38 + offset};2;${color[1]};${color[2]};${color[3]}`;
125
+ }
126
+ const fallback = color[4];
127
+ if (depth === 16 && fallback?.ansi16 !== undefined) {
128
+ index = colors[fallback.ansi16];
129
+ }
130
+ else if (depth === 256 && fallback?.ansi256 !== undefined) {
131
+ index = fallback.ansi256;
132
+ }
133
+ else {
134
+ index = nearest([color[1], color[2], color[3]], depth);
135
+ }
136
+ }
137
+ else if (depth !== 16) {
138
+ index = color[1];
139
+ }
140
+ else {
141
+ const fallback = color[2]?.ansi16;
142
+ index = fallback === undefined ? nearest(paletteRgb(color[1]), 16) : colors[fallback];
143
+ }
144
+ return index < 16
145
+ ? String((index < 8 ? 30 + index : 90 + index - 8) + offset)
146
+ : `${38 + offset};5;${index}`;
147
+ }
148
+ function sgr(codes) {
149
+ return codes.length ? `\u001b[${codes.join(';')}m` : '';
150
+ }
151
+ /** Diffs effective attributes, including the shared reset for bold and faint. */
152
+ function transition(previous, next, caps) {
153
+ const codes = [];
154
+ if (caps.color) {
155
+ for (const field of ['foreground', 'background']) {
156
+ const before = colorCode(previous[field], field === 'background', caps.depth);
157
+ const after = colorCode(next[field], field === 'background', caps.depth);
158
+ if (before !== after) {
159
+ codes.push(after);
160
+ }
161
+ }
162
+ }
163
+ if (caps.modifiers) {
164
+ const removed = new Set();
165
+ for (const modifier of previous.modifiers) {
166
+ if (!next.modifiers.has(modifier)) {
167
+ removed.add(resetModifiers[modifier]);
168
+ }
169
+ }
170
+ codes.push(...removed);
171
+ for (const modifier of next.modifiers) {
172
+ if (!previous.modifiers.has(modifier) || removed.has(resetModifiers[modifier])) {
173
+ codes.push(ansiModifiers[modifier]);
174
+ }
175
+ }
176
+ }
177
+ return sgr(codes);
178
+ }
179
+ export { applyOperations, emptyAttributes, transition };
@@ -0,0 +1,31 @@
1
+ import type { Attributes, Palette } from './style-state.js';
2
+ import type { Operation } from './style.js';
3
+ type Alignment = 'left' | 'right' | 'center';
4
+ interface Padding {
5
+ width: number;
6
+ align: Alignment;
7
+ }
8
+ type Boundary = {
9
+ kind: 'open';
10
+ padding: Padding;
11
+ attributes: Attributes;
12
+ scope: StyleScope;
13
+ } | {
14
+ kind: 'close';
15
+ };
16
+ interface ParsedText {
17
+ text: string;
18
+ scopes: StyleScope[];
19
+ root: StyleScope;
20
+ palette: Palette;
21
+ boundaries: Map<number, Boundary[]>;
22
+ }
23
+ interface StyleScope {
24
+ attributes: Attributes;
25
+ parent: StyleScope | undefined;
26
+ operations: readonly Operation[];
27
+ }
28
+ /** Decodes framing once. Rejected headers are opaque, and nesting uses an explicit stack. */
29
+ declare function parseText(input: string, palette: Palette, mainGlyphs: boolean): ParsedText;
30
+ export type { Alignment, Boundary, Padding, ParsedText, StyleScope };
31
+ export { parseText };
@@ -0,0 +1,201 @@
1
+ import { isPlainObject } from './facts.js';
2
+ import { glyphForms } from './glyphs.generated.js';
3
+ import { applyOperations, emptyAttributes } from './style-state.js';
4
+ import { close, colorFallbacks, escape, headerEnd, isColorName, modifiers, open, tokens, } from './style.js';
5
+ function isByte(value) {
6
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255;
7
+ }
8
+ function color(value) {
9
+ if (isColorName(value)) {
10
+ return value;
11
+ }
12
+ if (!Array.isArray(value)) {
13
+ return undefined;
14
+ }
15
+ if ((value.length === 2 || value.length === 3) && value[0] === 'ansi256' && isByte(value[1])) {
16
+ if (value.length === 2) {
17
+ return ['ansi256', value[1]];
18
+ }
19
+ if (!isPlainObject(value[2])) {
20
+ return undefined;
21
+ }
22
+ try {
23
+ return ['ansi256', value[1], colorFallbacks(value[2], false)];
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ if ((value.length === 4 || value.length === 5) &&
30
+ value[0] === 'rgb' &&
31
+ isByte(value[1]) &&
32
+ isByte(value[2]) &&
33
+ isByte(value[3])) {
34
+ if (value.length === 4) {
35
+ return ['rgb', value[1], value[2], value[3]];
36
+ }
37
+ if (!isPlainObject(value[4])) {
38
+ return undefined;
39
+ }
40
+ try {
41
+ return ['rgb', value[1], value[2], value[3], colorFallbacks(value[4], true)];
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ return undefined;
48
+ }
49
+ function operation(value, names) {
50
+ if (!Array.isArray(value)) {
51
+ return undefined;
52
+ }
53
+ if (value.length === 1 &&
54
+ (value[0] === 'reset' || value[0] === 'resetForeground' || value[0] === 'resetBackground')) {
55
+ return [value[0]];
56
+ }
57
+ if (value.length !== 2) {
58
+ return undefined;
59
+ }
60
+ if (value[0] === 'token' && typeof value[1] === 'string' && names.has(value[1])) {
61
+ return ['token', value[1]];
62
+ }
63
+ if (value[0] === 'modifier') {
64
+ const modifier = modifiers.find((name) => name === value[1]);
65
+ return modifier === undefined ? undefined : ['modifier', modifier];
66
+ }
67
+ if (value[0] === 'foreground' || value[0] === 'background') {
68
+ const parsed = color(value[1]);
69
+ return parsed === undefined ? undefined : [value[0], parsed];
70
+ }
71
+ return undefined;
72
+ }
73
+ function glyphName(value) {
74
+ return typeof value === 'string' && Object.hasOwn(glyphForms, value);
75
+ }
76
+ function parseHeader(text, names) {
77
+ let value = undefined;
78
+ try {
79
+ value = JSON.parse(text);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ if (!Array.isArray(value)) {
85
+ return undefined;
86
+ }
87
+ if (value.length === 2 && value[0] === 'glyph' && glyphName(value[1])) {
88
+ return { kind: 'glyph', name: value[1] };
89
+ }
90
+ if (value.length === 3 &&
91
+ value[0] === 'pad' &&
92
+ typeof value[1] === 'number' &&
93
+ Number.isSafeInteger(value[1]) &&
94
+ value[1] >= 0 &&
95
+ (value[2] === 'left' || value[2] === 'right' || value[2] === 'center')) {
96
+ return { kind: 'pad', padding: { align: value[2], width: value[1] } };
97
+ }
98
+ if (value.length !== 2 || value[0] !== 'style' || !Array.isArray(value[1])) {
99
+ return undefined;
100
+ }
101
+ const operations = [];
102
+ for (const item of value[1]) {
103
+ const parsed = operation(item, names);
104
+ if (parsed === undefined) {
105
+ return undefined;
106
+ }
107
+ operations.push(parsed);
108
+ }
109
+ return { kind: 'style', operations };
110
+ }
111
+ /** Decodes framing once. Rejected headers are opaque, and nesting uses an explicit stack. */
112
+ function parseText(input, palette, mainGlyphs) {
113
+ const pieces = [];
114
+ const scopes = [];
115
+ const boundaries = new Map();
116
+ const stack = [];
117
+ const names = new Set([...tokens, ...palette.keys()]);
118
+ const root = { attributes: emptyAttributes, operations: [], parent: undefined };
119
+ let active = root;
120
+ function append(text) {
121
+ pieces.push(text);
122
+ // Positions are UTF-16 offsets, including both units of surrogate pairs.
123
+ // oxlint-disable-next-line typescript/prefer-for-of
124
+ for (let index = 0; index < text.length; index += 1) {
125
+ scopes.push(active);
126
+ }
127
+ }
128
+ function boundary(value) {
129
+ const at = scopes.length;
130
+ const current = boundaries.get(at);
131
+ if (current) {
132
+ current.push(value);
133
+ }
134
+ else {
135
+ boundaries.set(at, [value]);
136
+ }
137
+ }
138
+ function endFrame() {
139
+ const ended = stack.pop();
140
+ if (ended?.kind === 'style') {
141
+ active = ended.previous;
142
+ }
143
+ else if (ended?.kind === 'pad') {
144
+ boundary({ kind: 'close' });
145
+ }
146
+ }
147
+ for (let cursor = 0; cursor < input.length;) {
148
+ const character = input[cursor];
149
+ if (character === open) {
150
+ const end = input.indexOf(headerEnd, cursor + 1);
151
+ const candidate = end === -1 ? undefined : parseHeader(input.slice(cursor + 1, end), names);
152
+ if (candidate === undefined || (candidate.kind === 'glyph' && input[end + 1] !== close)) {
153
+ const until = end === -1 ? input.length : end + 1;
154
+ append(input.slice(cursor, until));
155
+ cursor = until;
156
+ continue;
157
+ }
158
+ cursor = end + 1;
159
+ if (candidate.kind === 'glyph') {
160
+ append(glyphForms[candidate.name][mainGlyphs ? 0 : 1]);
161
+ cursor += 1;
162
+ }
163
+ else if (candidate.kind === 'style') {
164
+ stack.push({ kind: 'style', previous: active });
165
+ active = {
166
+ attributes: applyOperations(active.attributes, candidate.operations, palette),
167
+ operations: candidate.operations,
168
+ parent: active,
169
+ };
170
+ }
171
+ else {
172
+ stack.push({ kind: 'pad' });
173
+ boundary({
174
+ attributes: active.attributes,
175
+ kind: 'open',
176
+ padding: candidate.padding,
177
+ scope: active,
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ if (character === close && stack.length > 0) {
183
+ endFrame();
184
+ cursor += 1;
185
+ continue;
186
+ }
187
+ if (character === escape && /^E00[0-3]$/u.test(input.slice(cursor + 1, cursor + 5))) {
188
+ const digits = input.slice(cursor + 1, cursor + 5);
189
+ append(String.fromCharCode(Number.parseInt(digits, 16)));
190
+ cursor += 5;
191
+ continue;
192
+ }
193
+ append(character ?? '');
194
+ cursor += 1;
195
+ }
196
+ while (stack.length > 0) {
197
+ endFrame();
198
+ }
199
+ return { boundaries, palette, root, scopes, text: pieces.join('') };
200
+ }
201
+ export { parseText };