@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Yandell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # svelterm
2
+
3
+ Svelte 5 components rendered to the terminal with real CSS.
4
+
5
+ Write standard Svelte components with `<style>` blocks. They render in the terminal with ANSI escape sequences — flexbox layout, scoped styles, CSS variables, pseudo-classes, all on a cell grid.
6
+
7
+ > **Early release.** Svelterm requires an unmerged Svelte branch (`svelte-custom-renderer` by [@paoloricciuti](https://github.com/paoloricciuti)) that adds the custom renderer API. It is not usable with mainline Svelte yet.
8
+
9
+ ## Example
10
+
11
+ ```svelte
12
+ <script>
13
+ let count = $state(0)
14
+ </script>
15
+
16
+ <style>
17
+ .counter {
18
+ display: flex;
19
+ flex-direction: column;
20
+ border: rounded;
21
+ border-color: cyan;
22
+ padding: 1cell;
23
+ gap: 1cell;
24
+ }
25
+
26
+ .value {
27
+ color: yellow;
28
+ font-weight: bold;
29
+ }
30
+
31
+ button:focus {
32
+ color: cyan;
33
+ font-weight: bold;
34
+ }
35
+ </style>
36
+
37
+ <div class="counter">
38
+ <span>Count: <span class="value">{count}</span></span>
39
+ <button onclick={() => count++}>Increment</button>
40
+ <button onclick={() => count--}>Decrement</button>
41
+ </div>
42
+ ```
43
+
44
+ ```typescript
45
+ import { run } from '@svelterm/core/app'
46
+ import { readFileSync } from 'fs'
47
+ import App from './App.svelte'
48
+
49
+ const css = readFileSync('./main.css', 'utf-8')
50
+ run(App, { css })
51
+ ```
52
+
53
+ ## Dual-target components
54
+
55
+ The same Svelte component can render in both terminal and browser. Terminal-specific CSS values (`border: rounded`, `1cell`, `opacity: dim`) are naturally ignored by browsers — they're invalid CSS. Browser-specific rules go in `@media (display-mode: screen)`.
56
+
57
+ ```svelte
58
+ <style>
59
+ .greeting {
60
+ border: rounded;
61
+ border-color: cyan;
62
+ padding: 1cell;
63
+ }
64
+
65
+ @media (display-mode: screen) {
66
+ .greeting {
67
+ border: 2px solid #00b4d8;
68
+ border-radius: 8px;
69
+ padding: 1rem;
70
+ }
71
+ }
72
+ </style>
73
+ ```
74
+
75
+ To build for each target, use separate Vite configs — one with `customRenderer: '@svelterm/core'` for terminal, one without for browser. The component source is the same.
76
+
77
+ ## What's different in terminal CSS
78
+
79
+ Standard CSS works as expected. These are the terminal-specific additions:
80
+
81
+ | Feature | Terminal | Browser |
82
+ |---------|----------|---------|
83
+ | **Borders** | `single`, `double`, `rounded`, `heavy` (box-drawing characters) | Ignored (invalid values) |
84
+ | **Units** | `cell` — one monospace character position | Ignored (unknown unit) |
85
+ | **Opacity** | `dim` — terminal dim attribute | Ignored (invalid value) |
86
+ | **Colors** | ANSI names, 256-color, truecolor hex, CSS named colors | Standard CSS colors |
87
+ | **Media** | `@media (display-mode: terminal)` | `@media (display-mode: screen)` |
88
+
89
+ ## Features
90
+
91
+ - **CSS engine** — selectors, specificity, cascade, inheritance, scoped styles, `var()`, `calc()`, `@media`, `@keyframes`
92
+ - **Flexbox layout** — `flex-direction`, `justify-content`, `align-items`, `flex-grow`, `flex-shrink`, `gap`, `flex-wrap`
93
+ - **Terminal rendering** — ANSI colors (16, 256, truecolor), borders, text styles, differential output
94
+ - **Input** — keyboard events, mouse click and scroll, focus management with Tab/Shift+Tab, `:focus` and `:hover` pseudo-classes
95
+ - **Text input** — `<input>` and `<textarea>` with readline-like editing
96
+ - **Incremental updates** — mutation tracking classifies changes as paint-only, style-resolve, or layout to avoid full recomputation
97
+ - **Color scheme** — automatic `prefers-color-scheme` detection via terminal queries
98
+
99
+ ## Prerequisites
100
+
101
+ Svelterm requires the experimental custom renderer API, available on the [`svelte-custom-renderer`](https://github.com/paoloricciuti/svelte/tree/svelte-custom-renderer) branch:
102
+
103
+ ```bash
104
+ # Clone the branch
105
+ git clone -b svelte-custom-renderer https://github.com/paoloricciuti/svelte.git svelte-fork
106
+ cd svelte-fork
107
+ pnpm install
108
+ pnpm -C packages/svelte build
109
+ ```
110
+
111
+ Then reference it in your project's `package.json`:
112
+
113
+ ```json
114
+ {
115
+ "peerDependencies": {
116
+ "svelte": "file:../svelte-fork/packages/svelte"
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Setup
122
+
123
+ Configure the Svelte compiler to use svelterm as the custom renderer:
124
+
125
+ ```typescript
126
+ // vite.config.ts
127
+ import { defineConfig } from 'vite'
128
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
129
+
130
+ export default defineConfig({
131
+ plugins: [
132
+ svelte({
133
+ compilerOptions: {
134
+ experimental: {
135
+ customRenderer: '@svelterm/core',
136
+ },
137
+ css: 'external',
138
+ },
139
+ }),
140
+ ],
141
+ build: {
142
+ target: 'node22',
143
+ rollupOptions: {
144
+ external: ['svelte', 'svelte/renderer', 'svelte/internal',
145
+ 'svelte/internal/client', 'ws', 'http', 'crypto'],
146
+ },
147
+ },
148
+ })
149
+ ```
150
+
151
+ ## API
152
+
153
+ ### `run(component, options?)`
154
+
155
+ Start an interactive terminal application.
156
+
157
+ ```typescript
158
+ import { run } from '@svelterm/core/app'
159
+
160
+ const stop = run(App, {
161
+ css, // Extracted CSS string
162
+ fullscreen: true, // Use alternate screen buffer (default: true)
163
+ mouse: true, // Enable mouse input (default: true)
164
+ props: { name: 'world' },
165
+ })
166
+
167
+ // Call stop() to shut down and restore terminal
168
+ ```
169
+
170
+ Returns a function that stops the application, unmounts the component, and restores the terminal.
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,11 @@
1
+ export declare const SPINNER_DOTS: string[];
2
+ export declare const SPINNER_LINE: string[];
3
+ export declare const SPINNER_BRAILLE: string[];
4
+ export declare class Spinner {
5
+ private frames;
6
+ private index;
7
+ constructor(frames?: string[]);
8
+ get frame(): string;
9
+ tick(): void;
10
+ reset(): void;
11
+ }
@@ -0,0 +1,19 @@
1
+ export const SPINNER_DOTS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2
+ export const SPINNER_LINE = ['|', '/', '-', '\\'];
3
+ export const SPINNER_BRAILLE = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
4
+ export class Spinner {
5
+ frames;
6
+ index = 0;
7
+ constructor(frames = SPINNER_DOTS) {
8
+ this.frames = frames;
9
+ }
10
+ get frame() {
11
+ return this.frames[this.index];
12
+ }
13
+ tick() {
14
+ this.index = (this.index + 1) % this.frames.length;
15
+ }
16
+ reset() {
17
+ this.index = 0;
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import type { KeyEvent } from '../input/keyboard.js';
2
+ export declare class TextBuffer {
3
+ private _text;
4
+ private _cursor;
5
+ constructor(initial?: string);
6
+ get text(): string;
7
+ set text(value: string);
8
+ get cursor(): number;
9
+ set cursor(value: number);
10
+ insert(chars: string): void;
11
+ delete(): void;
12
+ backspace(): void;
13
+ moveLeft(): void;
14
+ moveRight(): void;
15
+ home(): void;
16
+ end(): void;
17
+ clearToStart(): void;
18
+ clearToEnd(): void;
19
+ handleKey(key: KeyEvent): boolean;
20
+ private handleCtrl;
21
+ }
@@ -0,0 +1,87 @@
1
+ export class TextBuffer {
2
+ _text;
3
+ _cursor;
4
+ constructor(initial = '') {
5
+ this._text = initial;
6
+ this._cursor = initial.length;
7
+ }
8
+ get text() { return this._text; }
9
+ set text(value) { this._text = value; }
10
+ get cursor() { return this._cursor; }
11
+ set cursor(value) {
12
+ this._cursor = Math.max(0, Math.min(value, this._text.length));
13
+ }
14
+ insert(chars) {
15
+ this._text = this._text.substring(0, this._cursor) + chars + this._text.substring(this._cursor);
16
+ this._cursor += chars.length;
17
+ }
18
+ delete() {
19
+ if (this._cursor >= this._text.length)
20
+ return;
21
+ this._text = this._text.substring(0, this._cursor) + this._text.substring(this._cursor + 1);
22
+ }
23
+ backspace() {
24
+ if (this._cursor <= 0)
25
+ return;
26
+ this._text = this._text.substring(0, this._cursor - 1) + this._text.substring(this._cursor);
27
+ this._cursor--;
28
+ }
29
+ moveLeft() { this.cursor--; }
30
+ moveRight() { this.cursor++; }
31
+ home() { this._cursor = 0; }
32
+ end() { this._cursor = this._text.length; }
33
+ clearToStart() {
34
+ this._text = this._text.substring(this._cursor);
35
+ this._cursor = 0;
36
+ }
37
+ clearToEnd() {
38
+ this._text = this._text.substring(0, this._cursor);
39
+ }
40
+ handleKey(key) {
41
+ if (key.ctrl)
42
+ return this.handleCtrl(key.key);
43
+ switch (key.key) {
44
+ case 'Backspace':
45
+ this.backspace();
46
+ return true;
47
+ case 'Delete':
48
+ this.delete();
49
+ return true;
50
+ case 'ArrowLeft':
51
+ this.moveLeft();
52
+ return true;
53
+ case 'ArrowRight':
54
+ this.moveRight();
55
+ return true;
56
+ case 'Home':
57
+ this.home();
58
+ return true;
59
+ case 'End':
60
+ this.end();
61
+ return true;
62
+ default:
63
+ if (key.key.length === 1) {
64
+ this.insert(key.key);
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ }
70
+ handleCtrl(key) {
71
+ switch (key) {
72
+ case 'a':
73
+ this.home();
74
+ return true;
75
+ case 'e':
76
+ this.end();
77
+ return true;
78
+ case 'u':
79
+ this.clearToStart();
80
+ return true;
81
+ case 'k':
82
+ this.clearToEnd();
83
+ return true;
84
+ default: return false;
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,17 @@
1
+ import type { KeyframeStop } from './parser.js';
2
+ import type { ResolvedStyle } from './compute.js';
3
+ /**
4
+ * Runs a CSS animation by applying keyframe properties at the current time.
5
+ * Terminal animations are discrete (no interpolation between color values).
6
+ */
7
+ export declare class AnimationRunner {
8
+ private keyframes;
9
+ private duration;
10
+ private iterations;
11
+ constructor(keyframes: KeyframeStop[], durationMs: number, iterations: number);
12
+ /** Apply the appropriate keyframe declarations to a style at the given elapsed time */
13
+ apply(style: ResolvedStyle, elapsedMs: number): void;
14
+ isFinished(elapsedMs: number): boolean;
15
+ private getProgress;
16
+ private getKeyframeAt;
17
+ }
@@ -0,0 +1,72 @@
1
+ import { resolveColor } from './color.js';
2
+ /**
3
+ * Runs a CSS animation by applying keyframe properties at the current time.
4
+ * Terminal animations are discrete (no interpolation between color values).
5
+ */
6
+ export class AnimationRunner {
7
+ keyframes;
8
+ duration;
9
+ iterations;
10
+ constructor(keyframes, durationMs, iterations) {
11
+ this.keyframes = keyframes.sort((a, b) => a.offset - b.offset);
12
+ this.duration = durationMs;
13
+ this.iterations = iterations;
14
+ }
15
+ /** Apply the appropriate keyframe declarations to a style at the given elapsed time */
16
+ apply(style, elapsedMs) {
17
+ if (this.keyframes.length === 0 || this.duration <= 0)
18
+ return;
19
+ const progress = this.getProgress(elapsedMs);
20
+ const kf = this.getKeyframeAt(progress);
21
+ if (!kf)
22
+ return;
23
+ for (const decl of kf.declarations) {
24
+ applyAnimatedProperty(style, decl);
25
+ }
26
+ }
27
+ isFinished(elapsedMs) {
28
+ if (this.iterations === Infinity)
29
+ return false;
30
+ return elapsedMs >= this.duration * this.iterations;
31
+ }
32
+ getProgress(elapsedMs) {
33
+ if (this.iterations === Infinity) {
34
+ return (elapsedMs % this.duration) / this.duration;
35
+ }
36
+ const totalDuration = this.duration * this.iterations;
37
+ if (elapsedMs >= totalDuration)
38
+ return 1; // finished — hold at end
39
+ return (elapsedMs % this.duration) / this.duration;
40
+ }
41
+ getKeyframeAt(progress) {
42
+ // Discrete: find the last keyframe whose offset <= progress
43
+ let result = null;
44
+ for (const kf of this.keyframes) {
45
+ if (kf.offset <= progress)
46
+ result = kf;
47
+ else
48
+ break;
49
+ }
50
+ return result ?? this.keyframes[0];
51
+ }
52
+ }
53
+ function applyAnimatedProperty(style, decl) {
54
+ switch (decl.property) {
55
+ case 'color':
56
+ style.fg = resolveColor(decl.value);
57
+ break;
58
+ case 'background-color':
59
+ case 'background':
60
+ style.bg = resolveColor(decl.value);
61
+ break;
62
+ case 'font-weight':
63
+ style.bold = decl.value === 'bold' || parseInt(decl.value) >= 700;
64
+ break;
65
+ case 'font-style':
66
+ style.italic = decl.value === 'italic';
67
+ break;
68
+ case 'opacity':
69
+ style.dim = decl.value === 'dim' || (parseFloat(decl.value) < 1);
70
+ break;
71
+ }
72
+ }
@@ -0,0 +1,5 @@
1
+ import type { CSSStyleSheet, KeyframeStop } from './parser.js';
2
+ /**
3
+ * Extract keyframe definitions from a parsed stylesheet.
4
+ */
5
+ export declare function getKeyframes(sheet: CSSStyleSheet): Map<string, KeyframeStop[]>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Extract keyframe definitions from a parsed stylesheet.
3
+ */
4
+ export function getKeyframes(sheet) {
5
+ return sheet.keyframes;
6
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Evaluate CSS math functions: calc(), min(), max(), clamp().
3
+ * Returns the computed cell value, or null if the input is not a math function.
4
+ */
5
+ export declare function evaluateCalc(value: string, available: number): number | null;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Evaluate CSS math functions: calc(), min(), max(), clamp().
3
+ * Returns the computed cell value, or null if the input is not a math function.
4
+ */
5
+ export function evaluateCalc(value, available) {
6
+ const trimmed = value.trim();
7
+ if (trimmed.startsWith('calc(')) {
8
+ const expr = trimmed.slice(5, -1).trim();
9
+ return Math.round(evaluateExpression(expr, available));
10
+ }
11
+ if (trimmed.startsWith('min(')) {
12
+ const args = splitArgs(trimmed.slice(4, -1));
13
+ const values = args.map(a => resolveValue(a.trim(), available));
14
+ return Math.round(Math.min(...values));
15
+ }
16
+ if (trimmed.startsWith('max(')) {
17
+ const args = splitArgs(trimmed.slice(4, -1));
18
+ const values = args.map(a => resolveValue(a.trim(), available));
19
+ return Math.round(Math.max(...values));
20
+ }
21
+ if (trimmed.startsWith('clamp(')) {
22
+ const args = splitArgs(trimmed.slice(6, -1));
23
+ if (args.length !== 3)
24
+ return null;
25
+ const min = resolveValue(args[0].trim(), available);
26
+ const preferred = resolveValue(args[1].trim(), available);
27
+ const max = resolveValue(args[2].trim(), available);
28
+ return Math.round(Math.min(Math.max(preferred, min), max));
29
+ }
30
+ return null;
31
+ }
32
+ function evaluateExpression(expr, available) {
33
+ // Tokenize: split on +, -, *, / while preserving the operators
34
+ const tokens = tokenize(expr);
35
+ if (tokens.length === 0)
36
+ return 0;
37
+ // First pass: resolve values
38
+ const values = [];
39
+ const ops = [];
40
+ for (const token of tokens) {
41
+ if (token === '+' || token === '-' || token === '*' || token === '/') {
42
+ ops.push(token);
43
+ }
44
+ else {
45
+ values.push(resolveValue(token.trim(), available));
46
+ }
47
+ }
48
+ // Second pass: * and / first
49
+ let i = 0;
50
+ while (i < ops.length) {
51
+ if (ops[i] === '*') {
52
+ values[i] = values[i] * values[i + 1];
53
+ values.splice(i + 1, 1);
54
+ ops.splice(i, 1);
55
+ }
56
+ else if (ops[i] === '/') {
57
+ values[i] = values[i] / values[i + 1];
58
+ values.splice(i + 1, 1);
59
+ ops.splice(i, 1);
60
+ }
61
+ else {
62
+ i++;
63
+ }
64
+ }
65
+ // Third pass: + and -
66
+ let result = values[0];
67
+ for (let j = 0; j < ops.length; j++) {
68
+ if (ops[j] === '+')
69
+ result += values[j + 1];
70
+ else if (ops[j] === '-')
71
+ result -= values[j + 1];
72
+ }
73
+ return result;
74
+ }
75
+ function tokenize(expr) {
76
+ const tokens = [];
77
+ let current = '';
78
+ for (let i = 0; i < expr.length; i++) {
79
+ const ch = expr[i];
80
+ if ((ch === '+' || ch === '-') && current.trim() && i > 0 && expr[i - 1] !== '(') {
81
+ tokens.push(current.trim());
82
+ tokens.push(ch);
83
+ current = '';
84
+ }
85
+ else if (ch === '*' || ch === '/') {
86
+ tokens.push(current.trim());
87
+ tokens.push(ch);
88
+ current = '';
89
+ }
90
+ else {
91
+ current += ch;
92
+ }
93
+ }
94
+ if (current.trim())
95
+ tokens.push(current.trim());
96
+ return tokens.filter(Boolean);
97
+ }
98
+ function resolveValue(value, available) {
99
+ const trimmed = value.trim();
100
+ if (trimmed.endsWith('%')) {
101
+ return available * parseFloat(trimmed) / 100;
102
+ }
103
+ if (trimmed.endsWith('cell')) {
104
+ return parseFloat(trimmed);
105
+ }
106
+ // Bare number (for multipliers like * 2)
107
+ const num = parseFloat(trimmed);
108
+ return isNaN(num) ? 0 : num;
109
+ }
110
+ function splitArgs(argsStr) {
111
+ const args = [];
112
+ let current = '';
113
+ let depth = 0;
114
+ for (const ch of argsStr) {
115
+ if (ch === '(')
116
+ depth++;
117
+ if (ch === ')')
118
+ depth--;
119
+ if (ch === ',' && depth === 0) {
120
+ args.push(current);
121
+ current = '';
122
+ }
123
+ else {
124
+ current += ch;
125
+ }
126
+ }
127
+ if (current.trim())
128
+ args.push(current);
129
+ return args;
130
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveColor(value: string): string;