@svelterm/core 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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/src/components/spinner.d.ts +11 -0
  4. package/dist/src/components/spinner.js +19 -0
  5. package/dist/src/components/text-buffer.d.ts +21 -0
  6. package/dist/src/components/text-buffer.js +87 -0
  7. package/dist/src/css/animation-runner.d.ts +17 -0
  8. package/dist/src/css/animation-runner.js +72 -0
  9. package/dist/src/css/animation.d.ts +5 -0
  10. package/dist/src/css/animation.js +6 -0
  11. package/dist/src/css/calc.d.ts +5 -0
  12. package/dist/src/css/calc.js +130 -0
  13. package/dist/src/css/color.d.ts +1 -0
  14. package/dist/src/css/color.js +157 -0
  15. package/dist/src/css/compute.d.ts +63 -0
  16. package/dist/src/css/compute.js +606 -0
  17. package/dist/src/css/defaults.d.ts +8 -0
  18. package/dist/src/css/defaults.js +44 -0
  19. package/dist/src/css/incremental.d.ts +9 -0
  20. package/dist/src/css/incremental.js +46 -0
  21. package/dist/src/css/index.d.ts +5 -0
  22. package/dist/src/css/index.js +3 -0
  23. package/dist/src/css/media.d.ts +11 -0
  24. package/dist/src/css/media.js +59 -0
  25. package/dist/src/css/parser.d.ts +20 -0
  26. package/dist/src/css/parser.js +241 -0
  27. package/dist/src/css/selector.d.ts +17 -0
  28. package/dist/src/css/selector.js +272 -0
  29. package/dist/src/css/specificity.d.ts +7 -0
  30. package/dist/src/css/specificity.js +89 -0
  31. package/dist/src/css/values.d.ts +17 -0
  32. package/dist/src/css/values.js +58 -0
  33. package/dist/src/css/variables.d.ts +6 -0
  34. package/dist/src/css/variables.js +42 -0
  35. package/dist/src/debug/console.d.ts +16 -0
  36. package/dist/src/debug/console.js +65 -0
  37. package/dist/src/debug/server.d.ts +22 -0
  38. package/dist/src/debug/server.js +90 -0
  39. package/dist/src/headless.d.ts +21 -0
  40. package/dist/src/headless.js +26 -0
  41. package/dist/src/index.d.ts +18 -0
  42. package/dist/src/index.js +485 -0
  43. package/dist/src/input/dispatch.d.ts +18 -0
  44. package/dist/src/input/dispatch.js +70 -0
  45. package/dist/src/input/focus.d.ts +18 -0
  46. package/dist/src/input/focus.js +81 -0
  47. package/dist/src/input/hit.d.ts +3 -0
  48. package/dist/src/input/hit.js +29 -0
  49. package/dist/src/input/keyboard.d.ts +9 -0
  50. package/dist/src/input/keyboard.js +100 -0
  51. package/dist/src/input/mouse.d.ts +7 -0
  52. package/dist/src/input/mouse.js +35 -0
  53. package/dist/src/input/scroll.d.ts +2 -0
  54. package/dist/src/input/scroll.js +24 -0
  55. package/dist/src/layout/cache.d.ts +4 -0
  56. package/dist/src/layout/cache.js +8 -0
  57. package/dist/src/layout/engine.d.ts +9 -0
  58. package/dist/src/layout/engine.js +455 -0
  59. package/dist/src/layout/flex.d.ts +4 -0
  60. package/dist/src/layout/flex.js +30 -0
  61. package/dist/src/layout/incremental.d.ts +8 -0
  62. package/dist/src/layout/incremental.js +58 -0
  63. package/dist/src/layout/size.d.ts +2 -0
  64. package/dist/src/layout/size.js +25 -0
  65. package/dist/src/layout/text.d.ts +7 -0
  66. package/dist/src/layout/text.js +52 -0
  67. package/dist/src/render/ansi.d.ts +23 -0
  68. package/dist/src/render/ansi.js +108 -0
  69. package/dist/src/render/border.d.ts +4 -0
  70. package/dist/src/render/border.js +60 -0
  71. package/dist/src/render/buffer.d.ts +23 -0
  72. package/dist/src/render/buffer.js +70 -0
  73. package/dist/src/render/context.d.ts +19 -0
  74. package/dist/src/render/context.js +98 -0
  75. package/dist/src/render/diff.d.ts +2 -0
  76. package/dist/src/render/diff.js +53 -0
  77. package/dist/src/render/incremental-paint.d.ts +10 -0
  78. package/dist/src/render/incremental-paint.js +94 -0
  79. package/dist/src/render/paint-text.d.ts +29 -0
  80. package/dist/src/render/paint-text.js +120 -0
  81. package/dist/src/render/paint.d.ts +5 -0
  82. package/dist/src/render/paint.js +220 -0
  83. package/dist/src/render/queue.d.ts +24 -0
  84. package/dist/src/render/queue.js +54 -0
  85. package/dist/src/render/scrollbar.d.ts +3 -0
  86. package/dist/src/render/scrollbar.js +19 -0
  87. package/dist/src/render/snapshot.d.ts +18 -0
  88. package/dist/src/render/snapshot.js +126 -0
  89. package/dist/src/renderer/default.d.ts +3 -0
  90. package/dist/src/renderer/default.js +3 -0
  91. package/dist/src/renderer/index.d.ts +11 -0
  92. package/dist/src/renderer/index.js +116 -0
  93. package/dist/src/renderer/node.d.ts +44 -0
  94. package/dist/src/renderer/node.js +153 -0
  95. package/dist/src/terminal/screen.d.ts +10 -0
  96. package/dist/src/terminal/screen.js +31 -0
  97. package/dist/src/terminal/stdin-router.d.ts +31 -0
  98. package/dist/src/terminal/stdin-router.js +133 -0
  99. package/package.json +64 -0
@@ -0,0 +1,108 @@
1
+ const ESC = '\x1b';
2
+ const CSI = `${ESC}[`;
3
+ function expandHex(color) {
4
+ if (color.length === 4) {
5
+ return '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
6
+ }
7
+ return color;
8
+ }
9
+ export function moveTo(col, row) {
10
+ return `${CSI}${row};${col}H`;
11
+ }
12
+ export function clearScreen() {
13
+ return `${CSI}2J`;
14
+ }
15
+ export function hideCursor() {
16
+ return `${CSI}?25l`;
17
+ }
18
+ export function showCursor() {
19
+ return `${CSI}?25h`;
20
+ }
21
+ export function enterAltScreen() {
22
+ return `${CSI}?1049h`;
23
+ }
24
+ export function exitAltScreen() {
25
+ return `${CSI}?1049l`;
26
+ }
27
+ export function resetStyle() {
28
+ return `${CSI}0m`;
29
+ }
30
+ export function bold() {
31
+ return `${CSI}1m`;
32
+ }
33
+ export function dim() {
34
+ return `${CSI}2m`;
35
+ }
36
+ export function italic() {
37
+ return `${CSI}3m`;
38
+ }
39
+ export function underline() {
40
+ return `${CSI}4m`;
41
+ }
42
+ export function strikethrough() {
43
+ return `${CSI}9m`;
44
+ }
45
+ export function fgColor(color) {
46
+ const code = ANSI_FG[color];
47
+ if (code !== undefined)
48
+ return `${CSI}${code}m`;
49
+ if (color.startsWith('#')) {
50
+ const hex = expandHex(color);
51
+ const r = parseInt(hex.slice(1, 3), 16);
52
+ const g = parseInt(hex.slice(3, 5), 16);
53
+ const b = parseInt(hex.slice(5, 7), 16);
54
+ return `${CSI}38;2;${r};${g};${b}m`;
55
+ }
56
+ return '';
57
+ }
58
+ export function bgColor(color) {
59
+ const code = ANSI_BG[color];
60
+ if (code !== undefined)
61
+ return `${CSI}${code}m`;
62
+ if (color.startsWith('#')) {
63
+ const hex = expandHex(color);
64
+ const r = parseInt(hex.slice(1, 3), 16);
65
+ const g = parseInt(hex.slice(3, 5), 16);
66
+ const b = parseInt(hex.slice(5, 7), 16);
67
+ return `${CSI}48;2;${r};${g};${b}m`;
68
+ }
69
+ return '';
70
+ }
71
+ const ANSI_FG = {
72
+ black: 30, red: 31, green: 32, yellow: 33,
73
+ blue: 34, magenta: 35, cyan: 36, white: 37,
74
+ default: 39,
75
+ };
76
+ export function hyperlinkOpen(url) {
77
+ return `\x1b]8;;${url}\x1b\\`;
78
+ }
79
+ export function hyperlinkClose() {
80
+ return `\x1b]8;;\x1b\\`;
81
+ }
82
+ export function enableMouse() {
83
+ return `${CSI}?1006h${CSI}?1003h`; // enable SGR mode, then any-event tracking
84
+ }
85
+ export function disableMouse() {
86
+ return `${CSI}?1003l${CSI}?1006l`;
87
+ }
88
+ export function setCursorShape(shape) {
89
+ const code = shape === 'block' ? 2 : shape === 'underline' ? 4 : 6;
90
+ return `${CSI}${code} q`;
91
+ }
92
+ export function enableBracketedPaste() {
93
+ return `${CSI}?2004h`;
94
+ }
95
+ export function disableBracketedPaste() {
96
+ return `${CSI}?2004l`;
97
+ }
98
+ export function beginSyncUpdate() {
99
+ return `${CSI}?2026h`;
100
+ }
101
+ export function endSyncUpdate() {
102
+ return `${CSI}?2026l`;
103
+ }
104
+ const ANSI_BG = {
105
+ black: 40, red: 41, green: 42, yellow: 43,
106
+ blue: 44, magenta: 45, cyan: 46, white: 47,
107
+ default: 49,
108
+ };
@@ -0,0 +1,4 @@
1
+ import { CellBuffer } from './buffer.js';
2
+ import { LayoutBox } from '../layout/engine.js';
3
+ import { ResolvedStyle } from '../css/compute.js';
4
+ export declare function renderBorder(buffer: CellBuffer, box: LayoutBox, style: ResolvedStyle): void;
@@ -0,0 +1,60 @@
1
+ const BORDER_SETS = {
2
+ single: { topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', horizontal: '─', vertical: '│' },
3
+ double: { topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', horizontal: '═', vertical: '║' },
4
+ rounded: { topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯', horizontal: '─', vertical: '│' },
5
+ heavy: { topLeft: '┏', topRight: '┓', bottomLeft: '┗', bottomRight: '┛', horizontal: '━', vertical: '┃' },
6
+ };
7
+ export function renderBorder(buffer, box, style) {
8
+ if (style.borderStyle === 'none')
9
+ return;
10
+ const chars = BORDER_SETS[style.borderStyle];
11
+ if (!chars)
12
+ return;
13
+ const fg = style.borderColor !== 'default' ? style.borderColor : undefined;
14
+ const { x, y, width, height } = box;
15
+ const top = style.borderTop;
16
+ const right = style.borderRight;
17
+ const bottom = style.borderBottom;
18
+ const left = style.borderLeft;
19
+ // Corners (only where two adjacent sides meet)
20
+ if (top && left)
21
+ buffer.setCell(x, y, { char: chars.topLeft, fg });
22
+ if (top && right)
23
+ buffer.setCell(x + width - 1, y, { char: chars.topRight, fg });
24
+ if (bottom && left)
25
+ buffer.setCell(x, y + height - 1, { char: chars.bottomLeft, fg });
26
+ if (bottom && right)
27
+ buffer.setCell(x + width - 1, y + height - 1, { char: chars.bottomRight, fg });
28
+ // Top edge
29
+ if (top) {
30
+ const startCol = left ? x + 1 : x;
31
+ const endCol = right ? x + width - 1 : x + width;
32
+ for (let col = startCol; col < endCol; col++) {
33
+ buffer.setCell(col, y, { char: chars.horizontal, fg });
34
+ }
35
+ }
36
+ // Bottom edge
37
+ if (bottom) {
38
+ const startCol = left ? x + 1 : x;
39
+ const endCol = right ? x + width - 1 : x + width;
40
+ for (let col = startCol; col < endCol; col++) {
41
+ buffer.setCell(col, y + height - 1, { char: chars.horizontal, fg });
42
+ }
43
+ }
44
+ // Left edge
45
+ if (left) {
46
+ const startRow = top ? y + 1 : y;
47
+ const endRow = bottom ? y + height - 1 : y + height;
48
+ for (let row = startRow; row < endRow; row++) {
49
+ buffer.setCell(x, row, { char: chars.vertical, fg });
50
+ }
51
+ }
52
+ // Right edge
53
+ if (right) {
54
+ const startRow = top ? y + 1 : y;
55
+ const endRow = bottom ? y + height - 1 : y + height;
56
+ for (let row = startRow; row < endRow; row++) {
57
+ buffer.setCell(x + width - 1, row, { char: chars.vertical, fg });
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,23 @@
1
+ export interface Cell {
2
+ char: string;
3
+ fg: string;
4
+ bg: string;
5
+ bold: boolean;
6
+ italic: boolean;
7
+ underline: boolean;
8
+ strikethrough: boolean;
9
+ dim: boolean;
10
+ hyperlink?: string;
11
+ }
12
+ export declare class CellBuffer {
13
+ readonly width: number;
14
+ readonly height: number;
15
+ private cells;
16
+ constructor(width: number, height: number);
17
+ clear(): void;
18
+ getCell(col: number, row: number): Cell | undefined;
19
+ setCell(col: number, row: number, cell: Partial<Cell>): void;
20
+ writeText(col: number, row: number, text: string, style?: Partial<Cell>): void;
21
+ clone(): CellBuffer;
22
+ }
23
+ export declare function cellsEqual(a: Cell, b: Cell): boolean;
@@ -0,0 +1,70 @@
1
+ const EMPTY_CELL = {
2
+ char: ' ', fg: 'default', bg: 'default',
3
+ bold: false, italic: false, underline: false,
4
+ strikethrough: false, dim: false,
5
+ };
6
+ export class CellBuffer {
7
+ width;
8
+ height;
9
+ cells;
10
+ constructor(width, height) {
11
+ this.width = width;
12
+ this.height = height;
13
+ this.cells = new Array(width * height);
14
+ this.clear();
15
+ }
16
+ clear() {
17
+ for (let i = 0; i < this.cells.length; i++) {
18
+ this.cells[i] = { ...EMPTY_CELL };
19
+ }
20
+ }
21
+ getCell(col, row) {
22
+ if (col < 0 || col >= this.width || row < 0 || row >= this.height)
23
+ return undefined;
24
+ return this.cells[row * this.width + col];
25
+ }
26
+ setCell(col, row, cell) {
27
+ if (col < 0 || col >= this.width || row < 0 || row >= this.height)
28
+ return;
29
+ const idx = row * this.width + col;
30
+ const existing = this.cells[idx];
31
+ this.cells[idx] = {
32
+ char: cell.char ?? existing.char,
33
+ fg: cell.fg ?? existing.fg,
34
+ bg: cell.bg ?? existing.bg,
35
+ bold: cell.bold ?? existing.bold,
36
+ italic: cell.italic ?? existing.italic,
37
+ underline: cell.underline ?? existing.underline,
38
+ strikethrough: cell.strikethrough ?? existing.strikethrough,
39
+ dim: cell.dim ?? existing.dim,
40
+ hyperlink: cell.hyperlink ?? existing.hyperlink,
41
+ };
42
+ }
43
+ writeText(col, row, text, style) {
44
+ for (let i = 0; i < text.length; i++) {
45
+ this.setCell(col + i, row, {
46
+ char: text[i],
47
+ fg: style?.fg,
48
+ bg: style?.bg,
49
+ bold: style?.bold,
50
+ italic: style?.italic,
51
+ underline: style?.underline,
52
+ strikethrough: style?.strikethrough,
53
+ dim: style?.dim,
54
+ });
55
+ }
56
+ }
57
+ clone() {
58
+ const copy = new CellBuffer(this.width, this.height);
59
+ for (let i = 0; i < this.cells.length; i++) {
60
+ copy.cells[i] = { ...this.cells[i] };
61
+ }
62
+ return copy;
63
+ }
64
+ }
65
+ export function cellsEqual(a, b) {
66
+ return a.char === b.char && a.fg === b.fg && a.bg === b.bg
67
+ && a.bold === b.bold && a.italic === b.italic
68
+ && a.underline === b.underline && a.strikethrough === b.strikethrough
69
+ && a.dim === b.dim && a.hyperlink === b.hyperlink;
70
+ }
@@ -0,0 +1,19 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ import { RenderQueue } from './queue.js';
3
+ /**
4
+ * RenderContext tracks mutations and determines the minimum rendering path.
5
+ * Each renderer method calls the appropriate onX method, which enqueues
6
+ * the minimum work needed.
7
+ */
8
+ export declare class RenderContext {
9
+ readonly queue: RenderQueue;
10
+ onScheduleRender?: () => void;
11
+ onSetText(node: TermNode, newText: string): void;
12
+ onSetAttribute(node: TermNode, key: string, value: string): void;
13
+ onRemoveAttribute(node: TermNode, key: string): void;
14
+ onInsert(parent: TermNode, child: TermNode): void;
15
+ onRemove(child: TermNode, parent: TermNode): void;
16
+ onScroll(node: TermNode): void;
17
+ onResize(): void;
18
+ private invalidateDescendantStyles;
19
+ }
@@ -0,0 +1,98 @@
1
+ import { RenderQueue } from './queue.js';
2
+ /**
3
+ * RenderContext tracks mutations and determines the minimum rendering path.
4
+ * Each renderer method calls the appropriate onX method, which enqueues
5
+ * the minimum work needed.
6
+ */
7
+ export class RenderContext {
8
+ queue = new RenderQueue();
9
+ onScheduleRender;
10
+ onSetText(node, newText) {
11
+ const oldText = node.text ?? '';
12
+ node.text = newText;
13
+ if (oldText.length === newText.length) {
14
+ this.queue.enqueuePaintOnly(node);
15
+ }
16
+ else {
17
+ this.queue.enqueueLayoutBubble(node);
18
+ }
19
+ this.onScheduleRender?.();
20
+ }
21
+ onSetAttribute(node, key, value) {
22
+ if (key === 'class') {
23
+ if (node.cache.classAttr === value)
24
+ return; // no change
25
+ node.cache.classAttr = value;
26
+ node.invalidateStyle();
27
+ this.queue.enqueueStyleResolve(node);
28
+ // Also invalidate descendants — descendant selectors may change
29
+ this.invalidateDescendantStyles(node);
30
+ }
31
+ else if (key === 'id' || key === 'data-focused' || key === 'data-hovered') {
32
+ node.invalidateStyle();
33
+ this.queue.enqueueStyleResolve(node);
34
+ }
35
+ else {
36
+ this.queue.enqueuePaintOnly(node);
37
+ }
38
+ node.attributes.set(key, value);
39
+ this.onScheduleRender?.();
40
+ }
41
+ onRemoveAttribute(node, key) {
42
+ node.attributes.delete(key);
43
+ if (key === 'class' || key === 'id' || key === 'data-focused' || key === 'data-hovered') {
44
+ node.cache.classAttr = '';
45
+ node.invalidateStyle();
46
+ this.queue.enqueueStyleResolve(node);
47
+ this.invalidateDescendantStyles(node);
48
+ }
49
+ this.onScheduleRender?.();
50
+ }
51
+ onInsert(parent, child) {
52
+ // New node needs full computation
53
+ child.invalidateAll();
54
+ this.queue.enqueueStyleResolve(child);
55
+ // Parent needs re-layout
56
+ if (hasFixedDimensions(parent)) {
57
+ this.queue.enqueueLayoutSubtree(parent);
58
+ }
59
+ else {
60
+ this.queue.enqueueLayoutBubble(parent);
61
+ }
62
+ this.onScheduleRender?.();
63
+ }
64
+ onRemove(child, parent) {
65
+ if (hasFixedDimensions(parent)) {
66
+ this.queue.enqueueLayoutSubtree(parent);
67
+ }
68
+ else {
69
+ this.queue.enqueueLayoutBubble(parent);
70
+ }
71
+ // Paint the area where the removed node was
72
+ this.queue.enqueuePaintOnly(parent);
73
+ this.onScheduleRender?.();
74
+ }
75
+ onScroll(node) {
76
+ this.queue.setFullRecompute();
77
+ this.onScheduleRender?.();
78
+ }
79
+ onResize() {
80
+ this.queue.setFullRecompute();
81
+ this.onScheduleRender?.();
82
+ }
83
+ invalidateDescendantStyles(node) {
84
+ for (const child of node.children) {
85
+ if (child.nodeType === 'element') {
86
+ child.invalidateStyle();
87
+ this.queue.enqueueStyleResolve(child);
88
+ this.invalidateDescendantStyles(child);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ function hasFixedDimensions(node) {
94
+ const style = node.cache.resolvedStyle;
95
+ if (!style)
96
+ return false;
97
+ return style.width !== null && style.height !== null;
98
+ }
@@ -0,0 +1,2 @@
1
+ import { CellBuffer } from './buffer.js';
2
+ export declare function diffBuffers(prev: CellBuffer | null, next: CellBuffer): string;
@@ -0,0 +1,53 @@
1
+ import { cellsEqual } from './buffer.js';
2
+ import * as ansi from './ansi.js';
3
+ export function diffBuffers(prev, next) {
4
+ const parts = [];
5
+ let lastStyle = null;
6
+ let currentHyperlink = undefined;
7
+ for (let row = 0; row < next.height; row++) {
8
+ for (let col = 0; col < next.width; col++) {
9
+ const cell = next.getCell(col, row);
10
+ const prevCell = prev?.getCell(col, row);
11
+ if (prevCell && cellsEqual(prevCell, cell))
12
+ continue;
13
+ parts.push(ansi.moveTo(col + 1, row + 1));
14
+ const styleCode = buildStyleCode(cell);
15
+ if (styleCode !== lastStyle) {
16
+ parts.push(ansi.resetStyle());
17
+ parts.push(styleCode);
18
+ lastStyle = styleCode;
19
+ }
20
+ if (cell.hyperlink !== currentHyperlink) {
21
+ if (currentHyperlink)
22
+ parts.push(ansi.hyperlinkClose());
23
+ if (cell.hyperlink)
24
+ parts.push(ansi.hyperlinkOpen(cell.hyperlink));
25
+ currentHyperlink = cell.hyperlink;
26
+ }
27
+ parts.push(cell.char);
28
+ }
29
+ }
30
+ if (currentHyperlink)
31
+ parts.push(ansi.hyperlinkClose());
32
+ if (parts.length > 0)
33
+ parts.push(ansi.resetStyle());
34
+ return parts.join('');
35
+ }
36
+ function buildStyleCode(cell) {
37
+ const parts = [];
38
+ if (cell.fg !== 'default')
39
+ parts.push(ansi.fgColor(cell.fg));
40
+ if (cell.bg !== 'default')
41
+ parts.push(ansi.bgColor(cell.bg));
42
+ if (cell.bold)
43
+ parts.push(ansi.bold());
44
+ if (cell.dim)
45
+ parts.push(ansi.dim());
46
+ if (cell.italic)
47
+ parts.push(ansi.italic());
48
+ if (cell.underline)
49
+ parts.push(ansi.underline());
50
+ if (cell.strikethrough)
51
+ parts.push(ansi.strikethrough());
52
+ return parts.join('');
53
+ }
@@ -0,0 +1,10 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ import { CellBuffer } from './buffer.js';
3
+ import { ResolvedStyle } from '../css/compute.js';
4
+ import { LayoutBox } from '../layout/engine.js';
5
+ /**
6
+ * Repaint only specific nodes' cells in the buffer.
7
+ * Clears the old area and writes the new content.
8
+ * Handles text-align, text-overflow, and white-space from ancestors.
9
+ */
10
+ export declare function paintNodes(nodes: Set<TermNode>, buffer: CellBuffer, styles: Map<number, ResolvedStyle>, layout: Map<number, LayoutBox>, root: TermNode): void;
@@ -0,0 +1,94 @@
1
+ import { renderBorder } from './border.js';
2
+ import { paintTextContent } from './paint-text.js';
3
+ /**
4
+ * Repaint only specific nodes' cells in the buffer.
5
+ * Clears the old area and writes the new content.
6
+ * Handles text-align, text-overflow, and white-space from ancestors.
7
+ */
8
+ export function paintNodes(nodes, buffer, styles, layout, root) {
9
+ for (const node of nodes) {
10
+ const box = layout.get(node.id);
11
+ if (!box)
12
+ continue;
13
+ const oldBox = node.cache.layoutBox;
14
+ if (oldBox)
15
+ clearArea(buffer, oldBox);
16
+ if (node.nodeType === 'text') {
17
+ paintTextShared(node, buffer, box, styles, layout);
18
+ }
19
+ else if (node.nodeType === 'element') {
20
+ paintElementNode(node, buffer, box, styles, layout);
21
+ }
22
+ }
23
+ }
24
+ function paintElementNode(node, buffer, box, styles, layout) {
25
+ const style = styles.get(node.id);
26
+ if (!style || style.display === 'none')
27
+ return;
28
+ // Background fill
29
+ const visuals = resolveInheritedVisuals(node, styles);
30
+ // Element's own style overrides inherited
31
+ const bg = style.bg !== 'default' ? style.bg : visuals.bg;
32
+ if (bg !== 'default') {
33
+ for (let row = box.y; row < box.y + box.height; row++) {
34
+ for (let col = box.x; col < box.x + box.width; col++) {
35
+ buffer.setCell(col, row, { bg });
36
+ }
37
+ }
38
+ }
39
+ // Border
40
+ if (style.borderStyle !== 'none') {
41
+ renderBorder(buffer, box, style);
42
+ }
43
+ // Repaint text children
44
+ for (const child of node.children) {
45
+ const childBox = layout.get(child.id);
46
+ if (!childBox)
47
+ continue;
48
+ if (child.nodeType === 'text') {
49
+ paintTextShared(child, buffer, childBox, styles, layout);
50
+ }
51
+ else if (child.nodeType === 'element') {
52
+ paintElementNode(child, buffer, childBox, styles, layout);
53
+ }
54
+ }
55
+ }
56
+ function paintTextShared(node, buffer, box, styles, layout) {
57
+ const visuals = resolveInheritedVisuals(node, styles);
58
+ paintTextContent(node, buffer, box, visuals, styles, layout);
59
+ }
60
+ function clearArea(buffer, box) {
61
+ for (let row = box.y; row < box.y + box.height; row++) {
62
+ for (let col = box.x; col < box.x + box.width; col++) {
63
+ buffer.setCell(col, row, { char: ' ', fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false });
64
+ }
65
+ }
66
+ }
67
+ function resolveInheritedVisuals(node, styles) {
68
+ const result = { fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false };
69
+ let current = node.parent;
70
+ while (current) {
71
+ const style = styles.get(current.id);
72
+ if (style) {
73
+ if (result.fg === 'default' && style.fg !== 'default')
74
+ result.fg = style.fg;
75
+ if (result.bg === 'default' && style.bg !== 'default')
76
+ result.bg = style.bg;
77
+ if (!result.bold && style.bold)
78
+ result.bold = true;
79
+ if (!result.italic && style.italic)
80
+ result.italic = true;
81
+ if (!result.underline && style.underline)
82
+ result.underline = true;
83
+ if (!result.strikethrough && style.strikethrough)
84
+ result.strikethrough = true;
85
+ if (!result.dim && style.dim)
86
+ result.dim = true;
87
+ }
88
+ if (!result.hyperlink && current.tag === 'a') {
89
+ result.hyperlink = current.attributes.get('href');
90
+ }
91
+ current = current.parent;
92
+ }
93
+ return result;
94
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared text painting logic used by both full and incremental paint.
3
+ * Single implementation prevents divergence.
4
+ */
5
+ import { TermNode } from '../renderer/node.js';
6
+ import { CellBuffer } from './buffer.js';
7
+ import { ResolvedStyle } from '../css/compute.js';
8
+ import { LayoutBox } from '../layout/engine.js';
9
+ interface TextVisuals {
10
+ fg: string;
11
+ bg: string;
12
+ bold: boolean;
13
+ italic: boolean;
14
+ underline: boolean;
15
+ strikethrough: boolean;
16
+ dim: boolean;
17
+ hyperlink?: string;
18
+ }
19
+ /**
20
+ * Paint a text node's content into the buffer, respecting inherited
21
+ * text-align, white-space, text-overflow from ancestors.
22
+ */
23
+ export declare function paintTextContent(node: TermNode, buffer: CellBuffer, box: LayoutBox, visuals: TextVisuals, styles: Map<number, ResolvedStyle>, layout: Map<number, LayoutBox>, clip?: {
24
+ x: number;
25
+ y: number;
26
+ width: number;
27
+ height: number;
28
+ } | null): void;
29
+ export {};