@nsis/dent-ui 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.
@@ -0,0 +1,10 @@
1
+ $ vite build
2
+ 2:57:56 PM [vite-plugin-svelte] no Svelte config found at /home/runner/work/nsis-javascript-tools/nsis-javascript-tools/packages/dent-ui - using default configuration.
3
+ vite v8.1.3 building client environment for production...
4
+ 
5
+ rendering chunks...
6
+ computing gzip size...
7
+ dist/index.html 0.30 kB │ gzip: 0.22 kB
8
+ dist/assets/index-CqDOLnCc.js 403.94 kB │ gzip: 131.52 kB
9
+
10
+ ✓ built in 219ms
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @nsis/dent-ui
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 51036be: first version 🎉
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jan T. Sott
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @nsis/dent-ui
2
+
3
+ > Web UI for the Dent NSIS code formatter
4
+
5
+ ![License](https://img.shields.io/npm/l/%40nsis%2Fdent-ui?style=for-the-badge)
6
+ [![Version](https://img.shields.io/npm/v/@nsis/dent-ui?style=for-the-badge)](https://www.npmjs.org/package/@nsis/dent-ui)
7
+ [![Build](https://img.shields.io/github/actions/workflow/status/idleberg/nsis-javascript-tools/ci.yml?style=for-the-badge)](https://github.com/idleberg/nsis-javascript-tools/actions)
8
+
9
+ [Demo Time](https://idleberg.github.io/nsis-javascript-tools/dent/) 🙌
10
+
11
+ ## License
12
+
13
+ This work is licensed under [The MIT License](LICENSE).
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Dent UI</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@nsis/dent-ui",
3
+ "version": "0.1.0",
4
+ "description": "Web UI for the Dent NSIS code formatter",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/main.ts",
9
+ "./DentUI.svelte": "./src/App.svelte"
10
+ },
11
+ "engines": {
12
+ "node": ">=20.0.0"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/idleberg/nsis-javascript-tools.git"
17
+ },
18
+ "homepage": "https://github.com/idleberg/nsis-javascript-tools/tree/main/packages/dent-ui#readme",
19
+ "keywords": [
20
+ "nsis",
21
+ "formatter",
22
+ "ui"
23
+ ],
24
+ "dependencies": {
25
+ "@codemirror/commands": "^6.10.4",
26
+ "@codemirror/language": "^6.12.4",
27
+ "@codemirror/state": "^6.7.1",
28
+ "@codemirror/view": "^6.43.5",
29
+ "@lezer/highlight": "^1.2.3",
30
+ "svelte": "^5.56.4",
31
+ "@nsis/codemirror": "0.13.0",
32
+ "@nsis/dent": "0.13.0"
33
+ },
34
+ "devDependencies": {
35
+ "@sveltejs/vite-plugin-svelte": "^7.2.0",
36
+ "@vitest/browser": "^4.1.10",
37
+ "@vitest/browser-playwright": "^4.1.10",
38
+ "playwright": "^1.53.0",
39
+ "svelte-check": "^4.7.2",
40
+ "vite": "^8.1.3",
41
+ "vitest": "^4.1.9",
42
+ "vitest-browser-svelte": "^2.2.0"
43
+ },
44
+ "scripts": {
45
+ "build": "vite build",
46
+ "dev": "vite",
47
+ "lint": "concurrently --prefix-colors blue,green,magenta npm:lint:*",
48
+ "lint:biome": "biome check",
49
+ "lint:biome:ci": "biome ci",
50
+ "check": "svelte-check --tsconfig ./tsconfig.json",
51
+ "test": "vitest run"
52
+ }
53
+ }
package/src/App.svelte ADDED
@@ -0,0 +1,453 @@
1
+ <script lang="ts">
2
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
3
+ import { EditorState } from '@codemirror/state';
4
+ import { EditorView, keymap } from '@codemirror/view';
5
+ import { nsisLanguage } from '@nsis/codemirror';
6
+ import { createFormatter } from '@nsis/dent';
7
+ import { onDestroy, onMount } from 'svelte';
8
+ import Editor from './Editor.svelte';
9
+
10
+ let dark = $state(document.documentElement.dataset.theme !== 'light');
11
+ let themePreference = $state(localStorage.getItem('theme') || 'system');
12
+
13
+ let useTabs = $state(true);
14
+ let singleQuote = $state(false);
15
+ let trimEmptyLines = $state(true);
16
+ let indentSize = $state(2);
17
+ let printWidth = $state(120);
18
+ let endOfLine = $state<'lf' | 'crlf'>('lf');
19
+ let autoFormat = $state(true);
20
+
21
+ let status = $state('');
22
+ let error = $state('');
23
+ let hasOutput = $state(false);
24
+ let inputView: EditorView | undefined;
25
+ let outputView: EditorView | undefined;
26
+ let debounceTimer: ReturnType<typeof setTimeout>;
27
+ let statusTimer: ReturnType<typeof setTimeout>;
28
+
29
+ const mql = matchMedia('(prefers-color-scheme: light)');
30
+ function handleColorSchemeChange() {
31
+ if (themePreference === 'system') applyTheme('system');
32
+ }
33
+
34
+ onMount(() => {
35
+ mql.addEventListener('change', handleColorSchemeChange);
36
+ return () => mql.removeEventListener('change', handleColorSchemeChange);
37
+ });
38
+
39
+ onDestroy(() => {
40
+ clearTimeout(debounceTimer);
41
+ clearTimeout(statusTimer);
42
+ });
43
+
44
+ function applyTheme(pref: string) {
45
+ const isLight = pref === 'light' || (pref === 'system' && mql.matches);
46
+ if (isLight) {
47
+ document.documentElement.dataset.theme = 'light';
48
+ } else {
49
+ delete document.documentElement.dataset.theme;
50
+ }
51
+ localStorage.setItem('theme', pref);
52
+ dark = !isLight;
53
+ }
54
+
55
+ applyTheme(localStorage.getItem('theme') || 'system');
56
+
57
+ function scheduleAutoFormat() {
58
+ if (!autoFormat) return;
59
+ clearTimeout(debounceTimer);
60
+ debounceTimer = setTimeout(formatInput, 400);
61
+ }
62
+
63
+ function formatInput() {
64
+ error = '';
65
+ const source = inputView?.state.doc.toString() ?? '';
66
+
67
+ if (!source.trim()) {
68
+ setOutput('');
69
+ return;
70
+ }
71
+
72
+ try {
73
+ const { format } = createFormatter({
74
+ useTabs,
75
+ singleQuote,
76
+ trimEmptyLines,
77
+ indentSize,
78
+ printWidth,
79
+ endOfLine,
80
+ });
81
+ setOutput(format(source));
82
+ clearTimeout(statusTimer);
83
+ status = 'Formatted';
84
+ statusTimer = setTimeout(() => {
85
+ status = '';
86
+ }, 1500);
87
+ } catch (err) {
88
+ error = err instanceof Error ? err.message : String(err);
89
+ }
90
+ }
91
+
92
+ function setOutput(text: string) {
93
+ if (!outputView) return;
94
+ outputView.dispatch({
95
+ changes: { from: 0, to: outputView.state.doc.length, insert: text },
96
+ });
97
+ hasOutput = !!text.trim();
98
+ }
99
+
100
+ async function copyOutput() {
101
+ const text = outputView?.state.doc.toString() ?? '';
102
+ if (!text.trim()) return;
103
+ await navigator.clipboard.writeText(text);
104
+ clearTimeout(statusTimer);
105
+ status = 'Copied';
106
+ statusTimer = setTimeout(() => {
107
+ status = '';
108
+ }, 1500);
109
+ }
110
+
111
+ function clear() {
112
+ clearTimeout(debounceTimer);
113
+ if (inputView) {
114
+ inputView.dispatch({
115
+ changes: { from: 0, to: inputView.state.doc.length, insert: '' },
116
+ });
117
+ }
118
+ setOutput('');
119
+ error = '';
120
+ status = '';
121
+ }
122
+
123
+ const inputExtensions = [
124
+ nsisLanguage,
125
+ history(),
126
+ keymap.of([...defaultKeymap, ...historyKeymap]),
127
+ EditorView.lineWrapping,
128
+ EditorView.updateListener.of((update) => {
129
+ if (update.docChanged) scheduleAutoFormat();
130
+ }),
131
+ ];
132
+
133
+ const outputExtensions = [
134
+ nsisLanguage,
135
+ EditorState.readOnly.of(true),
136
+ EditorView.editable.of(false),
137
+ EditorView.lineWrapping,
138
+ ];
139
+ </script>
140
+
141
+ <header>
142
+ <h1>Dent UI</h1>
143
+ <fieldset class="settings" aria-label="Formatter settings">
144
+ <label
145
+ ><input
146
+ type="checkbox"
147
+ bind:checked={useTabs}
148
+ onchange={scheduleAutoFormat}
149
+ /> Tabs</label
150
+ >
151
+ <label
152
+ ><input
153
+ type="checkbox"
154
+ bind:checked={singleQuote}
155
+ onchange={scheduleAutoFormat}
156
+ /> Single quotes</label
157
+ >
158
+ <label
159
+ ><input
160
+ type="checkbox"
161
+ bind:checked={trimEmptyLines}
162
+ onchange={scheduleAutoFormat}
163
+ /> Trim empty lines</label
164
+ >
165
+ <label>
166
+ Indent
167
+ <select bind:value={indentSize} onchange={scheduleAutoFormat}>
168
+ <option value={2}>2</option>
169
+ <option value={4}>4</option>
170
+ <option value={8}>8</option>
171
+ </select>
172
+ </label>
173
+ <label>
174
+ Width
175
+ <input
176
+ type="number"
177
+ bind:value={printWidth}
178
+ onchange={scheduleAutoFormat}
179
+ min="40"
180
+ max="320"
181
+ step="10"
182
+ />
183
+ </label>
184
+ <label>
185
+ EOL
186
+ <select bind:value={endOfLine} onchange={scheduleAutoFormat}>
187
+ <option value="lf">LF</option>
188
+ <option value="crlf">CRLF</option>
189
+ </select>
190
+ </label>
191
+ </fieldset>
192
+ <select
193
+ class="theme-select"
194
+ aria-label="Color theme"
195
+ bind:value={themePreference}
196
+ onchange={() => applyTheme(themePreference)}
197
+ >
198
+ <option value="system">System</option>
199
+ <option value="light">Light</option>
200
+ <option value="dark">Dark</option>
201
+ </select>
202
+ </header>
203
+
204
+ <div class="editors">
205
+ <div class="editor-pane">
206
+ <div class="pane-header">Input</div>
207
+ <Editor
208
+ {dark}
209
+ extensions={inputExtensions}
210
+ label="NSIS input editor"
211
+ oncreate={(v) => { inputView = v; v.focus(); }}
212
+ />
213
+ </div>
214
+ <div class="editor-pane">
215
+ <div class="pane-header">Output</div>
216
+ <Editor
217
+ {dark}
218
+ extensions={outputExtensions}
219
+ label="Formatted output editor"
220
+ oncreate={(v) => (outputView = v)}
221
+ />
222
+ </div>
223
+ </div>
224
+
225
+ <footer>
226
+ <button class="btn-primary" onclick={formatInput}>Format</button>
227
+ <button class="btn-secondary" onclick={clear}>Clear</button>
228
+ <label class="auto-label">
229
+ <input type="checkbox" bind:checked={autoFormat} /> Auto-format
230
+ </label>
231
+ {#if status}
232
+ <span class="status" role="status" aria-live="polite">{status}</span>
233
+ {/if}
234
+ {#if error}
235
+ <span class="error" role="alert">{error}</span>
236
+ {/if}
237
+ <button
238
+ class="btn-secondary copy-btn"
239
+ onclick={copyOutput}
240
+ disabled={!hasOutput}>Copy</button
241
+ >
242
+ </footer>
243
+
244
+ <style>
245
+ :global(:root) {
246
+ --bg: #1e1e2e;
247
+ --surface: #313244;
248
+ --overlay: #45475a;
249
+ --text: #cdd6f4;
250
+ --subtext: #a6adc8;
251
+ --blue: #89b4fa;
252
+ --mauve: #cba6f7;
253
+ --red: #f38ba8;
254
+ --green: #a6e3a1;
255
+ --border: #585b70;
256
+ --radius: 6px;
257
+ }
258
+
259
+ :global([data-theme="light"]) {
260
+ --bg: #eff1f5;
261
+ --surface: #e6e9ef;
262
+ --overlay: #ccd0da;
263
+ --text: #4c4f69;
264
+ --subtext: #6c6f85;
265
+ --blue: #1e66f5;
266
+ --mauve: #8839ef;
267
+ --red: #d20f39;
268
+ --green: #40a02b;
269
+ --border: #bcc0cc;
270
+ }
271
+
272
+ :global(*) {
273
+ box-sizing: border-box;
274
+ margin: 0;
275
+ padding: 0;
276
+ }
277
+
278
+ :global(body) {
279
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
280
+ "Helvetica Neue", sans-serif;
281
+ background: var(--bg);
282
+ color: var(--text);
283
+ height: 100vh;
284
+ display: flex;
285
+ flex-direction: column;
286
+ overflow: hidden;
287
+ -webkit-user-select: none;
288
+ user-select: none;
289
+ }
290
+
291
+ :global(#app) {
292
+ display: contents;
293
+ }
294
+
295
+ header {
296
+ padding: 12px 16px;
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 16px;
300
+ border-bottom: 1px solid var(--border);
301
+ flex-shrink: 0;
302
+ }
303
+
304
+ header h1 {
305
+ font-size: 16px;
306
+ font-weight: 600;
307
+ color: var(--blue);
308
+ }
309
+
310
+ .settings {
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ align-items: center;
314
+ gap: 12px;
315
+ font-size: 13px;
316
+ border: none;
317
+ padding: 0;
318
+ }
319
+
320
+ .settings label {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 5px;
324
+ color: var(--subtext);
325
+ cursor: pointer;
326
+ }
327
+
328
+ .settings input[type="checkbox"] {
329
+ accent-color: var(--blue);
330
+ }
331
+
332
+ .settings select,
333
+ .settings input[type="number"] {
334
+ background: var(--surface);
335
+ color: var(--text);
336
+ border: 1px solid var(--border);
337
+ border-radius: var(--radius);
338
+ padding: 3px 6px;
339
+ font-size: 13px;
340
+ font-family: inherit;
341
+ }
342
+
343
+ .settings input[type="number"] {
344
+ width: 70px;
345
+ }
346
+
347
+ .theme-select {
348
+ margin-left: auto;
349
+ background: var(--surface);
350
+ color: var(--text);
351
+ border: 1px solid var(--border);
352
+ border-radius: var(--radius);
353
+ padding: 3px 6px;
354
+ font-size: 12px;
355
+ font-family: inherit;
356
+ cursor: pointer;
357
+ }
358
+
359
+ .editors {
360
+ flex: 1;
361
+ display: grid;
362
+ grid-template-columns: 1fr 1fr;
363
+ gap: 0;
364
+ min-height: 0;
365
+ }
366
+
367
+ .editor-pane {
368
+ display: flex;
369
+ flex-direction: column;
370
+ min-height: 0;
371
+ }
372
+
373
+ .editor-pane:first-child {
374
+ border-right: 1px solid var(--border);
375
+ }
376
+
377
+ .pane-header {
378
+ padding: 8px 12px;
379
+ font-size: 12px;
380
+ font-weight: 600;
381
+ text-transform: uppercase;
382
+ letter-spacing: 0.05em;
383
+ color: var(--subtext);
384
+ background: var(--surface);
385
+ border-bottom: 1px solid var(--border);
386
+ flex-shrink: 0;
387
+ }
388
+
389
+ footer {
390
+ padding: 8px 16px;
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 12px;
394
+ border-top: 1px solid var(--border);
395
+ flex-shrink: 0;
396
+ }
397
+
398
+ button {
399
+ font-family: inherit;
400
+ font-size: 13px;
401
+ font-weight: 500;
402
+ padding: 5px 14px;
403
+ border-radius: var(--radius);
404
+ border: 1px solid transparent;
405
+ cursor: pointer;
406
+ transition: background 0.15s;
407
+ }
408
+
409
+ .btn-primary {
410
+ background: var(--blue);
411
+ color: var(--bg);
412
+ }
413
+ .btn-primary:hover {
414
+ filter: brightness(1.1);
415
+ }
416
+
417
+ .btn-secondary {
418
+ background: var(--surface);
419
+ color: var(--text);
420
+ border-color: var(--border);
421
+ }
422
+ .btn-secondary:hover {
423
+ background: var(--overlay);
424
+ }
425
+
426
+ .status {
427
+ font-size: 12px;
428
+ color: var(--green);
429
+ }
430
+
431
+ .error {
432
+ font-size: 12px;
433
+ color: var(--red);
434
+ }
435
+
436
+ .copy-btn {
437
+ margin-left: auto;
438
+ }
439
+
440
+ .auto-label {
441
+ display: flex;
442
+ align-items: center;
443
+ gap: 5px;
444
+ font-size: 13px;
445
+ color: var(--subtext);
446
+ cursor: pointer;
447
+ }
448
+
449
+ button:disabled {
450
+ opacity: 0.4;
451
+ cursor: default;
452
+ }
453
+ </style>
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { Compartment, EditorState, type Extension } from '@codemirror/state';
3
+ import { EditorView } from '@codemirror/view';
4
+ import { onMount } from 'svelte';
5
+ import { darkTheme, editorTheme, lightTheme } from './themes.ts';
6
+
7
+ let {
8
+ dark = true,
9
+ extensions = [],
10
+ label = '',
11
+ oncreate,
12
+ }: {
13
+ dark?: boolean;
14
+ extensions?: Extension[];
15
+ label?: string;
16
+ oncreate?: (view: EditorView) => void;
17
+ } = $props();
18
+
19
+ let container: HTMLDivElement;
20
+ let view: EditorView | undefined;
21
+ const themeCompartment = new Compartment();
22
+
23
+ onMount(() => {
24
+ view = new EditorView({
25
+ state: EditorState.create({
26
+ doc: '',
27
+ extensions: [themeCompartment.of(dark ? darkTheme : lightTheme), editorTheme, ...extensions],
28
+ }),
29
+ parent: container,
30
+ });
31
+ oncreate?.(view);
32
+ return () => view?.destroy();
33
+ });
34
+
35
+ let initialized = false;
36
+ $effect(() => {
37
+ const theme = dark ? darkTheme : lightTheme;
38
+ if (initialized) {
39
+ view?.dispatch({ effects: themeCompartment.reconfigure(theme) });
40
+ }
41
+ initialized = true;
42
+ });
43
+ </script>
44
+
45
+ <div bind:this={container} aria-label={label}></div>
46
+
47
+ <style>
48
+ div {
49
+ flex: 1;
50
+ min-height: 0;
51
+ display: flex;
52
+ flex-direction: column;
53
+ }
54
+ div :global(.cm-editor) {
55
+ flex: 1;
56
+ min-height: 0;
57
+ }
58
+ </style>
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="svelte" />
package/src/main.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { mount } from 'svelte';
2
+ import App from './App.svelte';
3
+
4
+ const target = document.getElementById('app');
5
+ if (!target) throw new Error('Missing #app element');
6
+ mount(App, { target });
package/src/themes.ts ADDED
@@ -0,0 +1,198 @@
1
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
2
+ import { EditorView } from '@codemirror/view';
3
+ import { tags } from '@lezer/highlight';
4
+
5
+ export const editorTheme = EditorView.theme({
6
+ '&': { height: '100%', fontSize: '13px' },
7
+ '.cm-scroller': {
8
+ overflow: 'auto',
9
+ fontFamily: '"SF Mono", "Fira Code", "Cascadia Code", "JetBrains Mono", ui-monospace, monospace',
10
+ },
11
+ '.cm-content': { padding: '0.75rem 0' },
12
+ '.cm-line': { padding: '0 0.75rem' },
13
+ });
14
+
15
+ const mochaTheme = EditorView.theme(
16
+ {
17
+ '&': { color: '#cdd6f4', backgroundColor: '#1e1e2e' },
18
+ '.cm-content': { caretColor: '#f5e0dc' },
19
+ '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#f5e0dc' },
20
+ '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
21
+ { backgroundColor: '#585b7044' },
22
+ '.cm-activeLine': { backgroundColor: '#585b7015' },
23
+ '.cm-gutters': {
24
+ backgroundColor: '#1e1e2e',
25
+ color: '#6c7086',
26
+ border: 'none',
27
+ },
28
+ '.cm-activeLineGutter': { backgroundColor: '#585b7015' },
29
+ '.cm-selectionMatch': { backgroundColor: '#a6e3a122' },
30
+ '&.cm-focused .cm-matchingBracket': { backgroundColor: '#585b7044' },
31
+ '.cm-searchMatch': {
32
+ backgroundColor: '#f9e2af33',
33
+ outline: '1px solid #f9e2af66',
34
+ },
35
+ '.cm-searchMatch.cm-searchMatch-selected': { backgroundColor: '#a6e3a133' },
36
+ '.cm-foldPlaceholder': {
37
+ backgroundColor: 'transparent',
38
+ border: 'none',
39
+ color: '#6c7086',
40
+ },
41
+ '.cm-tooltip': { border: 'none', backgroundColor: '#313244' },
42
+ '.cm-tooltip-autocomplete': {
43
+ '& > ul > li[aria-selected]': {
44
+ backgroundColor: '#45475a',
45
+ color: '#cdd6f4',
46
+ },
47
+ },
48
+ },
49
+ { dark: true },
50
+ );
51
+
52
+ const mochaHighlightStyle = HighlightStyle.define([
53
+ { tag: tags.keyword, color: '#cba6f7' },
54
+ {
55
+ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
56
+ color: '#89b4fa',
57
+ },
58
+ { tag: [tags.function(tags.variableName), tags.labelName], color: '#89b4fa' },
59
+ {
60
+ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
61
+ color: '#fab387',
62
+ },
63
+ { tag: [tags.definition(tags.name), tags.separator], color: '#cdd6f4' },
64
+ {
65
+ tag: [
66
+ tags.typeName,
67
+ tags.className,
68
+ tags.number,
69
+ tags.changed,
70
+ tags.annotation,
71
+ tags.modifier,
72
+ tags.self,
73
+ tags.namespace,
74
+ ],
75
+ color: '#f9e2af',
76
+ },
77
+ {
78
+ tag: [
79
+ tags.operator,
80
+ tags.operatorKeyword,
81
+ tags.url,
82
+ tags.escape,
83
+ tags.regexp,
84
+ tags.link,
85
+ tags.special(tags.string),
86
+ ],
87
+ color: '#94e2d5',
88
+ },
89
+ { tag: [tags.meta, tags.comment], color: '#6c7086' },
90
+ { tag: tags.strong, fontWeight: 'bold' },
91
+ { tag: tags.emphasis, fontStyle: 'italic' },
92
+ { tag: tags.strikethrough, textDecoration: 'line-through' },
93
+ { tag: tags.link, color: '#94e2d5', textDecoration: 'underline' },
94
+ { tag: tags.heading, fontWeight: 'bold', color: '#f38ba8' },
95
+ {
96
+ tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
97
+ color: '#fab387',
98
+ },
99
+ {
100
+ tag: [tags.processingInstruction, tags.string, tags.inserted],
101
+ color: '#a6e3a1',
102
+ },
103
+ { tag: tags.invalid, color: '#f38ba8' },
104
+ ]);
105
+
106
+ const latteTheme = EditorView.theme(
107
+ {
108
+ '&': { color: '#4c4f69', backgroundColor: '#eff1f5' },
109
+ '.cm-content': { caretColor: '#dc8a78' },
110
+ '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#dc8a78' },
111
+ '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
112
+ { backgroundColor: '#acb0be44' },
113
+ '.cm-activeLine': { backgroundColor: '#acb0be15' },
114
+ '.cm-gutters': {
115
+ backgroundColor: '#eff1f5',
116
+ color: '#9ca0b0',
117
+ border: 'none',
118
+ },
119
+ '.cm-activeLineGutter': { backgroundColor: '#acb0be15' },
120
+ '.cm-selectionMatch': { backgroundColor: '#40a02b22' },
121
+ '&.cm-focused .cm-matchingBracket': { backgroundColor: '#acb0be44' },
122
+ '.cm-searchMatch': {
123
+ backgroundColor: '#df8e1d33',
124
+ outline: '1px solid #df8e1d66',
125
+ },
126
+ '.cm-searchMatch.cm-searchMatch-selected': { backgroundColor: '#40a02b33' },
127
+ '.cm-foldPlaceholder': {
128
+ backgroundColor: 'transparent',
129
+ border: 'none',
130
+ color: '#9ca0b0',
131
+ },
132
+ '.cm-tooltip': { border: 'none', backgroundColor: '#e6e9ef' },
133
+ '.cm-tooltip-autocomplete': {
134
+ '& > ul > li[aria-selected]': {
135
+ backgroundColor: '#ccd0da',
136
+ color: '#4c4f69',
137
+ },
138
+ },
139
+ },
140
+ { dark: false },
141
+ );
142
+
143
+ const latteHighlightStyle = HighlightStyle.define([
144
+ { tag: tags.keyword, color: '#8839ef' },
145
+ {
146
+ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
147
+ color: '#1e66f5',
148
+ },
149
+ { tag: [tags.function(tags.variableName), tags.labelName], color: '#1e66f5' },
150
+ {
151
+ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
152
+ color: '#fe640b',
153
+ },
154
+ { tag: [tags.definition(tags.name), tags.separator], color: '#4c4f69' },
155
+ {
156
+ tag: [
157
+ tags.typeName,
158
+ tags.className,
159
+ tags.number,
160
+ tags.changed,
161
+ tags.annotation,
162
+ tags.modifier,
163
+ tags.self,
164
+ tags.namespace,
165
+ ],
166
+ color: '#df8e1d',
167
+ },
168
+ {
169
+ tag: [
170
+ tags.operator,
171
+ tags.operatorKeyword,
172
+ tags.url,
173
+ tags.escape,
174
+ tags.regexp,
175
+ tags.link,
176
+ tags.special(tags.string),
177
+ ],
178
+ color: '#179299',
179
+ },
180
+ { tag: [tags.meta, tags.comment], color: '#9ca0b0' },
181
+ { tag: tags.strong, fontWeight: 'bold' },
182
+ { tag: tags.emphasis, fontStyle: 'italic' },
183
+ { tag: tags.strikethrough, textDecoration: 'line-through' },
184
+ { tag: tags.link, color: '#179299', textDecoration: 'underline' },
185
+ { tag: tags.heading, fontWeight: 'bold', color: '#d20f39' },
186
+ {
187
+ tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
188
+ color: '#fe640b',
189
+ },
190
+ {
191
+ tag: [tags.processingInstruction, tags.string, tags.inserted],
192
+ color: '#40a02b',
193
+ },
194
+ { tag: tags.invalid, color: '#d20f39' },
195
+ ]);
196
+
197
+ export const darkTheme = [mochaTheme, syntaxHighlighting(mochaHighlightStyle)];
198
+ export const lightTheme = [latteTheme, syntaxHighlighting(latteHighlightStyle)];
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { page } from 'vitest/browser';
3
+ import { render } from 'vitest-browser-svelte';
4
+ import App from '../src/App.svelte';
5
+
6
+ describe('App', () => {
7
+ it('renders the heading', async () => {
8
+ render(App);
9
+
10
+ await expect.element(page.getByRole('heading', { name: 'Dent UI' })).toBeVisible();
11
+ });
12
+
13
+ it('renders formatter settings fieldset', async () => {
14
+ render(App);
15
+
16
+ await expect.element(page.getByRole('group', { name: 'Formatter settings' })).toBeVisible();
17
+ });
18
+
19
+ it('renders the Format button', async () => {
20
+ render(App);
21
+
22
+ await expect.element(page.getByRole('button', { name: 'Format' })).toBeVisible();
23
+ });
24
+
25
+ it('renders the Clear button', async () => {
26
+ render(App);
27
+
28
+ await expect.element(page.getByRole('button', { name: 'Clear' })).toBeVisible();
29
+ });
30
+
31
+ it('renders the Copy button as disabled initially', async () => {
32
+ render(App);
33
+
34
+ await expect.element(page.getByRole('button', { name: 'Copy' })).toBeDisabled();
35
+ });
36
+
37
+ it('renders the theme selector', async () => {
38
+ render(App);
39
+
40
+ await expect.element(page.getByRole('combobox', { name: 'Color theme' })).toBeVisible();
41
+ });
42
+
43
+ it('renders the auto-format checkbox checked by default', async () => {
44
+ render(App);
45
+
46
+ await expect.element(page.getByRole('checkbox', { name: 'Auto-format' })).toBeChecked();
47
+ });
48
+
49
+ it('renders both editor panes', async () => {
50
+ render(App);
51
+
52
+ await expect.element(page.getByLabelText('NSIS input editor')).toBeVisible();
53
+ await expect.element(page.getByLabelText('Formatted output editor')).toBeVisible();
54
+ });
55
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { page } from 'vitest/browser';
3
+ import { render } from 'vitest-browser-svelte';
4
+ import Editor from '../src/Editor.svelte';
5
+
6
+ describe('Editor', () => {
7
+ it('renders a container div with aria-label', async () => {
8
+ render(Editor, {
9
+ props: { label: 'Test editor' },
10
+ });
11
+
12
+ await expect.element(page.getByLabelText('Test editor')).toBeVisible();
13
+ });
14
+
15
+ it('calls oncreate with an EditorView instance', () => {
16
+ const oncreate = vi.fn();
17
+
18
+ render(Editor, {
19
+ props: { label: 'Test editor', oncreate },
20
+ });
21
+
22
+ expect(oncreate).toHaveBeenCalledOnce();
23
+ const view = oncreate.mock.calls[0]?.[0];
24
+ expect(view).toBeDefined();
25
+ expect(typeof view?.dispatch).toBe('function');
26
+ });
27
+
28
+ it('initialises with an empty document', () => {
29
+ const oncreate = vi.fn();
30
+
31
+ render(Editor, {
32
+ props: { label: 'Test editor', oncreate },
33
+ });
34
+
35
+ const view = oncreate.mock.calls[0]?.[0];
36
+ expect(view?.state.doc.toString()).toBe('');
37
+ });
38
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { darkTheme, editorTheme, lightTheme } from '../src/themes.ts';
3
+
4
+ describe('themes', () => {
5
+ it('exports editorTheme as a CodeMirror extension', () => {
6
+ expect(editorTheme).toBeDefined();
7
+ });
8
+
9
+ it('exports darkTheme as an array of extensions', () => {
10
+ expect(Array.isArray(darkTheme)).toBe(true);
11
+ expect(darkTheme.length).toBe(2);
12
+ });
13
+
14
+ it('exports lightTheme as an array of extensions', () => {
15
+ expect(Array.isArray(lightTheme)).toBe(true);
16
+ expect(lightTheme.length).toBe(2);
17
+ });
18
+
19
+ it('darkTheme and lightTheme are different objects', () => {
20
+ expect(darkTheme).not.toBe(lightTheme);
21
+ expect(darkTheme[0]).not.toBe(lightTheme[0]);
22
+ });
23
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@idleberg/configs/tsc",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "verbatimModuleSyntax": true,
7
+ "lib": ["DOM", "DOM.Iterable", "ESNext"]
8
+ },
9
+ "include": ["src/**/*", "test/**/*"]
10
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
2
+ import { playwright } from '@vitest/browser-playwright';
3
+ import { defineConfig } from 'vitest/config';
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ svelte({
8
+ compilerOptions: { css: 'injected' },
9
+ }),
10
+ ],
11
+ resolve: {
12
+ conditions: ['browser'],
13
+ },
14
+ build: {
15
+ outDir: 'dist',
16
+ minify: true,
17
+ },
18
+ test: {
19
+ include: ['test/**/*.test.ts'],
20
+ browser: {
21
+ enabled: true,
22
+ provider: playwright(),
23
+ headless: true,
24
+ instances: [{ browser: 'chromium' }],
25
+ },
26
+ },
27
+ });