@svelterm/core 0.1.0 → 0.23.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 (166) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/README.md +42 -29
  3. package/dist/src/cli/build.d.ts +13 -0
  4. package/dist/src/cli/build.js +119 -0
  5. package/dist/src/cli/bundle.d.ts +25 -0
  6. package/dist/src/cli/bundle.js +61 -0
  7. package/dist/src/cli/dev.d.ts +10 -0
  8. package/dist/src/cli/dev.js +152 -0
  9. package/dist/src/cli/devtools.d.ts +9 -0
  10. package/dist/src/cli/devtools.js +47 -0
  11. package/dist/src/cli/init.d.ts +8 -0
  12. package/dist/src/cli/init.js +153 -0
  13. package/dist/src/cli/main.d.ts +9 -0
  14. package/dist/src/cli/main.js +52 -0
  15. package/dist/src/cli/svt-bin.d.ts +2 -0
  16. package/dist/src/cli/svt-bin.js +6 -0
  17. package/dist/src/cli/svt.d.ts +14 -0
  18. package/dist/src/cli/svt.js +76 -0
  19. package/dist/src/components/text-buffer.js +8 -5
  20. package/dist/src/css/animation-runner.d.ts +15 -6
  21. package/dist/src/css/animation-runner.js +80 -29
  22. package/dist/src/css/animation.d.ts +12 -0
  23. package/dist/src/css/animation.js +21 -0
  24. package/dist/src/css/calc.js +4 -3
  25. package/dist/src/css/color.d.ts +19 -0
  26. package/dist/src/css/color.js +371 -62
  27. package/dist/src/css/compute.d.ts +31 -4
  28. package/dist/src/css/compute.js +273 -34
  29. package/dist/src/css/defaults.d.ts +1 -1
  30. package/dist/src/css/defaults.js +9 -0
  31. package/dist/src/css/easing.d.ts +9 -0
  32. package/dist/src/css/easing.js +95 -0
  33. package/dist/src/css/incremental.d.ts +1 -1
  34. package/dist/src/css/incremental.js +2 -2
  35. package/dist/src/css/interpolate.d.ts +13 -0
  36. package/dist/src/css/interpolate.js +41 -0
  37. package/dist/src/css/parser.js +59 -3
  38. package/dist/src/css/pseudo-elements.d.ts +9 -0
  39. package/dist/src/css/pseudo-elements.js +97 -0
  40. package/dist/src/css/selector.d.ts +17 -2
  41. package/dist/src/css/selector.js +128 -13
  42. package/dist/src/css/specificity.js +17 -6
  43. package/dist/src/css/values.d.ts +6 -1
  44. package/dist/src/css/values.js +13 -6
  45. package/dist/src/debug/context.d.ts +13 -0
  46. package/dist/src/debug/context.js +11 -0
  47. package/dist/src/debug/css.d.ts +12 -0
  48. package/dist/src/debug/css.js +28 -0
  49. package/dist/src/debug/dom.d.ts +17 -0
  50. package/dist/src/debug/dom.js +92 -0
  51. package/dist/src/devtools/DevTools.compiled.js +327 -0
  52. package/dist/src/devtools/DevTools.css.js +1 -0
  53. package/dist/src/devtools/client.d.ts +36 -0
  54. package/dist/src/devtools/client.js +76 -0
  55. package/dist/src/framelog.d.ts +54 -0
  56. package/dist/src/framelog.js +99 -0
  57. package/dist/src/headless.js +12 -4
  58. package/dist/src/index.d.ts +66 -3
  59. package/dist/src/index.js +610 -81
  60. package/dist/src/input/checkable.d.ts +8 -0
  61. package/dist/src/input/checkable.js +66 -0
  62. package/dist/src/input/details.d.ts +6 -0
  63. package/dist/src/input/details.js +34 -0
  64. package/dist/src/input/focus.d.ts +6 -0
  65. package/dist/src/input/focus.js +27 -9
  66. package/dist/src/input/keyboard.d.ts +2 -2
  67. package/dist/src/input/keyboard.js +32 -5
  68. package/dist/src/input/label.d.ts +8 -0
  69. package/dist/src/input/label.js +53 -0
  70. package/dist/src/input/modal.d.ts +9 -0
  71. package/dist/src/input/modal.js +28 -0
  72. package/dist/src/input/mouse.d.ts +2 -2
  73. package/dist/src/input/mouse.js +15 -2
  74. package/dist/src/input/select.d.ts +12 -0
  75. package/dist/src/input/select.js +63 -0
  76. package/dist/src/input/selection.d.ts +48 -0
  77. package/dist/src/input/selection.js +150 -0
  78. package/dist/src/layout/engine.d.ts +2 -0
  79. package/dist/src/layout/engine.js +1092 -142
  80. package/dist/src/layout/flex.js +4 -4
  81. package/dist/src/layout/size.js +3 -2
  82. package/dist/src/layout/text.d.ts +3 -2
  83. package/dist/src/layout/text.js +96 -17
  84. package/dist/src/layout/unicode.d.ts +20 -0
  85. package/dist/src/layout/unicode.js +121 -0
  86. package/dist/src/render/animation-clock.d.ts +57 -0
  87. package/dist/src/render/animation-clock.js +221 -0
  88. package/dist/src/render/ansi-text.d.ts +26 -0
  89. package/dist/src/render/ansi-text.js +131 -0
  90. package/dist/src/render/ansi.d.ts +18 -0
  91. package/dist/src/render/ansi.js +64 -19
  92. package/dist/src/render/border.js +166 -17
  93. package/dist/src/render/buffer.d.ts +1 -0
  94. package/dist/src/render/buffer.js +5 -2
  95. package/dist/src/render/clock.d.ts +35 -0
  96. package/dist/src/render/clock.js +67 -0
  97. package/dist/src/render/color-depth.d.ts +8 -0
  98. package/dist/src/render/color-depth.js +59 -0
  99. package/dist/src/render/context.d.ts +1 -0
  100. package/dist/src/render/context.js +17 -21
  101. package/dist/src/render/cursor-emit.d.ts +18 -0
  102. package/dist/src/render/cursor-emit.js +50 -0
  103. package/dist/src/render/diff.d.ts +12 -0
  104. package/dist/src/render/diff.js +120 -0
  105. package/dist/src/render/generation.d.ts +9 -0
  106. package/dist/src/render/generation.js +14 -0
  107. package/dist/src/render/graphics-layer.d.ts +27 -0
  108. package/dist/src/render/graphics-layer.js +86 -0
  109. package/dist/src/render/image.d.ts +27 -0
  110. package/dist/src/render/image.js +113 -0
  111. package/dist/src/render/incremental-paint.d.ts +7 -3
  112. package/dist/src/render/incremental-paint.js +52 -79
  113. package/dist/src/render/inline.d.ts +59 -0
  114. package/dist/src/render/inline.js +219 -0
  115. package/dist/src/render/kitty-graphics.d.ts +24 -0
  116. package/dist/src/render/kitty-graphics.js +58 -0
  117. package/dist/src/render/paint-text.js +68 -22
  118. package/dist/src/render/paint.d.ts +8 -1
  119. package/dist/src/render/paint.js +358 -31
  120. package/dist/src/render/png.d.ts +13 -0
  121. package/dist/src/render/png.js +145 -0
  122. package/dist/src/render/scrollbar.d.ts +8 -2
  123. package/dist/src/render/scrollbar.js +71 -14
  124. package/dist/src/render/snapshot.js +3 -1
  125. package/dist/src/renderer/default.d.ts +7 -0
  126. package/dist/src/renderer/default.js +11 -0
  127. package/dist/src/renderer/index.d.ts +8 -2
  128. package/dist/src/renderer/index.js +4 -2
  129. package/dist/src/renderer/node.d.ts +109 -0
  130. package/dist/src/renderer/node.js +165 -1
  131. package/dist/src/terminal/capabilities.d.ts +33 -0
  132. package/dist/src/terminal/capabilities.js +66 -0
  133. package/dist/src/terminal/clipboard.d.ts +9 -0
  134. package/dist/src/terminal/clipboard.js +39 -0
  135. package/dist/src/terminal/io.d.ts +82 -0
  136. package/dist/src/terminal/io.js +155 -0
  137. package/dist/src/terminal/screen.d.ts +3 -10
  138. package/dist/src/terminal/screen.js +5 -28
  139. package/dist/src/terminal/stdin-router.d.ts +8 -5
  140. package/dist/src/terminal/stdin-router.js +22 -11
  141. package/dist/src/utils/node-map.d.ts +24 -0
  142. package/dist/src/utils/node-map.js +75 -0
  143. package/dist/src/vite/config.d.ts +62 -0
  144. package/dist/src/vite/config.js +191 -0
  145. package/docs/compatibility.md +67 -0
  146. package/docs/debug/devtools.md +40 -0
  147. package/docs/debug/svt.md +50 -0
  148. package/docs/distribution.md +106 -0
  149. package/docs/elements.md +120 -0
  150. package/docs/getting-started.md +177 -0
  151. package/docs/guide/css.md +187 -0
  152. package/docs/guide/input.md +143 -0
  153. package/docs/guide/layout.md +171 -0
  154. package/docs/guide/theming.md +94 -0
  155. package/docs/how-it-works.md +115 -0
  156. package/docs/inline-mode.md +77 -0
  157. package/docs/layout.md +112 -0
  158. package/docs/motion.md +91 -0
  159. package/docs/reference/README.md +65 -0
  160. package/docs/reference/css/properties/border-corner.md +82 -0
  161. package/docs/reference/css/properties/border-style.md +168 -0
  162. package/docs/reference.md +227 -0
  163. package/docs/selectors.md +80 -0
  164. package/docs/terminal-css.md +149 -0
  165. package/docs/terminals.md +83 -0
  166. package/package.json +28 -7
@@ -1,5 +1,7 @@
1
+ import { NodeMap } from '../utils/node-map.js';
2
+ import { resolvePseudoElements } from './pseudo-elements.js';
1
3
  import { matchesSelector } from './selector.js';
2
- import { resolveColor } from './color.js';
4
+ import { resolveColor, expandLightDark } from './color.js';
3
5
  import { parseCellValue, parseSizeValue, parseJustify, parseAlign } from './values.js';
4
6
  import { collectVariables, resolveVar } from './variables.js';
5
7
  import { computeSpecificity, compareSpecificity } from './specificity.js';
@@ -13,7 +15,15 @@ const DEFAULT_MEDIA = {
13
15
  };
14
16
  const INLINE_ELEMENTS = new Set(['span', 'a', 'strong', 'em', 'b', 'i', 'u', 'code', 'small', 'sub', 'sup']);
15
17
  const TABLE_ELEMENTS = {
16
- table: 'table', tr: 'table-row', td: 'table-cell', th: 'table-cell',
18
+ table: 'table',
19
+ tr: 'table-row',
20
+ td: 'table-cell', th: 'table-cell',
21
+ thead: 'table-header-group',
22
+ tbody: 'table-row-group',
23
+ tfoot: 'table-footer-group',
24
+ caption: 'table-caption',
25
+ colgroup: 'table-column-group',
26
+ col: 'table-column',
17
27
  };
18
28
  function defaultDisplay(tag) {
19
29
  if (!tag)
@@ -25,49 +35,68 @@ function defaultDisplay(tag) {
25
35
  return 'block';
26
36
  }
27
37
  export function defaultStyle(tag) {
38
+ const defaultPaddingLeft = tag === 'ul' ? 2 : tag === 'ol' ? 4 : 0;
28
39
  return {
29
40
  fg: 'default', bg: 'default',
30
41
  bold: false, italic: false, underline: false, strikethrough: false, dim: false,
31
42
  display: defaultDisplay(tag),
32
43
  flexDirection: 'row',
33
- justifyContent: 'start', alignItems: 'start', alignSelf: 'auto',
44
+ justifyContent: 'start', alignItems: 'stretch', alignSelf: 'auto',
34
45
  gap: 0,
35
- paddingTop: 0, paddingRight: 0, paddingBottom: 0, paddingLeft: 0,
46
+ paddingTop: 0, paddingRight: 0, paddingBottom: 0, paddingLeft: defaultPaddingLeft,
36
47
  width: null, height: null,
37
48
  minWidth: null, minHeight: null, maxWidth: null, maxHeight: null,
38
49
  marginTop: 0, marginRight: 0, marginBottom: 0, marginLeft: 0,
39
50
  flexGrow: 0, flexShrink: 1, flexBasis: 'auto', flexWrap: 'nowrap', order: 0,
40
51
  gridTemplateColumns: null, gridTemplateRows: null,
52
+ gridColumnStart: null, gridColumnEnd: null, gridColumnSpan: null,
53
+ gridRowStart: null, gridRowEnd: null, gridRowSpan: null,
54
+ gridTemplateAreas: null, gridArea: null,
41
55
  animationName: null, animationDuration: 0, animationIterationCount: 1,
42
- borderStyle: 'none', borderColor: 'default',
56
+ animationTimingFunction: 'ease',
57
+ transitionProperty: null, transitionDuration: 0,
58
+ transitionTimingFunction: 'ease',
59
+ borderStyle: 'none', borderColor: 'default', borderCorner: 'none',
43
60
  borderTop: true, borderRight: true, borderBottom: true, borderLeft: true,
61
+ boxSizing: 'border-box',
44
62
  overflow: 'visible',
45
63
  textOverflow: 'clip',
46
64
  whiteSpace: 'normal',
47
- textAlign: 'left',
65
+ wordBreak: 'normal',
66
+ opacity: 1,
67
+ textAlign: tag === 'button' ? 'center' : 'left',
68
+ textTransform: 'none',
48
69
  position: 'static',
49
70
  top: null, right: null, bottom: null, left: null,
50
71
  zIndex: 0,
51
72
  visibility: 'visible',
73
+ captionSide: 'top',
74
+ tableLayout: 'auto',
75
+ verticalAlign: 'top',
76
+ borderCollapse: 'separate',
77
+ borderSpacingH: 0,
78
+ borderSpacingV: 0,
79
+ emptyCells: 'show',
52
80
  };
53
81
  }
54
82
  export function resolveStyles(root, stylesheet, media, availWidth, availHeight) {
55
83
  const hasContainerRules = stylesheet.rules.some(r => r.container);
56
84
  // Filter by media if context provided; always filter @supports
57
85
  const filtered = media ? filterByMedia(stylesheet, media) : filterSupports(stylesheet);
86
+ const scheme = media?.colorScheme ?? 'dark';
58
87
  if (!hasContainerRules) {
59
88
  // Simple path: no container queries
60
89
  const variables = collectVariables(root, filtered);
61
- const styles = new Map();
62
- resolveNode(root, filtered, styles, variables);
90
+ const styles = new NodeMap();
91
+ resolveNode(root, filtered, styles, variables, scheme);
63
92
  return styles;
64
93
  }
65
94
  // Two-pass for container queries:
66
95
  // Pass 1: resolve without @container rules to get initial layout
67
96
  const withoutContainer = filterContainerRules(filtered, false);
68
97
  const variables1 = collectVariables(root, withoutContainer);
69
- const styles1 = new Map();
70
- resolveNode(root, withoutContainer, styles1, variables1);
98
+ const styles1 = new NodeMap();
99
+ resolveNode(root, withoutContainer, styles1, variables1, scheme);
71
100
  // Compute layout to get container dimensions
72
101
  const layout = computeLayout(root, styles1, availWidth ?? media?.width ?? 80, availHeight ?? media?.height ?? 24);
73
102
  // Pass 2: evaluate container rules against computed layout
@@ -81,8 +110,8 @@ export function resolveStyles(root, stylesheet, media, availWidth, availHeight)
81
110
  keyframes: filtered.keyframes,
82
111
  };
83
112
  const variables2 = collectVariables(root, withMatchingContainers);
84
- const styles2 = new Map();
85
- resolveNode(root, withMatchingContainers, styles2, variables2);
113
+ const styles2 = new NodeMap();
114
+ resolveNode(root, withMatchingContainers, styles2, variables2, scheme);
86
115
  return styles2;
87
116
  }
88
117
  function filterSupports(stylesheet) {
@@ -106,14 +135,15 @@ export function filterByMedia(stylesheet, context) {
106
135
  const SUPPORTED_PROPERTIES = new Set([
107
136
  'display', 'flex-direction', 'justify-content', 'align-items', 'align-self',
108
137
  'gap', 'flex-grow', 'flex-shrink', 'flex-wrap', 'flex', 'order',
109
- 'grid-template-columns',
138
+ 'grid-template-columns', 'grid-column',
110
139
  'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
111
140
  'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
112
141
  'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
113
142
  'color', 'background-color', 'background',
114
- 'font-weight', 'font-style', 'text-decoration', 'text-align', 'text-overflow',
115
- 'white-space', 'overflow', 'visibility', 'opacity',
116
- 'border', 'border-style', 'border-color',
143
+ 'font-weight', 'font-style', 'text-decoration', 'text-align', 'text-overflow', 'text-transform',
144
+ 'white-space', 'word-break', 'overflow', 'visibility', 'opacity',
145
+ 'border', 'border-style', 'border-color', 'border-corner',
146
+ 'box-sizing',
117
147
  'position', 'top', 'right', 'bottom', 'left', 'z-index',
118
148
  ]);
119
149
  function filterContainerRules(stylesheet, include) {
@@ -174,20 +204,21 @@ function evaluateSupports(condition) {
174
204
  const property = condition.substring(0, colonIdx).trim();
175
205
  return SUPPORTED_PROPERTIES.has(property);
176
206
  }
177
- export function resolveNode(node, stylesheet, styles, variables) {
207
+ export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark') {
178
208
  if (node.nodeType === 'element') {
179
209
  const vars = variables.get(node.id) ?? new Map();
180
210
  const parentStyle = node.parent ? styles.get(node.parent.id) : undefined;
181
- const resolved = computeStyle(node, stylesheet, vars, parentStyle);
211
+ const resolved = computeStyle(node, stylesheet, vars, parentStyle, scheme);
182
212
  styles.set(node.id, resolved);
183
213
  node.cache.resolvedStyle = resolved;
184
214
  node.cache.classAttr = node.attributes.get('class') ?? '';
215
+ resolvePseudoElements(node, stylesheet, styles, vars, scheme);
185
216
  }
186
217
  for (const child of node.children) {
187
- resolveNode(child, stylesheet, styles, variables);
218
+ resolveNode(child, stylesheet, styles, variables, scheme);
188
219
  }
189
220
  }
190
- function computeStyle(node, stylesheet, vars, parentStyle) {
221
+ function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
191
222
  const style = defaultStyle(node.tag);
192
223
  // Collect all matching declarations with specificity
193
224
  const scored = [];
@@ -231,14 +262,34 @@ function computeStyle(node, stylesheet, vars, parentStyle) {
231
262
  }
232
263
  }
233
264
  else {
234
- applyDeclaration(style, decl.property, decl.value);
265
+ applyDeclaration(style, decl.property, decl.value, scheme);
266
+ }
267
+ }
268
+ // Inline style="..." attribute — applied last (higher than any selector specificity)
269
+ const inline = node.attributes?.get('style');
270
+ if (inline) {
271
+ for (const decl of parseInlineStyle(inline)) {
272
+ applyDeclaration(style, decl.property, resolveVar(decl.value, vars), scheme);
235
273
  }
236
274
  }
237
275
  return style;
238
276
  }
277
+ function parseInlineStyle(text) {
278
+ const result = [];
279
+ for (const part of text.split(';')) {
280
+ const colon = part.indexOf(':');
281
+ if (colon < 0)
282
+ continue;
283
+ const property = part.slice(0, colon).trim().toLowerCase();
284
+ const value = part.slice(colon + 1).trim();
285
+ if (property && value)
286
+ result.push({ property, value });
287
+ }
288
+ return result;
289
+ }
239
290
  const INHERITABLE_PROPERTIES = new Set([
240
291
  'color', 'font-weight', 'font-style', 'text-decoration',
241
- 'white-space', 'text-align', 'visibility', 'opacity',
292
+ 'white-space', 'word-break', 'text-align', 'visibility', 'opacity',
242
293
  ]);
243
294
  function applyInherit(style, property, parentStyle) {
244
295
  switch (property) {
@@ -262,14 +313,21 @@ function applyInherit(style, property, parentStyle) {
262
313
  case 'white-space':
263
314
  style.whiteSpace = parentStyle.whiteSpace;
264
315
  break;
316
+ case 'word-break':
317
+ style.wordBreak = parentStyle.wordBreak;
318
+ break;
265
319
  case 'text-align':
266
320
  style.textAlign = parentStyle.textAlign;
267
321
  break;
322
+ case 'text-transform':
323
+ style.textTransform = parentStyle.textTransform;
324
+ break;
268
325
  case 'visibility':
269
326
  style.visibility = parentStyle.visibility;
270
327
  break;
271
328
  case 'opacity':
272
329
  style.dim = parentStyle.dim;
330
+ style.opacity = parentStyle.opacity;
273
331
  break;
274
332
  }
275
333
  }
@@ -295,14 +353,17 @@ function applyInitial(style, property, tag) {
295
353
  break;
296
354
  }
297
355
  }
298
- function applyDeclaration(style, property, value) {
356
+ export function applyDeclaration(style, property, value, scheme = 'dark') {
357
+ // light-dark(a, b) is valid wherever a colour is — expand once at the
358
+ // top so the property-specific branches don't each have to know about it.
359
+ const v = value.includes('light-dark(') ? expandLightDark(value, scheme) : value;
299
360
  switch (property) {
300
361
  case 'color':
301
- style.fg = resolveColor(value);
362
+ style.fg = resolveColor(v);
302
363
  break;
303
364
  case 'background-color':
304
365
  case 'background':
305
- style.bg = resolveColor(value);
366
+ style.bg = resolveColor(v);
306
367
  break;
307
368
  case 'font-weight':
308
369
  style.bold = value === 'bold' || parseInt(value) >= 700;
@@ -317,7 +378,14 @@ function applyDeclaration(style, property, value) {
317
378
  style.strikethrough = true;
318
379
  break;
319
380
  case 'display':
320
- if (['block', 'inline', 'inline-block', 'flex', 'grid', 'table', 'table-row', 'table-cell', 'none'].includes(value)) {
381
+ if ([
382
+ 'block', 'inline', 'inline-block', 'flex', 'grid',
383
+ 'table', 'inline-table',
384
+ 'table-row', 'table-cell',
385
+ 'table-row-group', 'table-header-group', 'table-footer-group',
386
+ 'table-caption', 'table-column-group', 'table-column',
387
+ 'none', 'contents',
388
+ ].includes(value)) {
321
389
  style.display = value;
322
390
  }
323
391
  break;
@@ -416,6 +484,10 @@ function applyDeclaration(style, property, value) {
416
484
  }
417
485
  break;
418
486
  }
487
+ case 'box-sizing':
488
+ if (value === 'border-box' || value === 'content-box')
489
+ style.boxSizing = value;
490
+ break;
419
491
  case 'margin-top':
420
492
  style.marginTop = value === 'auto' ? -1 : parseSizeOrCell(value);
421
493
  break;
@@ -459,15 +531,52 @@ function applyDeclaration(style, property, value) {
459
531
  case 'grid-template-rows':
460
532
  style.gridTemplateRows = value;
461
533
  break;
534
+ case 'grid-column': {
535
+ const line = parseGridLine(value);
536
+ style.gridColumnStart = line.start;
537
+ style.gridColumnEnd = line.end;
538
+ if (line.span !== null)
539
+ style.gridColumnSpan = line.span;
540
+ break;
541
+ }
542
+ case 'grid-row': {
543
+ const line = parseGridLine(value);
544
+ style.gridRowStart = line.start;
545
+ style.gridRowEnd = line.end;
546
+ if (line.span !== null)
547
+ style.gridRowSpan = line.span;
548
+ break;
549
+ }
550
+ case 'grid-template-areas':
551
+ style.gridTemplateAreas = value === 'none' ? null : value;
552
+ break;
553
+ case 'grid-area':
554
+ parseGridArea(style, value);
555
+ break;
462
556
  case 'animation':
463
557
  parseAnimationShorthand(style, value);
464
558
  break;
559
+ case 'transition':
560
+ parseTransitionShorthand(style, value);
561
+ break;
562
+ case 'transition-property':
563
+ style.transitionProperty = value === 'none' ? null : value;
564
+ break;
565
+ case 'transition-duration':
566
+ style.transitionDuration = parseDuration(value);
567
+ break;
568
+ case 'transition-timing-function':
569
+ style.transitionTimingFunction = value;
570
+ break;
465
571
  case 'animation-name':
466
572
  style.animationName = value === 'none' ? null : value;
467
573
  break;
468
574
  case 'animation-duration':
469
575
  style.animationDuration = parseDuration(value);
470
576
  break;
577
+ case 'animation-timing-function':
578
+ style.animationTimingFunction = value;
579
+ break;
471
580
  case 'animation-iteration-count':
472
581
  style.animationIterationCount = value === 'infinite' ? Infinity : (parseInt(value) || 1);
473
582
  break;
@@ -480,7 +589,11 @@ function applyDeclaration(style, property, value) {
480
589
  style.borderStyle = value;
481
590
  break;
482
591
  case 'border-color':
483
- style.borderColor = value === 'currentColor' ? style.fg : resolveColor(value);
592
+ style.borderColor = value === 'currentColor' ? style.fg : resolveColor(v);
593
+ break;
594
+ case 'border-corner':
595
+ if (value === 'h' || value === 'v' || value === 'none')
596
+ style.borderCorner = value;
484
597
  break;
485
598
  case 'border-top':
486
599
  setIndividualBorderSide(style, 'borderTop', value);
@@ -510,14 +623,23 @@ function applyDeclaration(style, property, value) {
510
623
  else
511
624
  style.whiteSpace = 'normal';
512
625
  break;
626
+ case 'word-break':
627
+ style.wordBreak = value === 'break-all' ? 'break-all' : 'normal';
628
+ break;
513
629
  case 'text-align':
514
630
  if (value === 'center' || value === 'right')
515
631
  style.textAlign = value;
516
632
  else
517
633
  style.textAlign = 'left';
518
634
  break;
635
+ case 'text-transform':
636
+ if (value === 'uppercase' || value === 'lowercase' || value === 'capitalize')
637
+ style.textTransform = value;
638
+ else
639
+ style.textTransform = 'none';
640
+ break;
519
641
  case 'position':
520
- if (value === 'relative' || value === 'absolute' || value === 'fixed')
642
+ if (value === 'relative' || value === 'absolute' || value === 'fixed' || value === 'sticky')
521
643
  style.position = value;
522
644
  else
523
645
  style.position = 'static';
@@ -540,18 +662,48 @@ function applyDeclaration(style, property, value) {
540
662
  case 'visibility':
541
663
  style.visibility = value === 'hidden' ? 'hidden' : 'visible';
542
664
  break;
665
+ case 'caption-side':
666
+ style.captionSide = value === 'bottom' ? 'bottom' : 'top';
667
+ break;
668
+ case 'table-layout':
669
+ style.tableLayout = value === 'fixed' ? 'fixed' : 'auto';
670
+ break;
671
+ case 'border-collapse':
672
+ style.borderCollapse = value === 'collapse' ? 'collapse' : 'separate';
673
+ break;
674
+ case 'empty-cells':
675
+ style.emptyCells = value === 'hide' ? 'hide' : 'show';
676
+ break;
677
+ case 'border-spacing': {
678
+ // One value applies to both axes; two values are horizontal then vertical.
679
+ const parts = value.split(/\s+/).map(parseCellValue);
680
+ style.borderSpacingH = parts[0] ?? 0;
681
+ style.borderSpacingV = (parts.length > 1 ? parts[1] : parts[0]) ?? 0;
682
+ break;
683
+ }
684
+ case 'vertical-align':
685
+ if (value === 'top' || value === 'middle' || value === 'bottom' || value === 'baseline') {
686
+ style.verticalAlign = value;
687
+ }
688
+ break;
543
689
  case 'opacity':
544
690
  if (value === 'dim') {
545
691
  style.dim = true;
546
692
  }
547
693
  else {
548
694
  const num = parseFloat(value);
549
- style.dim = !isNaN(num) && num < 1;
695
+ if (!isNaN(num))
696
+ style.opacity = Math.max(0, Math.min(1, num));
550
697
  }
551
698
  break;
552
699
  }
553
700
  }
554
- const BORDER_STYLES = new Set(['none', 'single', 'double', 'rounded', 'heavy']);
701
+ const BORDER_STYLES = new Set([
702
+ 'none', 'single', 'double', 'rounded', 'heavy', 'ascii',
703
+ 'eighth-cell-inner', 'eighth-cell-outer',
704
+ 'half-cell-inner', 'half-cell-outer',
705
+ 'full-cell',
706
+ ]);
555
707
  function setIndividualBorderSide(style, side, value) {
556
708
  const enabled = value === 'true' || value === '1';
557
709
  // When setting individual sides, disable all others first (if this is the first individual side set)
@@ -576,14 +728,71 @@ function parseSizeOrCell(value) {
576
728
  return value;
577
729
  return parseCellValue(value);
578
730
  }
731
+ /** Parse grid-area: an area name, or numeric row-start / col-start / row-end / col-end lines. */
732
+ function parseGridArea(style, value) {
733
+ const trimmed = value.trim();
734
+ if (trimmed.includes('/')) {
735
+ const parts = trimmed.split('/').map(s => s.trim());
736
+ style.gridRowStart = parseIntOrNull(parts[0]);
737
+ if (parts.length > 1)
738
+ style.gridColumnStart = parseIntOrNull(parts[1]);
739
+ if (parts.length > 2)
740
+ style.gridRowEnd = parseIntOrNull(parts[2]);
741
+ if (parts.length > 3)
742
+ style.gridColumnEnd = parseIntOrNull(parts[3]);
743
+ return;
744
+ }
745
+ if (/^\d+$/.test(trimmed)) {
746
+ style.gridRowStart = parseInt(trimmed);
747
+ return;
748
+ }
749
+ style.gridArea = trimmed;
750
+ }
751
+ /** Parse a grid line value (grid-column/grid-row): span N | start / end | start */
752
+ function parseGridLine(value) {
753
+ const trimmed = value.trim();
754
+ if (trimmed.startsWith('span')) {
755
+ return { start: null, end: null, span: parseInt(trimmed.replace('span', '').trim()) || 1 };
756
+ }
757
+ const parts = trimmed.split('/').map(s => s.trim());
758
+ return {
759
+ start: parseIntOrNull(parts[0]),
760
+ end: parts.length === 2 ? parseIntOrNull(parts[1]) : null,
761
+ span: null,
762
+ };
763
+ }
764
+ function parseIntOrNull(s) {
765
+ const n = parseInt(s);
766
+ return isNaN(n) ? null : n;
767
+ }
768
+ const TIMING_KEYWORDS = new Set([
769
+ 'ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end',
770
+ ]);
771
+ const EASING_FUNCTION_PATTERN = /(?:cubic-bezier|steps)\([^)]*\)/;
772
+ /**
773
+ * Pull an easing function token (`cubic-bezier(...)`/`steps(...)`) out of a
774
+ * shorthand value so the remainder can be split on whitespace and commas
775
+ * without breaking inside the function's arguments.
776
+ */
777
+ function extractEasingFunction(value) {
778
+ const match = EASING_FUNCTION_PATTERN.exec(value);
779
+ if (!match)
780
+ return { easing: null, rest: value };
781
+ return { easing: match[0], rest: value.replace(match[0], ' ') };
782
+ }
579
783
  function parseAnimationShorthand(style, value) {
580
784
  if (value === 'none') {
581
785
  style.animationName = null;
582
786
  return;
583
787
  }
584
- const parts = value.split(/\s+/);
585
- for (const part of parts) {
586
- if (part.endsWith('s') && !part.endsWith('ss')) {
788
+ const { easing, rest } = extractEasingFunction(value);
789
+ if (easing)
790
+ style.animationTimingFunction = easing;
791
+ for (const part of rest.split(/\s+/)) {
792
+ if (TIMING_KEYWORDS.has(part)) {
793
+ style.animationTimingFunction = part;
794
+ }
795
+ else if (part.endsWith('s') && !part.endsWith('ss')) {
587
796
  style.animationDuration = parseDuration(part);
588
797
  }
589
798
  else if (part === 'infinite') {
@@ -592,11 +801,41 @@ function parseAnimationShorthand(style, value) {
592
801
  else if (/^\d+$/.test(part)) {
593
802
  style.animationIterationCount = parseInt(part);
594
803
  }
595
- else if (!['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'normal', 'reverse', 'alternate', 'forwards', 'backwards', 'both', 'running', 'paused'].includes(part)) {
804
+ else if (part && !['normal', 'reverse', 'alternate', 'forwards', 'backwards', 'both', 'running', 'paused'].includes(part)) {
596
805
  style.animationName = part;
597
806
  }
598
807
  }
599
808
  }
809
+ /** Parse `transition: <property> <duration> [...]`, comma-separated groups. */
810
+ function parseTransitionShorthand(style, value) {
811
+ const trimmed = value.trim();
812
+ if (trimmed === 'none') {
813
+ style.transitionProperty = null;
814
+ style.transitionDuration = 0;
815
+ return;
816
+ }
817
+ const { easing, rest } = extractEasingFunction(trimmed);
818
+ if (easing)
819
+ style.transitionTimingFunction = easing;
820
+ const properties = [];
821
+ let duration = 0;
822
+ for (const group of rest.split(',')) {
823
+ for (const token of group.trim().split(/\s+/)) {
824
+ if (/^\d*\.?\d+(ms|s)$/.test(token)) {
825
+ if (duration === 0)
826
+ duration = parseDuration(token);
827
+ }
828
+ else if (TIMING_KEYWORDS.has(token)) {
829
+ style.transitionTimingFunction = token;
830
+ }
831
+ else if (token) {
832
+ properties.push(token);
833
+ }
834
+ }
835
+ }
836
+ style.transitionProperty = properties.length > 0 ? properties.join(',') : 'all';
837
+ style.transitionDuration = duration;
838
+ }
600
839
  function parseDuration(value) {
601
840
  if (value.endsWith('ms'))
602
841
  return parseFloat(value);
@@ -5,4 +5,4 @@
5
5
  *
6
6
  * Adapts to dark/light mode via @media (prefers-color-scheme).
7
7
  */
8
- export declare const DEFAULT_STYLESHEET = "\nh1, h2, h3, h4, h5, h6 { font-weight: bold; }\nstrong, b { font-weight: bold; }\nem, i { font-style: italic; }\nu { text-decoration: underline; }\ns, del, strike { text-decoration: line-through; }\na { text-decoration: underline; }\npre { display: flex; }\np { margin-top: 1cell; margin-bottom: 1cell; }\nh1 { margin-top: 1cell; margin-bottom: 1cell; }\nh2 { margin-top: 1cell; margin-bottom: 1cell; }\nh3, h4, h5, h6 { margin-top: 1cell; margin-bottom: 1cell; }\nhr { height: 1cell; width: 100%; margin-top: 1cell; margin-bottom: 1cell; }\nblockquote { margin-left: 2cell; border-left: true; border-style: single; border-color: gray; padding-left: 1cell; }\nli { padding-left: 3cell; }\nul, ol { margin-top: 1cell; margin-bottom: 1cell; }\nmark { background-color: yellow; color: black; }\nkbd { border: single; border-color: gray; padding: 0 1cell; }\nabbr { text-decoration: underline; }\nsamp { color: cyan; }\nvar { font-style: italic; }\ndt { font-weight: bold; }\ndd { margin-left: 2cell; }\nfigure { margin-left: 2cell; margin-right: 2cell; margin-top: 1cell; margin-bottom: 1cell; }\nfigcaption { font-style: italic; }\n\n@media (prefers-color-scheme: dark) {\n code { color: cyan; }\n a { color: #5599ff; }\n mark { background-color: #666600; color: white; }\n}\n\n@media (prefers-color-scheme: light) {\n code { color: #8800cc; }\n a { color: #0044cc; }\n}\n";
8
+ export declare const DEFAULT_STYLESHEET = "\nh1, h2, h3, h4, h5, h6 { font-weight: bold; }\nstrong, b { font-weight: bold; }\nem, i { font-style: italic; }\nu { text-decoration: underline; }\ns, del, strike { text-decoration: line-through; }\na { text-decoration: underline; }\npre { display: flex; }\np { margin-top: 1cell; margin-bottom: 1cell; }\nh1 { margin-top: 1cell; margin-bottom: 1cell; }\nh2 { margin-top: 1cell; margin-bottom: 1cell; }\nh3, h4, h5, h6 { margin-top: 1cell; margin-bottom: 1cell; }\nhr { height: 1cell; width: 100%; margin-top: 1cell; margin-bottom: 1cell; }\nblockquote { margin-left: 2cell; border-left: true; border-style: single; border-color: gray; padding-left: 1cell; }\nli { padding-left: 3cell; }\nul, ol { margin-top: 1cell; margin-bottom: 1cell; }\nmark { background-color: yellow; color: black; }\nkbd { border: single; border-color: gray; padding: 0 1cell; }\nprogress, meter { display: inline-block; width: 20cell; height: 1cell; }\ninput[type=\"checkbox\"], input[type=\"radio\"] { display: inline-block; width: 3cell; height: 1cell; }\ndetails:not([open]) > :not(summary) { display: none; }\nsummary { padding-left: 2cell; }\nselect { display: inline-block; height: 1cell; }\nselect option, select optgroup { display: none; }\nabbr { text-decoration: underline; }\nsamp { color: cyan; }\nvar { font-style: italic; }\ndt { font-weight: bold; }\ndd { margin-left: 2cell; }\ntable { border-spacing: 2cell 0; }\nth { font-weight: bold; text-align: center; }\ncaption { text-align: center; }\nfigure { margin-left: 2cell; margin-right: 2cell; margin-top: 1cell; margin-bottom: 1cell; }\nfigcaption { font-style: italic; }\n\n@media (prefers-color-scheme: dark) {\n code { color: cyan; }\n a { color: #5599ff; }\n mark { background-color: #666600; color: white; }\n}\n\n@media (prefers-color-scheme: light) {\n code { color: #8800cc; }\n a { color: #0044cc; }\n}\n";
@@ -23,11 +23,20 @@ li { padding-left: 3cell; }
23
23
  ul, ol { margin-top: 1cell; margin-bottom: 1cell; }
24
24
  mark { background-color: yellow; color: black; }
25
25
  kbd { border: single; border-color: gray; padding: 0 1cell; }
26
+ progress, meter { display: inline-block; width: 20cell; height: 1cell; }
27
+ input[type="checkbox"], input[type="radio"] { display: inline-block; width: 3cell; height: 1cell; }
28
+ details:not([open]) > :not(summary) { display: none; }
29
+ summary { padding-left: 2cell; }
30
+ select { display: inline-block; height: 1cell; }
31
+ select option, select optgroup { display: none; }
26
32
  abbr { text-decoration: underline; }
27
33
  samp { color: cyan; }
28
34
  var { font-style: italic; }
29
35
  dt { font-weight: bold; }
30
36
  dd { margin-left: 2cell; }
37
+ table { border-spacing: 2cell 0; }
38
+ th { font-weight: bold; text-align: center; }
39
+ caption { text-align: center; }
31
40
  figure { margin-left: 2cell; margin-right: 2cell; margin-top: 1cell; margin-bottom: 1cell; }
32
41
  figcaption { font-style: italic; }
33
42
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * CSS easing functions: keywords, cubic-bezier(), and steps().
3
+ *
4
+ * An easing maps segment progress t ∈ [0, 1] to eased progress. Keyframe
5
+ * animations apply it per segment; transitions over their single segment.
6
+ */
7
+ export type Easing = (t: number) => number;
8
+ /** Parse a CSS easing value; null when the value is not a valid easing. */
9
+ export declare function parseEasing(value: string): Easing | null;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CSS easing functions: keywords, cubic-bezier(), and steps().
3
+ *
4
+ * An easing maps segment progress t ∈ [0, 1] to eased progress. Keyframe
5
+ * animations apply it per segment; transitions over their single segment.
6
+ */
7
+ const KEYWORD_CONTROL_POINTS = {
8
+ 'ease': [0.25, 0.1, 0.25, 1],
9
+ 'ease-in': [0.42, 0, 1, 1],
10
+ 'ease-out': [0, 0, 0.58, 1],
11
+ 'ease-in-out': [0.42, 0, 0.58, 1],
12
+ };
13
+ /** Parse a CSS easing value; null when the value is not a valid easing. */
14
+ export function parseEasing(value) {
15
+ const trimmed = value.trim();
16
+ if (trimmed === 'linear')
17
+ return t => t;
18
+ if (trimmed === 'step-start')
19
+ return steps(1, 'start');
20
+ if (trimmed === 'step-end')
21
+ return steps(1, 'end');
22
+ const preset = KEYWORD_CONTROL_POINTS[trimmed];
23
+ if (preset)
24
+ return cubicBezier(...preset);
25
+ if (trimmed.startsWith('cubic-bezier('))
26
+ return parseCubicBezier(trimmed);
27
+ if (trimmed.startsWith('steps('))
28
+ return parseSteps(trimmed);
29
+ return null;
30
+ }
31
+ function parseCubicBezier(value) {
32
+ const args = functionArguments(value, 'cubic-bezier');
33
+ if (args === null || args.length !== 4)
34
+ return null;
35
+ const numbers = args.map(Number);
36
+ if (numbers.some(isNaN))
37
+ return null;
38
+ const [x1, y1, x2, y2] = numbers;
39
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1)
40
+ return null;
41
+ return cubicBezier(x1, y1, x2, y2);
42
+ }
43
+ function parseSteps(value) {
44
+ const args = functionArguments(value, 'steps');
45
+ if (args === null || args.length < 1 || args.length > 2)
46
+ return null;
47
+ const count = parseInt(args[0], 10);
48
+ if (isNaN(count) || count <= 0 || String(count) !== args[0])
49
+ return null;
50
+ const position = args[1] ?? 'end';
51
+ if (!['start', 'end', 'jump-start', 'jump-end'].includes(position))
52
+ return null;
53
+ return steps(count, position.endsWith('start') ? 'start' : 'end');
54
+ }
55
+ /** The comma-separated arguments of `name(...)`, or null if malformed. */
56
+ function functionArguments(value, name) {
57
+ if (!value.startsWith(`${name}(`) || !value.endsWith(')'))
58
+ return null;
59
+ return value.slice(name.length + 1, -1).split(',').map(s => s.trim());
60
+ }
61
+ function steps(count, position) {
62
+ return t => {
63
+ if (t >= 1)
64
+ return 1;
65
+ const step = position === 'start' ? Math.ceil(t * count) : Math.floor(t * count);
66
+ return Math.min(step / count, 1);
67
+ };
68
+ }
69
+ /**
70
+ * A CSS cubic bezier: fixed endpoints (0,0) and (1,1) with the given
71
+ * control points. Evaluated by solving x(s) = t for the curve parameter s
72
+ * (bisection — x is monotone since x1, x2 ∈ [0, 1]), then reading y(s).
73
+ */
74
+ function cubicBezier(x1, y1, x2, y2) {
75
+ const sample = (p1, p2, s) => {
76
+ const inv = 1 - s;
77
+ return 3 * inv * inv * s * p1 + 3 * inv * s * s * p2 + s * s * s;
78
+ };
79
+ return t => {
80
+ if (t <= 0)
81
+ return 0;
82
+ if (t >= 1)
83
+ return 1;
84
+ let low = 0;
85
+ let high = 1;
86
+ for (let i = 0; i < 32; i++) {
87
+ const mid = (low + high) / 2;
88
+ if (sample(x1, x2, mid) < t)
89
+ low = mid;
90
+ else
91
+ high = mid;
92
+ }
93
+ return sample(y1, y2, (low + high) / 2);
94
+ };
95
+ }
@@ -6,4 +6,4 @@ import { type ResolvedStyle } from './compute.js';
6
6
  * descendants using the same resolveNode function as full resolution.
7
7
  * Variables are collected once from the full tree for consistency.
8
8
  */
9
- export declare function resolveStylesIncremental(root: TermNode, stylesheet: CSSStyleSheet, existingStyles: Map<number, ResolvedStyle>, dirtyNodes: Set<TermNode>, onResolve?: (nodeId: number) => void, onLayoutAffected?: (node: TermNode) => void): Map<number, ResolvedStyle>;
9
+ export declare function resolveStylesIncremental(root: TermNode, stylesheet: CSSStyleSheet, existingStyles: Map<number, ResolvedStyle>, dirtyNodes: Set<TermNode>, onResolve?: (nodeId: number) => void, onLayoutAffected?: (node: TermNode) => void, scheme?: 'dark' | 'light'): Map<number, ResolvedStyle>;