Skip to content

Commit 0ecc004

Browse files
committed
Add syntax
1 parent ea6d0bc commit 0ecc004

2 files changed

Lines changed: 53 additions & 31 deletions

File tree

index.html

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@
6565
.output-panel { display: none; padding: 0.5rem 0; }
6666
.output-panel.active { display: block; }
6767
#ast { font-size: 0.875rem; color: var(--muted); white-space: pre; line-height: 1.5; }
68+
#ast .op { color: #e3a857; }
69+
#ast .lit { color: #a5d6ff; }
70+
#ast .node { cursor: pointer; position: relative; }
71+
#ast .node:hover:not(:has(.node:hover)) .bracket { color: var(--accent); }
72+
#ast .node:hover:not(:has(.node:hover)) > .line { background: var(--accent); }
73+
#ast .line { position: absolute; left: calc(0.25ch + 2px); top: 1.5em; bottom: calc(1.1em + 4px); width: 1px; background: var(--border); opacity: 0.4; pointer-events: none; }
74+
#ast .node.collapsed > .args { display: none; }
75+
#ast .node.collapsed > .line { display: none; }
76+
#ast .node.collapsed > .ellipsis { display: inline; }
77+
#ast .ellipsis { display: none; color: var(--muted); }
6878

6979
/* Eval section */
7080
.eval-content { padding: 0.5rem 0; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem; }
@@ -222,12 +232,17 @@ <h1><a href="https://github.com/dy/subscript" class="logo-link" target="_blank"
222232

223233
<div class="output-section">
224234
<div class="output-tabs">
225-
<button class="output-tab active" data-tab="eval">Eval</button>
226-
<button class="output-tab" data-tab="tree">Tree</button>
235+
<button class="output-tab active" data-tab="tree">Tree</button>
236+
<button class="output-tab" data-tab="eval">Eval</button>
237+
</div>
238+
239+
<!-- Tree panel -->
240+
<div class="output-panel active" id="treePanel">
241+
<pre id="ast" data-testid="ast"></pre>
227242
</div>
228243

229244
<!-- Eval panel -->
230-
<div class="output-panel active" id="evalPanel">
245+
<div class="output-panel" id="evalPanel">
231246
<div class="eval-content">
232247
<div class="eval-field">
233248
<span class="label">Context</span>
@@ -247,11 +262,6 @@ <h1><a href="https://github.com/dy/subscript" class="logo-link" target="_blank"
247262
</div>
248263
</div>
249264
</div>
250-
251-
<!-- Tree panel -->
252-
<div class="output-panel" id="treePanel">
253-
<pre id="ast"></pre>
254-
</div>
255265
</div>
256266
</main>
257267

@@ -942,7 +952,7 @@ <h1><a href="https://github.com/dy/subscript" class="logo-link" target="_blank"
942952
editorEl.classList.remove('has-error')
943953
if (ast !== undefined) {
944954
currentAST = ast
945-
astEl.textContent = formatAST(ast)
955+
astEl.innerHTML = formatAST(ast)
946956
errorEl.textContent = ''
947957
}
948958
if (result !== undefined) {
@@ -974,27 +984,31 @@ <h1><a href="https://github.com/dy/subscript" class="logo-link" target="_blank"
974984

975985
// AST formatter
976986
function formatAST(node, depth = 0) {
977-
if (node === undefined) return 'undefined'
978-
if (node === null) return 'null'
979-
if (!Array.isArray(node)) return JSON.stringify(node)
987+
const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
988+
const cls = v => typeof v === 'string' && /^[+\-*\/%&|^~!<>=?.,:;@#()\[\]{}]|^(\|\||&&|\?\?|\*\*|=>|\.\.\.|if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally|function|class|let|const|var|new|delete|typeof|void|in|of|instanceof|async|await|yield|import|export|default|extends|static|get|set)$/.test(v) ? 'op' : 'lit'
989+
const fmt = v => `<span class="${cls(v)}">${esc(JSON.stringify(v))}</span>`
990+
const stop = `onclick="event.stopPropagation()"`
991+
992+
if (node === undefined) return '<span class="lit">undefined</span>'
993+
if (node === null) return '<span class="lit">null</span>'
994+
if (!Array.isArray(node)) return fmt(node)
980995
if (node.length === 0) return '[]'
981996

982997
const indent = ' '.repeat(depth)
983998
const inner = ' '.repeat(depth + 1)
984999

985-
if (node[0] === undefined && node.length === 2) return `[, ${JSON.stringify(node[1])}]`
1000+
if (node[0] === undefined && node.length === 2) return `<span class="node" ${stop}><span class="bracket">[</span>, ${fmt(node[1])}<span class="bracket">]</span></span>`
9861001

987-
const simple = node.every((n, i) => !Array.isArray(n) || (i > 0 && JSON.stringify(n).length < 20))
988-
if (simple) {
989-
const inline = `[${node.map((n, i) => i === 0 && n === undefined ? '' : (Array.isArray(n) ? formatAST(n, 0) : JSON.stringify(n))).join(', ')}]`
990-
if (inline.length < 60) return inline
1002+
// Inline if all args are plain (no nested arrays) or all nested are short
1003+
const short = n => !Array.isArray(n) || JSON.stringify(n).length < 30
1004+
if (node.every(short)) {
1005+
const parts = node.map((n, i) => i === 0 && n === undefined ? '' : (Array.isArray(n) ? formatAST(n, 0) : fmt(n))).join(', ')
1006+
return `<span class="node" ${stop}><span class="bracket">[</span>${parts}<span class="bracket">]</span></span>`
9911007
}
9921008

993-
const parts = node.map((n, i) => {
994-
if (i === 0 && n === undefined) return ''
995-
return Array.isArray(n) ? formatAST(n, depth + 1) : JSON.stringify(n)
996-
})
997-
return `[${parts[0]},\n${inner}${parts.slice(1).join(`,\n${inner}`)}\n${indent}]`
1009+
const op = node[0] !== undefined ? fmt(node[0]) : ''
1010+
const args = node.slice(1).map(n => Array.isArray(n) ? formatAST(n, depth + 1) : fmt(n))
1011+
return `<span class="node" onclick="this.classList.toggle('collapsed');event.stopPropagation()"><span class="bracket">[</span>${op}<span class="ellipsis">…</span><span class="args">,\n${inner}${args.join(`,\n${inner}`)}\n${indent}</span><span class="bracket">]</span><span class="line"></span></span>`
9981012
}
9991013

10001014
// Compact JSON formatter - multiline but minimal

test/index.spec.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@ test.describe('Subscript REPL', () => {
3232
})
3333

3434
test('runs expression with Run button', async ({ page }) => {
35+
const evalTab = page.locator('.output-tab[data-tab="eval"]')
3536
const runBtn = page.locator('#runBtn')
3637
const result = page.locator('[data-testid="result"]')
3738

39+
await evalTab.click()
3840
await runBtn.click()
3941
await expect(result).not.toBeEmpty({ timeout: 2000 })
4042
})
4143

4244
test('shows eval time after run', async ({ page }) => {
45+
const evalTab = page.locator('.output-tab[data-tab="eval"]')
4346
const runBtn = page.locator('#runBtn')
4447
const evalTime = page.locator('#evalTime')
4548

49+
await evalTab.click()
4650
await runBtn.click()
4751
await expect(evalTime).toContainText('ms', { timeout: 2000 })
4852
})
@@ -64,11 +68,13 @@ test.describe('Subscript REPL', () => {
6468
})
6569

6670
test('uses context in evaluation', async ({ page }) => {
71+
const evalTab = page.locator('.output-tab[data-tab="eval"]')
6772
const input = page.locator('#input')
6873
const context = page.locator('#context')
6974
const result = page.locator('[data-testid="result"]')
7075
const runBtn = page.locator('#runBtn')
7176

77+
await evalTab.click()
7278
await input.fill('a + b')
7379
await context.fill('{"a": 5, "b": 3}')
7480
await runBtn.click()
@@ -93,6 +99,7 @@ test.describe('Subscript REPL', () => {
9399
})
94100

95101
test('justin preset supports arrows', async ({ page }) => {
102+
const evalTab = page.locator('.output-tab[data-tab="eval"]')
96103
const preset = page.locator('[data-testid="preset"]')
97104
const input = page.locator('#input')
98105
const ast = page.locator('#ast')
@@ -102,6 +109,7 @@ test.describe('Subscript REPL', () => {
102109
await preset.selectOption('justin')
103110
await input.fill('[1,2,3].map(x => x * 2)')
104111
await expect(ast).not.toBeEmpty({ timeout: 2000 })
112+
await evalTab.click()
105113
await runBtn.click()
106114
await expect(result).toHaveText('[2,4,6]', { timeout: 2000 })
107115
})
@@ -136,20 +144,13 @@ test.describe('Subscript REPL', () => {
136144
expect(justinCode.length).toBeGreaterThan(0)
137145
})
138146

139-
test('tabs switch between Eval and Tree', async ({ page }) => {
147+
test('tabs switch between Tree and Eval', async ({ page }) => {
140148
const evalTab = page.locator('.output-tab[data-tab="eval"]')
141149
const treeTab = page.locator('.output-tab[data-tab="tree"]')
142150
const evalPanel = page.locator('#evalPanel')
143151
const treePanel = page.locator('#treePanel')
144152

145-
// Eval tab active by default
146-
await expect(evalTab).toHaveClass(/active/)
147-
await expect(evalPanel).toHaveClass(/active/)
148-
await expect(treeTab).not.toHaveClass(/active/)
149-
await expect(treePanel).not.toHaveClass(/active/)
150-
151-
// Click Tree tab
152-
await treeTab.click()
153+
// Tree tab active by default
153154
await expect(treeTab).toHaveClass(/active/)
154155
await expect(treePanel).toHaveClass(/active/)
155156
await expect(evalTab).not.toHaveClass(/active/)
@@ -159,6 +160,13 @@ test.describe('Subscript REPL', () => {
159160
await evalTab.click()
160161
await expect(evalTab).toHaveClass(/active/)
161162
await expect(evalPanel).toHaveClass(/active/)
163+
await expect(treeTab).not.toHaveClass(/active/)
164+
await expect(treePanel).not.toHaveClass(/active/)
165+
166+
// Click Tree tab
167+
await treeTab.click()
168+
await expect(treeTab).toHaveClass(/active/)
169+
await expect(treePanel).toHaveClass(/active/)
162170
})
163171

164172
test('sidebar toggles', async ({ page }) => {

0 commit comments

Comments
 (0)