@loomcli/core 0.2.0 → 0.4.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +642 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +80 -20
  12. package/dist/extension.js +115 -30
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +15 -6
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +38 -4
  21. package/dist/inspect.js +69 -6
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +174 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
@@ -0,0 +1,29 @@
1
+ import type { TextUnit, Unit } from './style-ansi.js';
2
+ /** Width at each tab-stop offset composes without rescanning nested text. */
3
+ interface Metrics {
4
+ advance: readonly number[];
5
+ tabs: boolean;
6
+ text: boolean;
7
+ }
8
+ type Rope = ({
9
+ kind: 'leaf';
10
+ unit: TextUnit;
11
+ tab: boolean;
12
+ } | {
13
+ kind: 'join';
14
+ children: readonly Rope[];
15
+ }) & Metrics;
16
+ interface Line {
17
+ content: Rope;
18
+ ending: readonly TextUnit[];
19
+ }
20
+ interface Block {
21
+ lines: readonly Line[];
22
+ minimum: number;
23
+ tabs: boolean;
24
+ }
25
+ /** Nested padding resolves inside outward; unchanged content retains its rope and analysis. */
26
+ declare function layout(units: readonly Unit[]): Block;
27
+ declare function renderedUnits(block: Block): Generator<TextUnit>;
28
+ declare function blockWidth(block: Block): number;
29
+ export { blockWidth, layout, renderedUnits };
@@ -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 };