tjs-lang 0.2.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.
- package/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- package/tjs-lang.svg +31 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TJS Playground - Interactive TJS editor and runner
|
|
3
|
+
*
|
|
4
|
+
* A light-DOM web component for editing and running TJS code.
|
|
5
|
+
* Features:
|
|
6
|
+
* - CodeMirror editor with TJS syntax highlighting
|
|
7
|
+
* - Tabbed output: JS output, Preview, Docs, Tests
|
|
8
|
+
* - CSS/HTML editing for preview customization
|
|
9
|
+
* - Console output panel
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Component, ElementCreator, PartsMap, elements, vars } from 'tosijs'
|
|
13
|
+
import {
|
|
14
|
+
tabSelector,
|
|
15
|
+
TabSelector,
|
|
16
|
+
icons,
|
|
17
|
+
markdownViewer,
|
|
18
|
+
MarkdownViewer,
|
|
19
|
+
} from 'tosijs-ui'
|
|
20
|
+
import { codeMirror, CodeMirror } from '../../editors/codemirror/component'
|
|
21
|
+
import { tjs, type TJSTranspileOptions } from '../../src/lang'
|
|
22
|
+
import { generateDocs } from '../../src/lang/docs'
|
|
23
|
+
import { extractImports, generateImportMap, resolveImports } from './imports'
|
|
24
|
+
import { ModuleStore, type ValidationResult } from './module-store'
|
|
25
|
+
|
|
26
|
+
const { div, button, span, pre, style, input, template } = elements
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert TypeDescriptor to readable string
|
|
30
|
+
* Handles both old format (type: 'string') and new format (type: { kind: 'string', ... })
|
|
31
|
+
*/
|
|
32
|
+
function typeToString(type: any): string {
|
|
33
|
+
if (!type) return 'unknown'
|
|
34
|
+
// Old format: type was just a string like 'string', 'number', etc.
|
|
35
|
+
if (typeof type === 'string') return type
|
|
36
|
+
|
|
37
|
+
// New format: type is an object with kind, shape, items, members
|
|
38
|
+
switch (type.kind) {
|
|
39
|
+
case 'string':
|
|
40
|
+
return type.nullable ? 'string | null' : 'string'
|
|
41
|
+
case 'number':
|
|
42
|
+
return type.nullable ? 'number | null' : 'number'
|
|
43
|
+
case 'boolean':
|
|
44
|
+
return type.nullable ? 'boolean | null' : 'boolean'
|
|
45
|
+
case 'null':
|
|
46
|
+
return 'null'
|
|
47
|
+
case 'undefined':
|
|
48
|
+
return 'undefined'
|
|
49
|
+
case 'any':
|
|
50
|
+
return 'any'
|
|
51
|
+
case 'array': {
|
|
52
|
+
const itemType = type.items ? typeToString(type.items) : 'any'
|
|
53
|
+
return type.nullable ? `${itemType}[] | null` : `${itemType}[]`
|
|
54
|
+
}
|
|
55
|
+
case 'object': {
|
|
56
|
+
if (!type.shape) return type.nullable ? 'object | null' : 'object'
|
|
57
|
+
const props = Object.entries(type.shape)
|
|
58
|
+
.map(([k, v]) => `${k}: ${typeToString(v)}`)
|
|
59
|
+
.join(', ')
|
|
60
|
+
const objStr = `{ ${props} }`
|
|
61
|
+
return type.nullable ? `${objStr} | null` : objStr
|
|
62
|
+
}
|
|
63
|
+
case 'union':
|
|
64
|
+
return type.members?.map(typeToString).join(' | ') || 'unknown'
|
|
65
|
+
default:
|
|
66
|
+
return type.kind || 'unknown'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Example TJS code
|
|
71
|
+
const DEFAULT_TJS = `// TJS Example - Type annotations via examples
|
|
72
|
+
function greet(name: 'World') -> '' {
|
|
73
|
+
return \`Hello, \${name}!\`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Call it
|
|
77
|
+
greet('TJS')`
|
|
78
|
+
|
|
79
|
+
const DEFAULT_HTML = `<div class="preview-content">
|
|
80
|
+
<h2>Preview</h2>
|
|
81
|
+
<div id="output"></div>
|
|
82
|
+
</div>`
|
|
83
|
+
|
|
84
|
+
const DEFAULT_CSS = `.preview-content {
|
|
85
|
+
padding: 1rem;
|
|
86
|
+
font-family: system-ui, sans-serif;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
h2 {
|
|
90
|
+
color: #3d4a6b;
|
|
91
|
+
margin-top: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#output {
|
|
95
|
+
padding: 0.5rem;
|
|
96
|
+
background: #f5f5f5;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
}`
|
|
99
|
+
|
|
100
|
+
interface TJSPlaygroundParts extends PartsMap {
|
|
101
|
+
tjsEditor: CodeMirror
|
|
102
|
+
htmlEditor: CodeMirror
|
|
103
|
+
cssEditor: CodeMirror
|
|
104
|
+
inputTabs: TabSelector
|
|
105
|
+
outputTabs: TabSelector
|
|
106
|
+
jsOutput: HTMLElement
|
|
107
|
+
previewFrame: HTMLIFrameElement
|
|
108
|
+
docsOutput: MarkdownViewer
|
|
109
|
+
testsOutput: HTMLElement
|
|
110
|
+
consoleHeader: HTMLElement
|
|
111
|
+
console: HTMLElement
|
|
112
|
+
runBtn: HTMLButtonElement
|
|
113
|
+
revertBtn: HTMLButtonElement
|
|
114
|
+
saveBtn: HTMLButtonElement
|
|
115
|
+
moduleNameInput: HTMLInputElement
|
|
116
|
+
statusBar: HTMLElement
|
|
117
|
+
// Build flags
|
|
118
|
+
testsToggle: HTMLInputElement
|
|
119
|
+
debugToggle: HTMLInputElement
|
|
120
|
+
safetyToggle: HTMLInputElement
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
124
|
+
private lastTranspileResult: any = null
|
|
125
|
+
private consoleMessages: string[] = []
|
|
126
|
+
private functionMetadata: Record<string, any> = {}
|
|
127
|
+
|
|
128
|
+
// Editor state persistence
|
|
129
|
+
private currentExampleName: string | null = null
|
|
130
|
+
private originalCode: string = DEFAULT_TJS
|
|
131
|
+
private editorCache: Map<string, string> = new Map()
|
|
132
|
+
|
|
133
|
+
// Build flags state
|
|
134
|
+
private buildFlags = {
|
|
135
|
+
tests: true, // Run tests at transpile time
|
|
136
|
+
debug: false, // Debug mode (call stack tracking)
|
|
137
|
+
safe: true, // Safe mode (validates inputs)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Transpilation sequence number to handle race conditions
|
|
141
|
+
private transpileSeq = 0
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get metadata for autocomplete - returns all discovered functions
|
|
145
|
+
*/
|
|
146
|
+
private getMetadataForAutocomplete = (): Record<string, any> | undefined => {
|
|
147
|
+
if (Object.keys(this.functionMetadata).length === 0) {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
return this.functionMetadata
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
content = () => [
|
|
154
|
+
// Toolbar
|
|
155
|
+
div(
|
|
156
|
+
{ class: 'tjs-toolbar' },
|
|
157
|
+
button(
|
|
158
|
+
{ part: 'runBtn', class: 'run-btn', onClick: this.run },
|
|
159
|
+
icons.play({ size: 16 }),
|
|
160
|
+
'Run'
|
|
161
|
+
),
|
|
162
|
+
span({ class: 'toolbar-separator' }),
|
|
163
|
+
// Build flags
|
|
164
|
+
div(
|
|
165
|
+
{ class: 'build-flags' },
|
|
166
|
+
elements.label(
|
|
167
|
+
{ class: 'flag-label', title: 'Run tests at transpile time' },
|
|
168
|
+
input({
|
|
169
|
+
part: 'testsToggle',
|
|
170
|
+
type: 'checkbox',
|
|
171
|
+
checked: true,
|
|
172
|
+
onChange: this.toggleTests,
|
|
173
|
+
}),
|
|
174
|
+
'Tests'
|
|
175
|
+
),
|
|
176
|
+
elements.label(
|
|
177
|
+
{ class: 'flag-label', title: 'Debug mode (call stack tracking)' },
|
|
178
|
+
input({
|
|
179
|
+
part: 'debugToggle',
|
|
180
|
+
type: 'checkbox',
|
|
181
|
+
onChange: this.toggleDebug,
|
|
182
|
+
}),
|
|
183
|
+
'Debug'
|
|
184
|
+
),
|
|
185
|
+
elements.label(
|
|
186
|
+
{ class: 'flag-label', title: 'Safe mode (validates inputs)' },
|
|
187
|
+
input({
|
|
188
|
+
part: 'safetyToggle',
|
|
189
|
+
type: 'checkbox',
|
|
190
|
+
checked: true,
|
|
191
|
+
onChange: this.toggleSafety,
|
|
192
|
+
}),
|
|
193
|
+
'Safe'
|
|
194
|
+
)
|
|
195
|
+
),
|
|
196
|
+
span({ class: 'toolbar-separator' }),
|
|
197
|
+
button(
|
|
198
|
+
{
|
|
199
|
+
part: 'revertBtn',
|
|
200
|
+
class: 'revert-btn',
|
|
201
|
+
onClick: this.revertToOriginal,
|
|
202
|
+
title: 'Revert to original example code',
|
|
203
|
+
},
|
|
204
|
+
icons.cornerUpLeft({ size: 16 }),
|
|
205
|
+
'Revert'
|
|
206
|
+
),
|
|
207
|
+
span({ class: 'toolbar-separator' }),
|
|
208
|
+
input({
|
|
209
|
+
part: 'moduleNameInput',
|
|
210
|
+
class: 'module-name-input',
|
|
211
|
+
type: 'text',
|
|
212
|
+
placeholder: 'module-name',
|
|
213
|
+
title: 'Module name for saving/importing',
|
|
214
|
+
}),
|
|
215
|
+
button(
|
|
216
|
+
{ part: 'saveBtn', class: 'save-btn', onClick: this.saveModule },
|
|
217
|
+
icons.save({ size: 16 }),
|
|
218
|
+
'Save'
|
|
219
|
+
),
|
|
220
|
+
span({ class: 'elastic' }),
|
|
221
|
+
span({ part: 'statusBar', class: 'status-bar' }, 'Ready')
|
|
222
|
+
),
|
|
223
|
+
|
|
224
|
+
// Main area - split into input (left) and output (right)
|
|
225
|
+
div(
|
|
226
|
+
{ class: 'tjs-main' },
|
|
227
|
+
|
|
228
|
+
// Input side - TJS, HTML, CSS editors
|
|
229
|
+
div(
|
|
230
|
+
{ class: 'tjs-input' },
|
|
231
|
+
tabSelector(
|
|
232
|
+
{ part: 'inputTabs' },
|
|
233
|
+
div(
|
|
234
|
+
{ name: 'TJS', class: 'editor-wrapper' },
|
|
235
|
+
codeMirror({
|
|
236
|
+
part: 'tjsEditor',
|
|
237
|
+
mode: 'tjs',
|
|
238
|
+
})
|
|
239
|
+
),
|
|
240
|
+
div(
|
|
241
|
+
{ name: 'HTML', class: 'editor-wrapper' },
|
|
242
|
+
codeMirror({
|
|
243
|
+
part: 'htmlEditor',
|
|
244
|
+
mode: 'html',
|
|
245
|
+
})
|
|
246
|
+
),
|
|
247
|
+
div(
|
|
248
|
+
{ name: 'CSS', class: 'editor-wrapper' },
|
|
249
|
+
codeMirror({
|
|
250
|
+
part: 'cssEditor',
|
|
251
|
+
mode: 'css',
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
),
|
|
256
|
+
|
|
257
|
+
// Output side - JS, Preview, Docs, Tests
|
|
258
|
+
div(
|
|
259
|
+
{ class: 'tjs-output' },
|
|
260
|
+
tabSelector(
|
|
261
|
+
{ part: 'outputTabs' },
|
|
262
|
+
div(
|
|
263
|
+
{ name: 'JS' },
|
|
264
|
+
pre(
|
|
265
|
+
{ part: 'jsOutput', class: 'js-output' },
|
|
266
|
+
'// Transpiled JavaScript will appear here'
|
|
267
|
+
)
|
|
268
|
+
),
|
|
269
|
+
div(
|
|
270
|
+
{ name: 'Preview' },
|
|
271
|
+
div(
|
|
272
|
+
{ class: 'preview-container' },
|
|
273
|
+
// Using an iframe for isolation
|
|
274
|
+
elements.iframe({
|
|
275
|
+
part: 'previewFrame',
|
|
276
|
+
class: 'preview-frame',
|
|
277
|
+
sandbox: 'allow-scripts allow-same-origin',
|
|
278
|
+
})
|
|
279
|
+
)
|
|
280
|
+
),
|
|
281
|
+
markdownViewer({
|
|
282
|
+
name: 'Docs',
|
|
283
|
+
part: 'docsOutput',
|
|
284
|
+
class: 'docs-output',
|
|
285
|
+
value: '*Documentation will appear here*',
|
|
286
|
+
}),
|
|
287
|
+
div(
|
|
288
|
+
{ name: 'Tests' },
|
|
289
|
+
template(
|
|
290
|
+
{ role: 'tab' },
|
|
291
|
+
span({
|
|
292
|
+
class: 'test-indicator',
|
|
293
|
+
style: {
|
|
294
|
+
display: 'inline-block',
|
|
295
|
+
width: '8px',
|
|
296
|
+
height: '8px',
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
marginRight: '6px',
|
|
299
|
+
background: vars.testIndicatorColor,
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
'Tests'
|
|
303
|
+
),
|
|
304
|
+
div(
|
|
305
|
+
{ part: 'testsOutput', class: 'tests-output' },
|
|
306
|
+
'Test results will appear here'
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
),
|
|
312
|
+
|
|
313
|
+
// Console panel at bottom
|
|
314
|
+
div(
|
|
315
|
+
{ class: 'tjs-console' },
|
|
316
|
+
div({ part: 'consoleHeader', class: 'console-header' }, 'Console'),
|
|
317
|
+
pre({ part: 'console', class: 'console-output' })
|
|
318
|
+
),
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
connectedCallback(): void {
|
|
322
|
+
super.connectedCallback()
|
|
323
|
+
|
|
324
|
+
// Set default content
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
this.parts.tjsEditor.value = DEFAULT_TJS
|
|
327
|
+
this.parts.htmlEditor.value = DEFAULT_HTML
|
|
328
|
+
this.parts.cssEditor.value = DEFAULT_CSS
|
|
329
|
+
|
|
330
|
+
// Wire up autocomplete to get metadata from transpiler
|
|
331
|
+
this.parts.tjsEditor.autocomplete = {
|
|
332
|
+
getMetadata: this.getMetadataForAutocomplete,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Auto-transpile on load
|
|
336
|
+
this.transpile()
|
|
337
|
+
}, 0)
|
|
338
|
+
|
|
339
|
+
// Listen for changes (debounced to avoid excessive transpilation)
|
|
340
|
+
let debounceTimer: ReturnType<typeof setTimeout>
|
|
341
|
+
this.parts.tjsEditor.addEventListener('change', () => {
|
|
342
|
+
clearTimeout(debounceTimer)
|
|
343
|
+
debounceTimer = setTimeout(() => {
|
|
344
|
+
this.transpile()
|
|
345
|
+
this.updateRevertButton()
|
|
346
|
+
}, 300)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
log = (message: string) => {
|
|
351
|
+
this.consoleMessages.push(message)
|
|
352
|
+
this.renderConsole()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
clearConsole = () => {
|
|
356
|
+
this.consoleMessages = []
|
|
357
|
+
this.parts.console.innerHTML = ''
|
|
358
|
+
this.parts.consoleHeader.textContent = 'Console'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private renderConsole() {
|
|
362
|
+
// Parse messages for line references and make them clickable
|
|
363
|
+
// Patterns: "at line X", "line X:", "Line X", ":X:" (line:col)
|
|
364
|
+
const linePattern =
|
|
365
|
+
/(?:at line |line |Line )(\d+)(?:[:,]?\s*(?:column |col )?(\d+))?|:(\d+):(\d+)/g
|
|
366
|
+
|
|
367
|
+
const html = this.consoleMessages
|
|
368
|
+
.map((msg) => {
|
|
369
|
+
// Escape HTML
|
|
370
|
+
const escaped = msg
|
|
371
|
+
.replace(/&/g, '&')
|
|
372
|
+
.replace(/</g, '<')
|
|
373
|
+
.replace(/>/g, '>')
|
|
374
|
+
|
|
375
|
+
// Replace line references with clickable spans
|
|
376
|
+
return escaped.replace(linePattern, (match, l1, c1, l2, c2) => {
|
|
377
|
+
const line = l1 || l2
|
|
378
|
+
const col = c1 || c2 || '1'
|
|
379
|
+
return `<span class="clickable-line" data-line="${line}" data-col="${col}">${match}</span>`
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
.join('\n')
|
|
383
|
+
|
|
384
|
+
this.parts.console.innerHTML = html
|
|
385
|
+
this.parts.console.scrollTop = this.parts.console.scrollHeight
|
|
386
|
+
|
|
387
|
+
// Add click handlers
|
|
388
|
+
this.parts.console.querySelectorAll('.clickable-line').forEach((el) => {
|
|
389
|
+
el.addEventListener('click', (e) => {
|
|
390
|
+
const target = e.currentTarget as HTMLElement
|
|
391
|
+
const line = parseInt(target.dataset.line || '0', 10)
|
|
392
|
+
const col = parseInt(target.dataset.col || '1', 10)
|
|
393
|
+
if (line > 0) {
|
|
394
|
+
this.goToSourceLine(line, col)
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Build flag toggle handlers
|
|
401
|
+
toggleTests = () => {
|
|
402
|
+
this.buildFlags.tests = this.parts.testsToggle.checked
|
|
403
|
+
this.transpile()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
toggleDebug = () => {
|
|
407
|
+
this.buildFlags.debug = this.parts.debugToggle.checked
|
|
408
|
+
this.transpile()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
toggleSafety = () => {
|
|
412
|
+
this.buildFlags.safe = this.parts.safetyToggle.checked
|
|
413
|
+
this.transpile()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
lastTranspileTime = 0
|
|
417
|
+
|
|
418
|
+
transpile = () => {
|
|
419
|
+
// Kick off async transpilation
|
|
420
|
+
this.transpileAsync()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private transpileAsync = async () => {
|
|
424
|
+
// Increment sequence number to track this transpilation
|
|
425
|
+
const mySeq = ++this.transpileSeq
|
|
426
|
+
|
|
427
|
+
let source = this.parts.tjsEditor.value
|
|
428
|
+
|
|
429
|
+
// Extract function metadata for autocomplete (even if transpile fails)
|
|
430
|
+
this.extractFunctionMetadata(source)
|
|
431
|
+
|
|
432
|
+
// Inject safety directive if unsafe mode is enabled
|
|
433
|
+
// This prepends "safety none" to skip all validation
|
|
434
|
+
if (!this.buildFlags.safe) {
|
|
435
|
+
source = 'safety none\n' + source
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Resolve local imports before transpilation (for test execution)
|
|
440
|
+
const resolvedImports = await this.resolveImportsForTests(source)
|
|
441
|
+
|
|
442
|
+
// Check if a newer transpilation has started - if so, abandon this one
|
|
443
|
+
if (mySeq !== this.transpileSeq) {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Time the transpilation
|
|
448
|
+
const startTime = performance.now()
|
|
449
|
+
// Build transpiler options from flags
|
|
450
|
+
const options: TJSTranspileOptions = {
|
|
451
|
+
runTests: this.buildFlags.tests ? 'report' : false,
|
|
452
|
+
debug: this.buildFlags.debug,
|
|
453
|
+
resolvedImports,
|
|
454
|
+
}
|
|
455
|
+
const result = tjs(source, options)
|
|
456
|
+
this.lastTranspileTime = performance.now() - startTime
|
|
457
|
+
|
|
458
|
+
this.lastTranspileResult = result
|
|
459
|
+
this.parts.jsOutput.textContent = result.code
|
|
460
|
+
|
|
461
|
+
// Update docs
|
|
462
|
+
this.updateDocs(result)
|
|
463
|
+
|
|
464
|
+
// Update test results and status bar with timing
|
|
465
|
+
const tests = result.testResults || []
|
|
466
|
+
const failed = tests.filter((t: any) => !t.passed).length
|
|
467
|
+
const timeStr =
|
|
468
|
+
this.lastTranspileTime < 1
|
|
469
|
+
? `${(this.lastTranspileTime * 1000).toFixed(0)}μs`
|
|
470
|
+
: `${this.lastTranspileTime.toFixed(2)}ms`
|
|
471
|
+
if (failed > 0) {
|
|
472
|
+
this.parts.statusBar.textContent = `Transpiled in ${timeStr} with ${failed} test failure${
|
|
473
|
+
failed > 1 ? 's' : ''
|
|
474
|
+
}`
|
|
475
|
+
this.parts.statusBar.classList.add('error')
|
|
476
|
+
} else {
|
|
477
|
+
this.parts.statusBar.textContent = `Transpiled in ${timeStr}`
|
|
478
|
+
this.parts.statusBar.classList.remove('error')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.updateTestResults(result)
|
|
482
|
+
|
|
483
|
+
// If we got metadata from transpiler, use it (more accurate)
|
|
484
|
+
if (result.metadata?.name && typeof result.metadata.name === 'string') {
|
|
485
|
+
this.functionMetadata[result.metadata.name] = result.metadata
|
|
486
|
+
}
|
|
487
|
+
} catch (e: any) {
|
|
488
|
+
// Format error with location info if available
|
|
489
|
+
const errorInfo = this.formatTranspileError(e, source)
|
|
490
|
+
this.parts.jsOutput.textContent = errorInfo.detailed
|
|
491
|
+
this.parts.statusBar.textContent = errorInfo.short
|
|
492
|
+
this.parts.statusBar.classList.add('error')
|
|
493
|
+
this.lastTranspileResult = null
|
|
494
|
+
// Clear test results on error
|
|
495
|
+
this.parts.testsOutput.textContent = 'Transpilation failed - no tests run'
|
|
496
|
+
|
|
497
|
+
// Set error marker in gutter
|
|
498
|
+
if (e.line) {
|
|
499
|
+
this.parts.tjsEditor.setMarkers([
|
|
500
|
+
{ line: e.line, message: e.message || 'Transpilation error' },
|
|
501
|
+
])
|
|
502
|
+
} else {
|
|
503
|
+
this.parts.tjsEditor.clearMarkers()
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Resolve local imports for test execution
|
|
510
|
+
* Returns a map of import specifier -> compiled code
|
|
511
|
+
*/
|
|
512
|
+
private resolveImportsForTests = async (
|
|
513
|
+
source: string
|
|
514
|
+
): Promise<Record<string, string>> => {
|
|
515
|
+
const imports = extractImports(source)
|
|
516
|
+
if (imports.length === 0) {
|
|
517
|
+
return {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const resolvedImports: Record<string, string> = {}
|
|
521
|
+
const store = await ModuleStore.open()
|
|
522
|
+
|
|
523
|
+
for (const specifier of imports) {
|
|
524
|
+
// Only resolve local modules (not CDN packages)
|
|
525
|
+
if (await store.exists(specifier)) {
|
|
526
|
+
const compiled = await store.getCompiled(specifier)
|
|
527
|
+
if (compiled) {
|
|
528
|
+
resolvedImports[specifier] = compiled
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return resolvedImports
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private updateTestResults(result: any) {
|
|
537
|
+
const tests = result.testResults
|
|
538
|
+
if (!tests || tests.length === 0) {
|
|
539
|
+
this.parts.testsOutput.textContent = 'No tests defined'
|
|
540
|
+
this.updateTestsTabLabel(0, 0)
|
|
541
|
+
this.parts.tjsEditor.clearMarkers()
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const passed = tests.filter((t: any) => t.passed).length
|
|
546
|
+
const failed = tests.filter((t: any) => !t.passed).length
|
|
547
|
+
|
|
548
|
+
// Update tab label with indicator
|
|
549
|
+
this.updateTestsTabLabel(passed, failed)
|
|
550
|
+
|
|
551
|
+
// Set gutter markers for failed tests
|
|
552
|
+
const failedTests = tests.filter((t: any) => !t.passed && t.line)
|
|
553
|
+
if (failedTests.length > 0) {
|
|
554
|
+
this.parts.tjsEditor.setMarkers(
|
|
555
|
+
failedTests.map((t: any) => ({
|
|
556
|
+
line: t.line,
|
|
557
|
+
message: t.error || t.description,
|
|
558
|
+
severity: 'error' as const,
|
|
559
|
+
}))
|
|
560
|
+
)
|
|
561
|
+
} else {
|
|
562
|
+
this.parts.tjsEditor.clearMarkers()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let html = `<div class="test-summary">`
|
|
566
|
+
html += `<strong>${passed} passed</strong>`
|
|
567
|
+
if (failed > 0) {
|
|
568
|
+
html += `, <strong class="test-failed">${failed} failed</strong>`
|
|
569
|
+
}
|
|
570
|
+
html += `</div><ul class="test-list">`
|
|
571
|
+
|
|
572
|
+
for (const test of tests) {
|
|
573
|
+
const icon = test.passed ? '✓' : '✗'
|
|
574
|
+
const cls = test.passed ? 'test-pass' : 'test-fail'
|
|
575
|
+
const sigBadge = test.isSignatureTest
|
|
576
|
+
? ' <span class="sig-badge">signature</span>'
|
|
577
|
+
: ''
|
|
578
|
+
const clickable =
|
|
579
|
+
!test.passed && test.line ? ' class="clickable-error"' : ''
|
|
580
|
+
const dataLine = test.line ? ` data-line="${test.line}"` : ''
|
|
581
|
+
html += `<li class="${cls}"${dataLine}>${icon} ${test.description}${sigBadge}`
|
|
582
|
+
if (!test.passed && test.error) {
|
|
583
|
+
html += `<div${clickable}${dataLine} class="test-error${
|
|
584
|
+
test.line ? ' clickable-error' : ''
|
|
585
|
+
}">${test.error}</div>`
|
|
586
|
+
}
|
|
587
|
+
html += `</li>`
|
|
588
|
+
}
|
|
589
|
+
html += `</ul>`
|
|
590
|
+
|
|
591
|
+
this.parts.testsOutput.innerHTML = html
|
|
592
|
+
|
|
593
|
+
// Add click handlers for clickable errors
|
|
594
|
+
this.parts.testsOutput
|
|
595
|
+
.querySelectorAll('.clickable-error')
|
|
596
|
+
.forEach((el) => {
|
|
597
|
+
el.addEventListener('click', (e) => {
|
|
598
|
+
const line = parseInt(
|
|
599
|
+
(e.currentTarget as HTMLElement).dataset.line || '0',
|
|
600
|
+
10
|
|
601
|
+
)
|
|
602
|
+
if (line > 0) {
|
|
603
|
+
this.goToSourceLine(line)
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private updateTestsTabLabel(passed: number, failed: number) {
|
|
610
|
+
const tabs = this.parts.outputTabs
|
|
611
|
+
if (!tabs) return
|
|
612
|
+
|
|
613
|
+
if (failed > 0) {
|
|
614
|
+
tabs.style.setProperty('--test-indicator-color', '#dc2626')
|
|
615
|
+
} else if (passed > 0) {
|
|
616
|
+
tabs.style.setProperty('--test-indicator-color', '#16a34a')
|
|
617
|
+
} else {
|
|
618
|
+
tabs.style.setProperty('--test-indicator-color', 'transparent')
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Format transpile error with helpful context
|
|
624
|
+
*/
|
|
625
|
+
private formatTranspileError = (
|
|
626
|
+
e: any,
|
|
627
|
+
source: string
|
|
628
|
+
): { short: string; detailed: string } => {
|
|
629
|
+
const lines = source.split('\n')
|
|
630
|
+
const line = e.line ?? 1
|
|
631
|
+
const column = e.column ?? 0
|
|
632
|
+
const message = e.message || String(e)
|
|
633
|
+
|
|
634
|
+
// Short version for status bar
|
|
635
|
+
const short = e.line
|
|
636
|
+
? `Error at line ${line}: ${message}`
|
|
637
|
+
: `Error: ${message}`
|
|
638
|
+
|
|
639
|
+
// Detailed version with code context
|
|
640
|
+
const detailedLines = ['// Transpilation Error', '// ' + '='.repeat(50), '']
|
|
641
|
+
|
|
642
|
+
// Add the error message
|
|
643
|
+
detailedLines.push(`// ${message}`)
|
|
644
|
+
if (e.line) {
|
|
645
|
+
detailedLines.push(`// at line ${line}, column ${column}`)
|
|
646
|
+
}
|
|
647
|
+
detailedLines.push('')
|
|
648
|
+
|
|
649
|
+
// Show code context (3 lines before and after)
|
|
650
|
+
if (e.line && lines.length > 0) {
|
|
651
|
+
detailedLines.push('// Code context:')
|
|
652
|
+
const start = Math.max(0, line - 3)
|
|
653
|
+
const end = Math.min(lines.length, line + 2)
|
|
654
|
+
|
|
655
|
+
for (let i = start; i < end; i++) {
|
|
656
|
+
const lineNum = i + 1
|
|
657
|
+
const prefix = lineNum === line ? '>> ' : ' '
|
|
658
|
+
const lineContent = lines[i] ?? ''
|
|
659
|
+
detailedLines.push(
|
|
660
|
+
`// ${prefix}${lineNum.toString().padStart(3)}: ${lineContent}`
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
// Show caret pointing to error column
|
|
664
|
+
if (lineNum === line && column > 0) {
|
|
665
|
+
const caretPos = 10 + column // account for prefix
|
|
666
|
+
detailedLines.push('// ' + ' '.repeat(caretPos) + '^')
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Add suggestions based on common errors
|
|
672
|
+
const suggestions = this.getSuggestions(message, source)
|
|
673
|
+
if (suggestions.length > 0) {
|
|
674
|
+
detailedLines.push('')
|
|
675
|
+
detailedLines.push('// Suggestions:')
|
|
676
|
+
for (const suggestion of suggestions) {
|
|
677
|
+
detailedLines.push(`// - ${suggestion}`)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return { short, detailed: detailedLines.join('\n') }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Get helpful suggestions based on error message
|
|
686
|
+
*/
|
|
687
|
+
private getSuggestions = (message: string, source: string): string[] => {
|
|
688
|
+
const suggestions: string[] = []
|
|
689
|
+
const msg = message.toLowerCase()
|
|
690
|
+
|
|
691
|
+
if (msg.includes('unexpected token')) {
|
|
692
|
+
suggestions.push('Check for missing brackets, parentheses, or quotes')
|
|
693
|
+
suggestions.push('TJS uses : for type annotations, = for defaults')
|
|
694
|
+
if (source.includes('=>')) {
|
|
695
|
+
suggestions.push(
|
|
696
|
+
'Arrow functions are not supported - use function keyword'
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (msg.includes('unexpected identifier')) {
|
|
702
|
+
suggestions.push('Check for missing commas between parameters')
|
|
703
|
+
suggestions.push(
|
|
704
|
+
'Check for typos in keywords (function, return, if, while)'
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (msg.includes('unterminated string')) {
|
|
709
|
+
suggestions.push('Check for unmatched quotes')
|
|
710
|
+
suggestions.push('Template literals use backticks (`), not quotes')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (msg.includes('imports are not supported')) {
|
|
714
|
+
suggestions.push('For TJS modules, imports work - this error is for AJS')
|
|
715
|
+
suggestions.push('Make sure the module exists in the store')
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (msg.includes('required parameter') && msg.includes('optional')) {
|
|
719
|
+
suggestions.push(
|
|
720
|
+
'Required parameters (name: type) must come before optional (name = default)'
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (msg.includes('duplicate parameter')) {
|
|
725
|
+
suggestions.push('Each parameter must have a unique name')
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return suggestions
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Extract function metadata from source for autocomplete
|
|
733
|
+
* This runs even when transpilation fails (incomplete code)
|
|
734
|
+
*/
|
|
735
|
+
private extractFunctionMetadata = (source: string) => {
|
|
736
|
+
// Match function declarations with TJS syntax
|
|
737
|
+
// function name(param: 'type', param2 = default) -> returnType { ... }
|
|
738
|
+
const funcRegex =
|
|
739
|
+
/function\s+(\w+)\s*\(\s*([^)]*)\s*\)\s*(?:->\s*([^\s{]+))?\s*\{/g
|
|
740
|
+
|
|
741
|
+
const newMetadata: Record<string, any> = {}
|
|
742
|
+
let match
|
|
743
|
+
|
|
744
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
745
|
+
const [, funcName, paramsStr, returnType] = match
|
|
746
|
+
|
|
747
|
+
// Parse parameters
|
|
748
|
+
const params: Record<string, any> = {}
|
|
749
|
+
if (paramsStr.trim()) {
|
|
750
|
+
// Split on commas, but be careful of nested structures
|
|
751
|
+
const paramParts = this.splitParams(paramsStr)
|
|
752
|
+
|
|
753
|
+
for (const paramStr of paramParts) {
|
|
754
|
+
const trimmed = paramStr.trim()
|
|
755
|
+
if (!trimmed) continue
|
|
756
|
+
|
|
757
|
+
// Match: name: 'type' or name = default or name: type = default
|
|
758
|
+
const paramMatch = trimmed.match(
|
|
759
|
+
/^(\w+)\s*(?::\s*([^=]+?))?\s*(?:=\s*(.+))?$/
|
|
760
|
+
)
|
|
761
|
+
if (paramMatch) {
|
|
762
|
+
const [, paramName, typeExample, defaultValue] = paramMatch
|
|
763
|
+
const hasDefault = defaultValue !== undefined
|
|
764
|
+
const typeStr = typeExample?.trim() || defaultValue?.trim()
|
|
765
|
+
|
|
766
|
+
params[paramName] = {
|
|
767
|
+
type: this.inferTypeFromExample(typeStr),
|
|
768
|
+
required: !hasDefault && typeExample !== undefined,
|
|
769
|
+
default: hasDefault ? this.parseDefault(defaultValue) : undefined,
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
newMetadata[funcName] = {
|
|
776
|
+
name: funcName,
|
|
777
|
+
params,
|
|
778
|
+
returns: returnType ? this.inferTypeFromExample(returnType) : undefined,
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
this.functionMetadata = newMetadata
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Split parameter string handling nested brackets
|
|
787
|
+
*/
|
|
788
|
+
private splitParams = (paramsStr: string): string[] => {
|
|
789
|
+
const result: string[] = []
|
|
790
|
+
let current = ''
|
|
791
|
+
let depth = 0
|
|
792
|
+
|
|
793
|
+
for (const char of paramsStr) {
|
|
794
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
795
|
+
depth++
|
|
796
|
+
current += char
|
|
797
|
+
} else if (char === ')' || char === ']' || char === '}') {
|
|
798
|
+
depth--
|
|
799
|
+
current += char
|
|
800
|
+
} else if (char === ',' && depth === 0) {
|
|
801
|
+
result.push(current)
|
|
802
|
+
current = ''
|
|
803
|
+
} else {
|
|
804
|
+
current += char
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (current.trim()) {
|
|
808
|
+
result.push(current)
|
|
809
|
+
}
|
|
810
|
+
return result
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Infer type descriptor from example value
|
|
815
|
+
*/
|
|
816
|
+
private inferTypeFromExample = (
|
|
817
|
+
example: string | undefined
|
|
818
|
+
): { kind: string } | undefined => {
|
|
819
|
+
if (!example) return undefined
|
|
820
|
+
const trimmed = example.trim()
|
|
821
|
+
|
|
822
|
+
// String literal
|
|
823
|
+
if (/^['"]/.test(trimmed)) {
|
|
824
|
+
return { kind: 'string' }
|
|
825
|
+
}
|
|
826
|
+
// Number
|
|
827
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
828
|
+
return { kind: 'number' }
|
|
829
|
+
}
|
|
830
|
+
// Boolean
|
|
831
|
+
if (trimmed === 'true' || trimmed === 'false') {
|
|
832
|
+
return { kind: 'boolean' }
|
|
833
|
+
}
|
|
834
|
+
// Null
|
|
835
|
+
if (trimmed === 'null') {
|
|
836
|
+
return { kind: 'null' }
|
|
837
|
+
}
|
|
838
|
+
// Array
|
|
839
|
+
if (trimmed.startsWith('[')) {
|
|
840
|
+
return { kind: 'array' }
|
|
841
|
+
}
|
|
842
|
+
// Object
|
|
843
|
+
if (trimmed.startsWith('{')) {
|
|
844
|
+
return { kind: 'object' }
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return { kind: 'any' }
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Parse default value to JS value
|
|
852
|
+
*/
|
|
853
|
+
private parseDefault = (value: string): any => {
|
|
854
|
+
const trimmed = value.trim()
|
|
855
|
+
try {
|
|
856
|
+
// Try to parse as JSON-like value
|
|
857
|
+
if (
|
|
858
|
+
trimmed === 'true' ||
|
|
859
|
+
trimmed === 'false' ||
|
|
860
|
+
trimmed === 'null' ||
|
|
861
|
+
/^-?\d+(\.\d+)?$/.test(trimmed)
|
|
862
|
+
) {
|
|
863
|
+
return JSON.parse(trimmed)
|
|
864
|
+
}
|
|
865
|
+
// String literal
|
|
866
|
+
if (/^['"]/.test(trimmed)) {
|
|
867
|
+
return trimmed.slice(1, -1)
|
|
868
|
+
}
|
|
869
|
+
} catch {
|
|
870
|
+
// Return as-is if parsing fails
|
|
871
|
+
}
|
|
872
|
+
return trimmed
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
updateDocs = (_result: any) => {
|
|
876
|
+
const source = this.parts.tjsEditor.value
|
|
877
|
+
|
|
878
|
+
// Use the core generateDocs function to extract all doc blocks and functions
|
|
879
|
+
const docs = generateDocs(source)
|
|
880
|
+
|
|
881
|
+
if (docs.items.length === 0) {
|
|
882
|
+
this.parts.docsOutput.value = '*No documentation available*'
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
this.parts.docsOutput.value = docs.markdown
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
saveModule = async () => {
|
|
890
|
+
const name = this.parts.moduleNameInput.value.trim()
|
|
891
|
+
if (!name) {
|
|
892
|
+
this.parts.statusBar.textContent = 'Enter a module name to save'
|
|
893
|
+
this.parts.statusBar.classList.add('error')
|
|
894
|
+
this.parts.moduleNameInput.focus()
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Validate module name format
|
|
899
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(name)) {
|
|
900
|
+
this.parts.statusBar.textContent =
|
|
901
|
+
'Module name must start with letter, contain only letters, numbers, dashes'
|
|
902
|
+
this.parts.statusBar.classList.add('error')
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
this.parts.statusBar.textContent = 'Validating...'
|
|
907
|
+
this.parts.statusBar.classList.remove('error')
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const store = await ModuleStore.open()
|
|
911
|
+
const code = this.parts.tjsEditor.value
|
|
912
|
+
|
|
913
|
+
// Validate first to get detailed results
|
|
914
|
+
const validation = await store.validate(code, 'tjs')
|
|
915
|
+
|
|
916
|
+
if (!validation.valid) {
|
|
917
|
+
// Show validation errors
|
|
918
|
+
const errorMessages = validation.errors.map((e) => e.message).join('; ')
|
|
919
|
+
this.parts.statusBar.textContent = `Save failed: ${errorMessages}`
|
|
920
|
+
this.parts.statusBar.classList.add('error')
|
|
921
|
+
|
|
922
|
+
// Log detailed errors to console
|
|
923
|
+
this.clearConsole()
|
|
924
|
+
this.log('=== Save Validation Failed ===')
|
|
925
|
+
for (const error of validation.errors) {
|
|
926
|
+
if (error.line) {
|
|
927
|
+
this.log(
|
|
928
|
+
`${error.type} error at line ${error.line}: ${error.message}`
|
|
929
|
+
)
|
|
930
|
+
} else {
|
|
931
|
+
this.log(`${error.type} error: ${error.message}`)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (validation.warnings.length > 0) {
|
|
935
|
+
this.log('')
|
|
936
|
+
this.log('Warnings:')
|
|
937
|
+
for (const warning of validation.warnings) {
|
|
938
|
+
this.log(` - ${warning}`)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Validation passed, save (skip re-validation)
|
|
945
|
+
await store.save({ name, type: 'tjs', code }, { skipValidation: true })
|
|
946
|
+
|
|
947
|
+
// Success!
|
|
948
|
+
this.parts.statusBar.textContent = `Saved as "${name}"`
|
|
949
|
+
this.parts.statusBar.classList.remove('error')
|
|
950
|
+
|
|
951
|
+
// Show test results if any
|
|
952
|
+
if (validation.testResults && validation.testResults.length > 0) {
|
|
953
|
+
this.clearConsole()
|
|
954
|
+
this.log(`=== Module "${name}" saved successfully ===`)
|
|
955
|
+
this.log('')
|
|
956
|
+
this.log(`Tests: ${validation.testResults.length} passed`)
|
|
957
|
+
for (const test of validation.testResults) {
|
|
958
|
+
this.log(` ✓ ${test.name}`)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} catch (e: any) {
|
|
962
|
+
this.parts.statusBar.textContent = `Save error: ${e.message}`
|
|
963
|
+
this.parts.statusBar.classList.add('error')
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
run = async () => {
|
|
968
|
+
this.clearConsole()
|
|
969
|
+
this.transpile()
|
|
970
|
+
|
|
971
|
+
if (!this.lastTranspileResult) {
|
|
972
|
+
this.log('Cannot run - transpilation failed')
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
this.parts.statusBar.textContent = 'Running...'
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
// Build the preview HTML
|
|
980
|
+
const htmlContent = this.parts.htmlEditor.value
|
|
981
|
+
const cssContent = this.parts.cssEditor.value
|
|
982
|
+
const jsCode = this.lastTranspileResult.code
|
|
983
|
+
|
|
984
|
+
// Resolve imports from the transpiled code
|
|
985
|
+
const imports = extractImports(jsCode)
|
|
986
|
+
let importMapScript = ''
|
|
987
|
+
|
|
988
|
+
if (imports.length > 0) {
|
|
989
|
+
this.log(`Resolving imports: ${imports.join(', ')}`)
|
|
990
|
+
const { importMap, errors, localModules } = await resolveImports(jsCode)
|
|
991
|
+
|
|
992
|
+
if (errors.length > 0) {
|
|
993
|
+
for (const err of errors) {
|
|
994
|
+
this.log(`Import error: ${err}`)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
console.log('[run] importMap:', JSON.stringify(importMap, null, 2))
|
|
999
|
+
if (Object.keys(importMap.imports).length > 0) {
|
|
1000
|
+
importMapScript = `<script type="importmap">${JSON.stringify(
|
|
1001
|
+
importMap
|
|
1002
|
+
)}</script>`
|
|
1003
|
+
console.log('[run] importMapScript:', importMapScript)
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Extract import statements from jsCode - they must be at the top of the module
|
|
1008
|
+
// Matches: import ... from 'pkg', import 'pkg' (side-effect)
|
|
1009
|
+
const importStatements: string[] = []
|
|
1010
|
+
const codeWithoutImports = jsCode.replace(
|
|
1011
|
+
/^import\s+(?:.*?from\s+)?['"][^'"]+['"];?\s*$/gm,
|
|
1012
|
+
(match) => {
|
|
1013
|
+
importStatements.push(match)
|
|
1014
|
+
return ''
|
|
1015
|
+
}
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
// Create a complete HTML document for the iframe
|
|
1019
|
+
const iframeDoc = `<!DOCTYPE html>
|
|
1020
|
+
<html>
|
|
1021
|
+
<head>
|
|
1022
|
+
<style>${cssContent}</style>
|
|
1023
|
+
${importMapScript}
|
|
1024
|
+
</head>
|
|
1025
|
+
<body>
|
|
1026
|
+
${htmlContent}
|
|
1027
|
+
<!-- TJS Runtime stub must be set up BEFORE imports execute -->
|
|
1028
|
+
<script>
|
|
1029
|
+
globalThis.__tjs = {
|
|
1030
|
+
version: '0.0.0',
|
|
1031
|
+
pushStack: () => {},
|
|
1032
|
+
popStack: () => {},
|
|
1033
|
+
getStack: () => [],
|
|
1034
|
+
typeError: (path, expected, value) => {
|
|
1035
|
+
const actual = value === null ? 'null' : typeof value;
|
|
1036
|
+
const err = new Error(\`Expected \${expected} for '\${path}', got \${actual}\`);
|
|
1037
|
+
err.name = 'MonadicError';
|
|
1038
|
+
err.path = path;
|
|
1039
|
+
err.expected = expected;
|
|
1040
|
+
err.actual = actual;
|
|
1041
|
+
return err;
|
|
1042
|
+
},
|
|
1043
|
+
createRuntime: function() { return this; },
|
|
1044
|
+
Is: (a, b) => {
|
|
1045
|
+
if (a === b) return true;
|
|
1046
|
+
if (a === null || b === null) return a === b;
|
|
1047
|
+
if (typeof a !== typeof b) return false;
|
|
1048
|
+
if (typeof a !== 'object') return false;
|
|
1049
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1050
|
+
if (a.length !== b.length) return false;
|
|
1051
|
+
return a.every((v, i) => globalThis.__tjs.Is(v, b[i]));
|
|
1052
|
+
}
|
|
1053
|
+
const keysA = Object.keys(a);
|
|
1054
|
+
const keysB = Object.keys(b);
|
|
1055
|
+
if (keysA.length !== keysB.length) return false;
|
|
1056
|
+
return keysA.every(k => globalThis.__tjs.Is(a[k], b[k]));
|
|
1057
|
+
},
|
|
1058
|
+
IsNot: (a, b) => !globalThis.__tjs.Is(a, b),
|
|
1059
|
+
};
|
|
1060
|
+
</script>
|
|
1061
|
+
<script type="module">
|
|
1062
|
+
// Import statements must be at the top of the module
|
|
1063
|
+
${importStatements.join('\n ')}
|
|
1064
|
+
|
|
1065
|
+
// Capture console.log
|
|
1066
|
+
const _log = console.log;
|
|
1067
|
+
console.log = (...args) => {
|
|
1068
|
+
_log(...args);
|
|
1069
|
+
parent.postMessage({ type: 'console', message: args.map(a =>
|
|
1070
|
+
typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)
|
|
1071
|
+
).join(' ') }, '*');
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
try {
|
|
1075
|
+
const __execStart = performance.now();
|
|
1076
|
+
${codeWithoutImports}
|
|
1077
|
+
|
|
1078
|
+
// Try to call the function if it exists and show result
|
|
1079
|
+
const funcName = Object.keys(window).find(k => {
|
|
1080
|
+
try { return typeof window[k] === 'function' && window[k].__tjs; }
|
|
1081
|
+
catch { return false; }
|
|
1082
|
+
});
|
|
1083
|
+
if (funcName) {
|
|
1084
|
+
const __callStart = performance.now();
|
|
1085
|
+
const result = window[funcName]();
|
|
1086
|
+
const __execTime = performance.now() - __callStart;
|
|
1087
|
+
parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
|
|
1088
|
+
if (result !== undefined) {
|
|
1089
|
+
const output = document.getElementById('output');
|
|
1090
|
+
if (output) {
|
|
1091
|
+
output.textContent = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
|
|
1092
|
+
}
|
|
1093
|
+
console.log('Result:', result);
|
|
1094
|
+
}
|
|
1095
|
+
} else {
|
|
1096
|
+
// No TJS function found, report total parse/exec time
|
|
1097
|
+
const __execTime = performance.now() - __execStart;
|
|
1098
|
+
parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
|
|
1099
|
+
}
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
parent.postMessage({ type: 'error', message: e.message }, '*');
|
|
1102
|
+
}
|
|
1103
|
+
</script>
|
|
1104
|
+
</body>
|
|
1105
|
+
</html>`
|
|
1106
|
+
|
|
1107
|
+
// Listen for messages from iframe
|
|
1108
|
+
const messageHandler = (event: MessageEvent) => {
|
|
1109
|
+
if (event.data?.type === 'console') {
|
|
1110
|
+
this.log(event.data.message)
|
|
1111
|
+
} else if (event.data?.type === 'timing') {
|
|
1112
|
+
// Update console header with execution time
|
|
1113
|
+
const execTime = event.data.execTime
|
|
1114
|
+
const execStr =
|
|
1115
|
+
execTime < 1
|
|
1116
|
+
? `${(execTime * 1000).toFixed(0)}μs`
|
|
1117
|
+
: `${execTime.toFixed(2)}ms`
|
|
1118
|
+
this.parts.consoleHeader.textContent = `Console — executed in ${execStr}`
|
|
1119
|
+
} else if (event.data?.type === 'error') {
|
|
1120
|
+
this.log(`Error: ${event.data.message}`)
|
|
1121
|
+
this.parts.statusBar.textContent = 'Runtime error'
|
|
1122
|
+
this.parts.statusBar.classList.add('error')
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
window.addEventListener('message', messageHandler)
|
|
1126
|
+
|
|
1127
|
+
// Set iframe content using blob URL instead of srcdoc
|
|
1128
|
+
// This allows import maps to work with external URLs
|
|
1129
|
+
const iframe = this.parts.previewFrame
|
|
1130
|
+
const blob = new Blob([iframeDoc], { type: 'text/html' })
|
|
1131
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
1132
|
+
|
|
1133
|
+
// Clean up previous blob URL if any
|
|
1134
|
+
if (iframe.dataset.blobUrl) {
|
|
1135
|
+
URL.revokeObjectURL(iframe.dataset.blobUrl)
|
|
1136
|
+
}
|
|
1137
|
+
iframe.dataset.blobUrl = blobUrl
|
|
1138
|
+
iframe.src = blobUrl
|
|
1139
|
+
|
|
1140
|
+
// Wait a bit for execution, then clean up listener
|
|
1141
|
+
setTimeout(() => {
|
|
1142
|
+
window.removeEventListener('message', messageHandler)
|
|
1143
|
+
// Don't overwrite status bar - keep showing transpile time
|
|
1144
|
+
}, 1000)
|
|
1145
|
+
} catch (e: any) {
|
|
1146
|
+
this.log(`Error: ${e.message}`)
|
|
1147
|
+
this.parts.statusBar.textContent = 'Error'
|
|
1148
|
+
this.parts.statusBar.classList.add('error')
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
render(): void {
|
|
1153
|
+
super.render()
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Public method to set source code (auto-runs when examples are loaded)
|
|
1157
|
+
setSource(code: string, exampleName?: string) {
|
|
1158
|
+
// Save current edits before switching
|
|
1159
|
+
if (this.currentExampleName) {
|
|
1160
|
+
this.editorCache.set(this.currentExampleName, this.parts.tjsEditor.value)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Update current example tracking
|
|
1164
|
+
this.currentExampleName = exampleName || null
|
|
1165
|
+
this.originalCode = code
|
|
1166
|
+
|
|
1167
|
+
// Check if we have cached edits for this example
|
|
1168
|
+
const cachedCode = exampleName ? this.editorCache.get(exampleName) : null
|
|
1169
|
+
this.parts.tjsEditor.value = cachedCode || code
|
|
1170
|
+
|
|
1171
|
+
// Update revert button visibility
|
|
1172
|
+
this.updateRevertButton()
|
|
1173
|
+
|
|
1174
|
+
// Transpile and run sequentially to avoid race conditions
|
|
1175
|
+
this.transpileAndRun()
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private transpileAndRun = async () => {
|
|
1179
|
+
const mySeq = this.transpileSeq // Capture current seq before transpile increments it
|
|
1180
|
+
await this.transpileAsync()
|
|
1181
|
+
// Only run if this is still the current transpilation
|
|
1182
|
+
if (this.transpileSeq === mySeq + 1) {
|
|
1183
|
+
await this.run()
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Navigate to a specific line in the source editor
|
|
1188
|
+
goToSourceLine(line: number, column: number = 1) {
|
|
1189
|
+
this.parts.inputTabs.value = 0 // Switch to TJS tab (first tab)
|
|
1190
|
+
// Wait for tab switch and editor resize before scrolling
|
|
1191
|
+
setTimeout(() => {
|
|
1192
|
+
this.parts.tjsEditor.goToLine(line, column)
|
|
1193
|
+
}, 50)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Revert to the original example code
|
|
1197
|
+
revertToOriginal = () => {
|
|
1198
|
+
if (this.currentExampleName) {
|
|
1199
|
+
this.editorCache.delete(this.currentExampleName)
|
|
1200
|
+
}
|
|
1201
|
+
this.parts.tjsEditor.value = this.originalCode
|
|
1202
|
+
this.updateRevertButton()
|
|
1203
|
+
this.transpile()
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Update revert button state based on whether code has changed
|
|
1207
|
+
private updateRevertButton() {
|
|
1208
|
+
const hasChanges = this.parts.tjsEditor.value !== this.originalCode
|
|
1209
|
+
this.parts.revertBtn.disabled = !hasChanges
|
|
1210
|
+
this.parts.revertBtn.style.opacity = hasChanges ? '1' : '0.5'
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export const tjsPlayground = TJSPlayground.elementCreator({
|
|
1215
|
+
tag: 'tjs-playground',
|
|
1216
|
+
styleSpec: {
|
|
1217
|
+
':host': {
|
|
1218
|
+
display: 'flex',
|
|
1219
|
+
flexDirection: 'column',
|
|
1220
|
+
height: '100%',
|
|
1221
|
+
flex: '1 1 auto',
|
|
1222
|
+
background: 'var(--background, #fff)',
|
|
1223
|
+
color: 'var(--text-color, #1f2937)',
|
|
1224
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1225
|
+
},
|
|
1226
|
+
|
|
1227
|
+
':host .tjs-toolbar': {
|
|
1228
|
+
display: 'flex',
|
|
1229
|
+
alignItems: 'center',
|
|
1230
|
+
gap: '10px',
|
|
1231
|
+
padding: '8px 12px',
|
|
1232
|
+
background: 'var(--code-background, #f3f4f6)',
|
|
1233
|
+
borderBottom: '1px solid var(--code-border, #e5e7eb)',
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1236
|
+
':host .run-btn': {
|
|
1237
|
+
display: 'flex',
|
|
1238
|
+
alignItems: 'center',
|
|
1239
|
+
gap: '4px',
|
|
1240
|
+
padding: '6px 12px',
|
|
1241
|
+
background: 'var(--brand-color, #3d4a6b)',
|
|
1242
|
+
color: 'var(--brand-text-color, white)',
|
|
1243
|
+
border: 'none',
|
|
1244
|
+
borderRadius: '6px',
|
|
1245
|
+
cursor: 'pointer',
|
|
1246
|
+
fontWeight: '500',
|
|
1247
|
+
fontSize: '14px',
|
|
1248
|
+
},
|
|
1249
|
+
|
|
1250
|
+
':host .run-btn:hover': {
|
|
1251
|
+
filter: 'brightness(1.1)',
|
|
1252
|
+
},
|
|
1253
|
+
|
|
1254
|
+
':host .toolbar-separator': {
|
|
1255
|
+
width: '1px',
|
|
1256
|
+
height: '20px',
|
|
1257
|
+
background: 'var(--code-border, #d1d5db)',
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
':host .build-flags': {
|
|
1261
|
+
display: 'flex',
|
|
1262
|
+
alignItems: 'center',
|
|
1263
|
+
gap: '12px',
|
|
1264
|
+
},
|
|
1265
|
+
|
|
1266
|
+
':host .flag-label': {
|
|
1267
|
+
display: 'flex',
|
|
1268
|
+
alignItems: 'center',
|
|
1269
|
+
gap: '4px',
|
|
1270
|
+
fontSize: '13px',
|
|
1271
|
+
color: 'var(--text-color, #6b7280)',
|
|
1272
|
+
cursor: 'pointer',
|
|
1273
|
+
userSelect: 'none',
|
|
1274
|
+
},
|
|
1275
|
+
|
|
1276
|
+
':host .flag-label:hover': {
|
|
1277
|
+
color: 'var(--text-color, #374151)',
|
|
1278
|
+
},
|
|
1279
|
+
|
|
1280
|
+
':host .flag-label input[type="checkbox"]': {
|
|
1281
|
+
margin: '0',
|
|
1282
|
+
cursor: 'pointer',
|
|
1283
|
+
accentColor: 'var(--brand-color, #3d4a6b)',
|
|
1284
|
+
},
|
|
1285
|
+
|
|
1286
|
+
':host .module-name-input': {
|
|
1287
|
+
padding: '6px 10px',
|
|
1288
|
+
border: '1px solid var(--code-border, #d1d5db)',
|
|
1289
|
+
borderRadius: '6px',
|
|
1290
|
+
fontSize: '14px',
|
|
1291
|
+
fontFamily: 'ui-monospace, monospace',
|
|
1292
|
+
background: 'var(--background, #fff)',
|
|
1293
|
+
color: 'var(--text-color, #1f2937)',
|
|
1294
|
+
width: '160px',
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1297
|
+
':host .module-name-input:focus': {
|
|
1298
|
+
outline: 'none',
|
|
1299
|
+
borderColor: 'var(--brand-color, #3d4a6b)',
|
|
1300
|
+
boxShadow: '0 0 0 2px rgba(61, 74, 107, 0.2)',
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
':host .module-name-input::placeholder': {
|
|
1304
|
+
color: 'var(--text-color, #9ca3af)',
|
|
1305
|
+
opacity: '0.6',
|
|
1306
|
+
},
|
|
1307
|
+
|
|
1308
|
+
':host .save-btn': {
|
|
1309
|
+
display: 'flex',
|
|
1310
|
+
alignItems: 'center',
|
|
1311
|
+
gap: '4px',
|
|
1312
|
+
padding: '6px 12px',
|
|
1313
|
+
background: 'var(--code-background, #e5e7eb)',
|
|
1314
|
+
color: 'var(--text-color, #374151)',
|
|
1315
|
+
border: '1px solid var(--code-border, #d1d5db)',
|
|
1316
|
+
borderRadius: '6px',
|
|
1317
|
+
cursor: 'pointer',
|
|
1318
|
+
fontWeight: '500',
|
|
1319
|
+
fontSize: '14px',
|
|
1320
|
+
},
|
|
1321
|
+
|
|
1322
|
+
':host .save-btn:hover': {
|
|
1323
|
+
background: 'var(--brand-color, #3d4a6b)',
|
|
1324
|
+
color: 'var(--brand-text-color, white)',
|
|
1325
|
+
borderColor: 'var(--brand-color, #3d4a6b)',
|
|
1326
|
+
},
|
|
1327
|
+
|
|
1328
|
+
':host .revert-btn': {
|
|
1329
|
+
display: 'flex',
|
|
1330
|
+
alignItems: 'center',
|
|
1331
|
+
gap: '4px',
|
|
1332
|
+
padding: '6px 12px',
|
|
1333
|
+
background: 'var(--code-background, #e5e7eb)',
|
|
1334
|
+
color: 'var(--text-color, #374151)',
|
|
1335
|
+
border: '1px solid var(--code-border, #d1d5db)',
|
|
1336
|
+
borderRadius: '6px',
|
|
1337
|
+
cursor: 'pointer',
|
|
1338
|
+
fontWeight: '500',
|
|
1339
|
+
fontSize: '14px',
|
|
1340
|
+
transition: 'opacity 0.2s',
|
|
1341
|
+
},
|
|
1342
|
+
|
|
1343
|
+
':host .revert-btn:hover:not(:disabled)': {
|
|
1344
|
+
background: '#fef3c7',
|
|
1345
|
+
borderColor: '#f59e0b',
|
|
1346
|
+
color: '#92400e',
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
':host .revert-btn:disabled': {
|
|
1350
|
+
cursor: 'default',
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
':host .elastic': {
|
|
1354
|
+
flex: '1',
|
|
1355
|
+
},
|
|
1356
|
+
|
|
1357
|
+
':host .status-bar': {
|
|
1358
|
+
fontSize: '13px',
|
|
1359
|
+
color: 'var(--text-color, #6b7280)',
|
|
1360
|
+
opacity: '0.7',
|
|
1361
|
+
},
|
|
1362
|
+
|
|
1363
|
+
':host .status-bar.error': {
|
|
1364
|
+
color: '#dc2626',
|
|
1365
|
+
opacity: '1',
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
':host .tjs-main': {
|
|
1369
|
+
display: 'flex',
|
|
1370
|
+
flex: '1 1 auto',
|
|
1371
|
+
minHeight: '0',
|
|
1372
|
+
gap: '1px',
|
|
1373
|
+
background: 'var(--code-border, #e5e7eb)',
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1376
|
+
':host .tjs-input, :host .tjs-output': {
|
|
1377
|
+
flex: '1 1 50%',
|
|
1378
|
+
minWidth: '0',
|
|
1379
|
+
display: 'flex',
|
|
1380
|
+
flexDirection: 'column',
|
|
1381
|
+
background: 'var(--background, #fff)',
|
|
1382
|
+
overflow: 'hidden',
|
|
1383
|
+
},
|
|
1384
|
+
|
|
1385
|
+
':host .tjs-input xin-tabs, :host .tjs-output xin-tabs': {
|
|
1386
|
+
flex: '1 1 auto',
|
|
1387
|
+
display: 'flex',
|
|
1388
|
+
flexDirection: 'column',
|
|
1389
|
+
minHeight: '0',
|
|
1390
|
+
},
|
|
1391
|
+
|
|
1392
|
+
// Tab content panels need explicit background for dark mode
|
|
1393
|
+
':host xin-tabs > [name]': {
|
|
1394
|
+
background: 'var(--background, #fff)',
|
|
1395
|
+
color: 'var(--text-color, #1f2937)',
|
|
1396
|
+
},
|
|
1397
|
+
|
|
1398
|
+
// Editor wrapper - contains the shadow DOM code-mirror component
|
|
1399
|
+
':host .editor-wrapper': {
|
|
1400
|
+
flex: '1 1 auto',
|
|
1401
|
+
height: '100%',
|
|
1402
|
+
minHeight: '300px',
|
|
1403
|
+
position: 'relative',
|
|
1404
|
+
overflow: 'hidden',
|
|
1405
|
+
},
|
|
1406
|
+
|
|
1407
|
+
// code-mirror is shadow DOM, so we just size it - internal styles are handled by the component
|
|
1408
|
+
':host .editor-wrapper code-mirror': {
|
|
1409
|
+
display: 'block',
|
|
1410
|
+
position: 'absolute',
|
|
1411
|
+
top: '0',
|
|
1412
|
+
left: '0',
|
|
1413
|
+
right: '0',
|
|
1414
|
+
bottom: '0',
|
|
1415
|
+
},
|
|
1416
|
+
|
|
1417
|
+
':host .js-output': {
|
|
1418
|
+
margin: '0',
|
|
1419
|
+
padding: '12px',
|
|
1420
|
+
background: 'var(--code-background, #f3f4f6)',
|
|
1421
|
+
color: 'var(--text-color, #1f2937)',
|
|
1422
|
+
fontSize: '13px',
|
|
1423
|
+
fontFamily: 'ui-monospace, monospace',
|
|
1424
|
+
overflow: 'auto',
|
|
1425
|
+
height: '100%',
|
|
1426
|
+
whiteSpace: 'pre-wrap',
|
|
1427
|
+
},
|
|
1428
|
+
|
|
1429
|
+
':host .preview-container': {
|
|
1430
|
+
height: '100%',
|
|
1431
|
+
background: 'var(--background, #fff)',
|
|
1432
|
+
},
|
|
1433
|
+
|
|
1434
|
+
':host .preview-frame': {
|
|
1435
|
+
width: '100%',
|
|
1436
|
+
height: '100%',
|
|
1437
|
+
border: 'none',
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
':host .docs-output': {
|
|
1441
|
+
display: 'block',
|
|
1442
|
+
padding: '12px 16px',
|
|
1443
|
+
fontSize: '14px',
|
|
1444
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1445
|
+
color: 'var(--text-color, inherit)',
|
|
1446
|
+
background: 'var(--background, #fff)',
|
|
1447
|
+
height: '100%',
|
|
1448
|
+
overflow: 'auto',
|
|
1449
|
+
},
|
|
1450
|
+
|
|
1451
|
+
':host .docs-output h2': {
|
|
1452
|
+
fontSize: '1.25em',
|
|
1453
|
+
marginTop: '0',
|
|
1454
|
+
marginBottom: '0.5em',
|
|
1455
|
+
color: 'var(--text-color, #1f2937)',
|
|
1456
|
+
},
|
|
1457
|
+
|
|
1458
|
+
':host .docs-output pre': {
|
|
1459
|
+
background: 'var(--code-background, #f3f4f6)',
|
|
1460
|
+
padding: '8px 12px',
|
|
1461
|
+
borderRadius: '6px',
|
|
1462
|
+
overflow: 'auto',
|
|
1463
|
+
fontSize: '13px',
|
|
1464
|
+
},
|
|
1465
|
+
|
|
1466
|
+
':host .docs-output code': {
|
|
1467
|
+
fontFamily: 'ui-monospace, monospace',
|
|
1468
|
+
fontSize: '0.9em',
|
|
1469
|
+
},
|
|
1470
|
+
|
|
1471
|
+
':host .docs-output p': {
|
|
1472
|
+
margin: '0.75em 0',
|
|
1473
|
+
lineHeight: '1.5',
|
|
1474
|
+
},
|
|
1475
|
+
|
|
1476
|
+
':host .docs-output h3': {
|
|
1477
|
+
fontSize: '1em',
|
|
1478
|
+
marginTop: '1em',
|
|
1479
|
+
marginBottom: '0.5em',
|
|
1480
|
+
},
|
|
1481
|
+
|
|
1482
|
+
':host .docs-output ul': {
|
|
1483
|
+
paddingLeft: '1.5em',
|
|
1484
|
+
margin: '0.5em 0',
|
|
1485
|
+
},
|
|
1486
|
+
|
|
1487
|
+
':host .docs-output li': {
|
|
1488
|
+
marginBottom: '0.25em',
|
|
1489
|
+
},
|
|
1490
|
+
|
|
1491
|
+
':host .docs-output hr': {
|
|
1492
|
+
border: 'none',
|
|
1493
|
+
borderTop: '1px solid var(--code-border, #e5e7eb)',
|
|
1494
|
+
margin: '1.5em 0',
|
|
1495
|
+
},
|
|
1496
|
+
|
|
1497
|
+
':host .tests-output': {
|
|
1498
|
+
padding: '12px',
|
|
1499
|
+
fontSize: '14px',
|
|
1500
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1501
|
+
color: 'var(--text-color, inherit)',
|
|
1502
|
+
background: 'var(--background, #fff)',
|
|
1503
|
+
height: '100%',
|
|
1504
|
+
overflow: 'auto',
|
|
1505
|
+
},
|
|
1506
|
+
|
|
1507
|
+
':host .test-summary': {
|
|
1508
|
+
marginBottom: '12px',
|
|
1509
|
+
paddingBottom: '8px',
|
|
1510
|
+
borderBottom: '1px solid var(--code-border, #e5e7eb)',
|
|
1511
|
+
},
|
|
1512
|
+
|
|
1513
|
+
':host .test-failed': {
|
|
1514
|
+
color: '#dc2626',
|
|
1515
|
+
},
|
|
1516
|
+
|
|
1517
|
+
':host .test-list': {
|
|
1518
|
+
listStyle: 'none',
|
|
1519
|
+
padding: 0,
|
|
1520
|
+
margin: 0,
|
|
1521
|
+
},
|
|
1522
|
+
|
|
1523
|
+
':host .test-list li': {
|
|
1524
|
+
padding: '4px 0',
|
|
1525
|
+
},
|
|
1526
|
+
|
|
1527
|
+
':host .test-pass': {
|
|
1528
|
+
color: '#16a34a',
|
|
1529
|
+
},
|
|
1530
|
+
|
|
1531
|
+
':host .test-fail': {
|
|
1532
|
+
color: '#dc2626',
|
|
1533
|
+
},
|
|
1534
|
+
|
|
1535
|
+
':host .test-error': {
|
|
1536
|
+
marginLeft: '20px',
|
|
1537
|
+
marginTop: '4px',
|
|
1538
|
+
padding: '8px',
|
|
1539
|
+
background: 'rgba(220, 38, 38, 0.1)',
|
|
1540
|
+
borderRadius: '4px',
|
|
1541
|
+
fontSize: '13px',
|
|
1542
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
1543
|
+
},
|
|
1544
|
+
|
|
1545
|
+
':host .clickable-error': {
|
|
1546
|
+
cursor: 'pointer',
|
|
1547
|
+
textDecoration: 'underline',
|
|
1548
|
+
textDecorationStyle: 'dotted',
|
|
1549
|
+
},
|
|
1550
|
+
|
|
1551
|
+
':host .clickable-error:hover': {
|
|
1552
|
+
background: 'rgba(220, 38, 38, 0.2)',
|
|
1553
|
+
},
|
|
1554
|
+
|
|
1555
|
+
':host .sig-badge': {
|
|
1556
|
+
fontSize: '11px',
|
|
1557
|
+
padding: '2px 6px',
|
|
1558
|
+
marginLeft: '8px',
|
|
1559
|
+
background: 'rgba(99, 102, 241, 0.1)',
|
|
1560
|
+
color: '#6366f1',
|
|
1561
|
+
borderRadius: '4px',
|
|
1562
|
+
},
|
|
1563
|
+
|
|
1564
|
+
':host .tjs-console': {
|
|
1565
|
+
height: '120px',
|
|
1566
|
+
borderTop: '1px solid var(--code-border, #e5e7eb)',
|
|
1567
|
+
display: 'flex',
|
|
1568
|
+
flexDirection: 'column',
|
|
1569
|
+
},
|
|
1570
|
+
|
|
1571
|
+
':host .console-header': {
|
|
1572
|
+
padding: '4px 12px',
|
|
1573
|
+
background: 'var(--code-background, #f3f4f6)',
|
|
1574
|
+
fontSize: '12px',
|
|
1575
|
+
fontWeight: '500',
|
|
1576
|
+
color: 'var(--text-color, #6b7280)',
|
|
1577
|
+
opacity: '0.7',
|
|
1578
|
+
borderBottom: '1px solid var(--code-border, #e5e7eb)',
|
|
1579
|
+
},
|
|
1580
|
+
|
|
1581
|
+
':host .console-output': {
|
|
1582
|
+
flex: '1',
|
|
1583
|
+
margin: '0',
|
|
1584
|
+
padding: '8px 12px',
|
|
1585
|
+
background: 'var(--code-background, #f3f4f6)',
|
|
1586
|
+
color: 'var(--text-color, #1f2937)',
|
|
1587
|
+
fontSize: '12px',
|
|
1588
|
+
fontFamily: 'ui-monospace, monospace',
|
|
1589
|
+
overflow: 'auto',
|
|
1590
|
+
whiteSpace: 'pre-wrap',
|
|
1591
|
+
},
|
|
1592
|
+
|
|
1593
|
+
':host .clickable-line': {
|
|
1594
|
+
cursor: 'pointer',
|
|
1595
|
+
color: '#2563eb',
|
|
1596
|
+
textDecoration: 'underline',
|
|
1597
|
+
textDecorationStyle: 'dotted',
|
|
1598
|
+
},
|
|
1599
|
+
|
|
1600
|
+
':host .clickable-line:hover': {
|
|
1601
|
+
color: '#1d4ed8',
|
|
1602
|
+
background: 'rgba(37, 99, 235, 0.1)',
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
}) as ElementCreator<TJSPlayground>
|