lilylet-live-editor 0.0.4
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/.github/workflows/deploy.yml +81 -0
- package/README.md +78 -0
- package/package.json +50 -0
- package/scripts/build-docs.js +523 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/components/Editor.svelte +164 -0
- package/src/lib/components/Player.svelte +529 -0
- package/src/lib/components/Preview.svelte +496 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/lilylet/highlight.ts +179 -0
- package/src/lib/lilylet/index.ts +93 -0
- package/src/lib/stores/editor.ts +119 -0
- package/src/lib/utils/share.ts +68 -0
- package/src/lib/verovio/toolkit.ts +83 -0
- package/src/modules.d.ts +41 -0
- package/src/routes/+layout.svelte +8 -0
- package/src/routes/+layout.ts +3 -0
- package/src/routes/+page.svelte +596 -0
- package/src/routes/markdown/+page.svelte +980 -0
- package/static/docs/lilylet-tutorial.md +932 -0
- package/static/js/.gitkeep +7 -0
- package/static/js/musicWidgetsBrowser.umd.min.js +2 -0
- package/static/soundfont/acoustic_grand_piano-mp3.js +93 -0
- package/static/soundfont/acoustic_grand_piano-ogg.js +93 -0
- package/svelte.config.js +22 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +15 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build documentation - converts markdown files to styled HTML
|
|
4
|
+
* with rendered Lilylet sheet music examples
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import MarkdownIt from 'markdown-it';
|
|
11
|
+
import markdownItAnchor from 'markdown-it-anchor';
|
|
12
|
+
import { parseCode, meiEncoder } from '@k-l-lambda/lilylet';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
// Initialize Verovio
|
|
19
|
+
async function initVerovio() {
|
|
20
|
+
const verovioModule = await import('verovio');
|
|
21
|
+
const verovio = verovioModule.default;
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
verovio.module.onRuntimeInitialized = () => {
|
|
24
|
+
const toolkit = new verovio.toolkit();
|
|
25
|
+
toolkit.setOptions({
|
|
26
|
+
scale: 35,
|
|
27
|
+
adjustPageHeight: true,
|
|
28
|
+
pageWidth: 1800,
|
|
29
|
+
pageMarginLeft: 20,
|
|
30
|
+
pageMarginRight: 20,
|
|
31
|
+
pageMarginTop: 20,
|
|
32
|
+
pageMarginBottom: 20,
|
|
33
|
+
});
|
|
34
|
+
resolve(toolkit);
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Escape HTML special characters
|
|
40
|
+
function escapeHtml(str) {
|
|
41
|
+
return str
|
|
42
|
+
.replace(/&/g, '&')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>')
|
|
45
|
+
.replace(/"/g, '"');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Render lilylet code to SVG
|
|
49
|
+
async function renderLilyletToSvg(code, toolkit) {
|
|
50
|
+
try {
|
|
51
|
+
const doc = await parseCode(code);
|
|
52
|
+
const mei = meiEncoder.encode(doc);
|
|
53
|
+
const loaded = toolkit.loadData(mei);
|
|
54
|
+
if (!loaded) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return toolkit.renderToSVG(1);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(` Warning: Failed to render lilylet: ${error.message}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// HTML template with styling
|
|
65
|
+
const htmlTemplate = (title, content) => `<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>${title}</title>
|
|
71
|
+
<style>
|
|
72
|
+
:root {
|
|
73
|
+
--bg-primary: #1e1e1e;
|
|
74
|
+
--bg-secondary: #252526;
|
|
75
|
+
--bg-tertiary: #2d2d30;
|
|
76
|
+
--text-primary: #d4d4d4;
|
|
77
|
+
--text-secondary: #858585;
|
|
78
|
+
--accent: #0e639c;
|
|
79
|
+
--accent-hover: #1177bb;
|
|
80
|
+
--border: #454545;
|
|
81
|
+
--code-bg: #1a1a1a;
|
|
82
|
+
--link: #4fc1ff;
|
|
83
|
+
--success: #4ec9b0;
|
|
84
|
+
--warning: #dcdcaa;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
* {
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
body {
|
|
92
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
93
|
+
background: var(--bg-primary);
|
|
94
|
+
color: var(--text-primary);
|
|
95
|
+
line-height: 1.6;
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.container {
|
|
101
|
+
max-width: 900px;
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
padding: 20px 40px 60px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
header {
|
|
107
|
+
background: var(--bg-secondary);
|
|
108
|
+
border-bottom: 1px solid var(--border);
|
|
109
|
+
padding: 12px 20px;
|
|
110
|
+
position: sticky;
|
|
111
|
+
top: 0;
|
|
112
|
+
z-index: 100;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
header nav {
|
|
116
|
+
max-width: 900px;
|
|
117
|
+
margin: 0 auto;
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 20px;
|
|
120
|
+
align-items: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
header a {
|
|
124
|
+
color: var(--text-secondary);
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
font-size: 14px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
header a:hover {
|
|
130
|
+
color: var(--text-primary);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
header a.active {
|
|
134
|
+
color: var(--link);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
header .brand {
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
color: var(--text-primary);
|
|
140
|
+
font-size: 16px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
h1 {
|
|
144
|
+
color: #ffffff;
|
|
145
|
+
font-size: 2.5em;
|
|
146
|
+
margin: 1em 0 0.5em;
|
|
147
|
+
padding-bottom: 0.3em;
|
|
148
|
+
border-bottom: 1px solid var(--border);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
h2 {
|
|
152
|
+
color: #ffffff;
|
|
153
|
+
font-size: 1.8em;
|
|
154
|
+
margin: 1.5em 0 0.5em;
|
|
155
|
+
padding-bottom: 0.2em;
|
|
156
|
+
border-bottom: 1px solid var(--border);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
h3 {
|
|
160
|
+
color: var(--success);
|
|
161
|
+
font-size: 1.3em;
|
|
162
|
+
margin: 1.2em 0 0.5em;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
h4 {
|
|
166
|
+
color: var(--warning);
|
|
167
|
+
font-size: 1.1em;
|
|
168
|
+
margin: 1em 0 0.5em;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
p {
|
|
172
|
+
margin: 0.8em 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
a {
|
|
176
|
+
color: var(--link);
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
a:hover {
|
|
181
|
+
text-decoration: underline;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
code {
|
|
185
|
+
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
186
|
+
background: var(--code-bg);
|
|
187
|
+
padding: 2px 6px;
|
|
188
|
+
border-radius: 3px;
|
|
189
|
+
font-size: 0.9em;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
pre {
|
|
193
|
+
background: var(--code-bg);
|
|
194
|
+
padding: 16px 20px;
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
overflow-x: auto;
|
|
197
|
+
border: 1px solid var(--border);
|
|
198
|
+
margin: 1em 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pre code {
|
|
202
|
+
background: none;
|
|
203
|
+
padding: 0;
|
|
204
|
+
font-size: 14px;
|
|
205
|
+
line-height: 1.5;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Lilylet example container - code + rendered music */
|
|
209
|
+
.lilylet-example {
|
|
210
|
+
margin: 1.2em 0;
|
|
211
|
+
border-radius: 8px;
|
|
212
|
+
overflow: hidden;
|
|
213
|
+
border: 1px solid var(--border);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.lilylet-example .lilylet-code {
|
|
217
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
218
|
+
border-left: 4px solid var(--accent);
|
|
219
|
+
margin: 0;
|
|
220
|
+
border: none;
|
|
221
|
+
border-radius: 0;
|
|
222
|
+
border-bottom: 1px solid var(--border);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.lilylet-example .lilylet-code code {
|
|
226
|
+
color: #e0e0e0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.lilylet-example .lilylet-render {
|
|
230
|
+
background: #ffffff;
|
|
231
|
+
padding: 16px;
|
|
232
|
+
display: flex;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
align-items: center;
|
|
235
|
+
min-height: 80px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.lilylet-example .lilylet-render svg {
|
|
239
|
+
max-width: 100%;
|
|
240
|
+
height: auto;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.lilylet-example .lilylet-error {
|
|
244
|
+
background: #2d1a1a;
|
|
245
|
+
color: #ff6b6b;
|
|
246
|
+
padding: 12px 16px;
|
|
247
|
+
font-size: 0.9em;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Standalone code blocks (non-lilylet) */
|
|
251
|
+
pre.lilylet-code {
|
|
252
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
253
|
+
border-left: 4px solid var(--accent);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
pre.lilylet-code code {
|
|
257
|
+
color: #e0e0e0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
table {
|
|
261
|
+
width: 100%;
|
|
262
|
+
border-collapse: collapse;
|
|
263
|
+
margin: 1em 0;
|
|
264
|
+
font-size: 0.95em;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
th, td {
|
|
268
|
+
border: 1px solid var(--border);
|
|
269
|
+
padding: 10px 14px;
|
|
270
|
+
text-align: left;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
th {
|
|
274
|
+
background: var(--bg-tertiary);
|
|
275
|
+
color: #ffffff;
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
tr:nth-child(even) {
|
|
280
|
+
background: var(--bg-secondary);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
ul, ol {
|
|
284
|
+
padding-left: 1.5em;
|
|
285
|
+
margin: 0.8em 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
li {
|
|
289
|
+
margin: 0.3em 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
blockquote {
|
|
293
|
+
border-left: 4px solid var(--accent);
|
|
294
|
+
margin: 1em 0;
|
|
295
|
+
padding: 0.5em 1em;
|
|
296
|
+
background: var(--bg-secondary);
|
|
297
|
+
border-radius: 0 4px 4px 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
hr {
|
|
301
|
+
border: none;
|
|
302
|
+
border-top: 1px solid var(--border);
|
|
303
|
+
margin: 2em 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
strong {
|
|
307
|
+
color: #ffffff;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
em {
|
|
311
|
+
color: var(--warning);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Table of contents styling */
|
|
315
|
+
.container > ol:first-of-type {
|
|
316
|
+
background: var(--bg-secondary);
|
|
317
|
+
padding: 20px 20px 20px 40px;
|
|
318
|
+
border-radius: 8px;
|
|
319
|
+
border: 1px solid var(--border);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.container > ol:first-of-type li {
|
|
323
|
+
margin: 0.4em 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Responsive design */
|
|
327
|
+
@media (max-width: 768px) {
|
|
328
|
+
.container {
|
|
329
|
+
padding: 15px 20px 40px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
h1 {
|
|
333
|
+
font-size: 2em;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
h2 {
|
|
337
|
+
font-size: 1.5em;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
pre {
|
|
341
|
+
padding: 12px 14px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
table {
|
|
345
|
+
font-size: 0.85em;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
th, td {
|
|
349
|
+
padding: 8px 10px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.lilylet-example .lilylet-render {
|
|
353
|
+
padding: 10px;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Print styles */
|
|
358
|
+
@media print {
|
|
359
|
+
body {
|
|
360
|
+
background: white;
|
|
361
|
+
color: black;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
header {
|
|
365
|
+
display: none;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pre {
|
|
369
|
+
background: #f5f5f5;
|
|
370
|
+
border: 1px solid #ddd;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.lilylet-example .lilylet-code {
|
|
374
|
+
background: #f0f0f5;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
a {
|
|
378
|
+
color: #0066cc;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
</style>
|
|
382
|
+
</head>
|
|
383
|
+
<body>
|
|
384
|
+
<header>
|
|
385
|
+
<nav>
|
|
386
|
+
<a href="/lilylet-live-editor/" class="brand">Lilylet</a>
|
|
387
|
+
<a href="/lilylet-live-editor/markdown">Markdown Demo</a>
|
|
388
|
+
<a href="/lilylet-live-editor/docs/lilylet-tutorial.html" class="active">Tutorial</a>
|
|
389
|
+
</nav>
|
|
390
|
+
</header>
|
|
391
|
+
<div class="container">
|
|
392
|
+
${content}
|
|
393
|
+
</div>
|
|
394
|
+
</body>
|
|
395
|
+
</html>`;
|
|
396
|
+
|
|
397
|
+
// Process markdown files
|
|
398
|
+
async function buildDocs() {
|
|
399
|
+
console.log('Initializing Verovio...');
|
|
400
|
+
const toolkit = await initVerovio();
|
|
401
|
+
console.log('Verovio initialized.\n');
|
|
402
|
+
|
|
403
|
+
// Initialize markdown-it with anchor plugin for heading IDs
|
|
404
|
+
const md = new MarkdownIt({
|
|
405
|
+
html: true,
|
|
406
|
+
linkify: true,
|
|
407
|
+
typographer: true
|
|
408
|
+
}).use(markdownItAnchor, {
|
|
409
|
+
slugify: (s) => s.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').trim(),
|
|
410
|
+
permalink: false
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Custom renderer for lilylet code blocks - renders both code and music
|
|
414
|
+
const defaultFence = md.renderer.rules.fence;
|
|
415
|
+
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|
416
|
+
const token = tokens[idx];
|
|
417
|
+
const info = token.info.trim();
|
|
418
|
+
const content = token.content.trim();
|
|
419
|
+
|
|
420
|
+
// Check if it's a lilylet code block
|
|
421
|
+
if (info === 'lilylet' || info === 'lyl') {
|
|
422
|
+
const escaped = escapeHtml(content);
|
|
423
|
+
const codeBlock = `<pre class="lilylet-code"><code class="language-lilylet">${escaped}</code></pre>`;
|
|
424
|
+
|
|
425
|
+
// Get pre-rendered SVG from env
|
|
426
|
+
const svg = env.lilyletRenderings?.[content];
|
|
427
|
+
|
|
428
|
+
if (svg) {
|
|
429
|
+
return `<div class="lilylet-example">${codeBlock}<div class="lilylet-render">${svg}</div></div>`;
|
|
430
|
+
} else {
|
|
431
|
+
// Fallback: just show code if rendering failed
|
|
432
|
+
return `<div class="lilylet-example">${codeBlock}<div class="lilylet-error">Unable to render notation</div></div>`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return defaultFence(tokens, idx, options, env, self);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const docsDir = path.join(rootDir, 'static', 'docs');
|
|
440
|
+
const outputDir = path.join(rootDir, 'static', 'docs');
|
|
441
|
+
|
|
442
|
+
// Ensure output directory exists
|
|
443
|
+
if (!fs.existsSync(outputDir)) {
|
|
444
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Find all markdown files
|
|
448
|
+
const mdFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
449
|
+
|
|
450
|
+
for (const mdFile of mdFiles) {
|
|
451
|
+
const mdPath = path.join(docsDir, mdFile);
|
|
452
|
+
const htmlFile = mdFile.replace('.md', '.html');
|
|
453
|
+
const htmlPath = path.join(outputDir, htmlFile);
|
|
454
|
+
|
|
455
|
+
console.log(`Converting ${mdFile} -> ${htmlFile}`);
|
|
456
|
+
|
|
457
|
+
// Read markdown content
|
|
458
|
+
const mdContent = fs.readFileSync(mdPath, 'utf-8');
|
|
459
|
+
|
|
460
|
+
// Extract title from first H1
|
|
461
|
+
const titleMatch = mdContent.match(/^#\s+(.+)$/m);
|
|
462
|
+
const title = titleMatch ? titleMatch[1] : 'Documentation';
|
|
463
|
+
|
|
464
|
+
// Pre-render all lilylet blocks
|
|
465
|
+
console.log(' Pre-rendering lilylet examples...');
|
|
466
|
+
const tokens = md.parse(mdContent, {});
|
|
467
|
+
const lilyletRenderings = {};
|
|
468
|
+
let renderCount = 0;
|
|
469
|
+
|
|
470
|
+
for (const token of tokens) {
|
|
471
|
+
if (token.type === 'fence') {
|
|
472
|
+
const info = token.info.trim();
|
|
473
|
+
if (info === 'lilylet' || info === 'lyl') {
|
|
474
|
+
const code = token.content.trim();
|
|
475
|
+
if (!lilyletRenderings[code]) {
|
|
476
|
+
const svg = await renderLilyletToSvg(code, toolkit);
|
|
477
|
+
if (svg) {
|
|
478
|
+
lilyletRenderings[code] = svg;
|
|
479
|
+
renderCount++;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log(` Rendered ${renderCount} music examples.`);
|
|
486
|
+
|
|
487
|
+
// Convert to HTML with pre-rendered SVGs
|
|
488
|
+
const env = { lilyletRenderings };
|
|
489
|
+
const htmlContent = md.render(mdContent, env);
|
|
490
|
+
|
|
491
|
+
// Generate full HTML page
|
|
492
|
+
const fullHtml = htmlTemplate(title, htmlContent);
|
|
493
|
+
|
|
494
|
+
// Write HTML file
|
|
495
|
+
fs.writeFileSync(htmlPath, fullHtml);
|
|
496
|
+
console.log(` Written: ${htmlPath}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Create index redirect if tutorial exists
|
|
500
|
+
const tutorialHtml = path.join(outputDir, 'lilylet-tutorial.html');
|
|
501
|
+
if (fs.existsSync(tutorialHtml)) {
|
|
502
|
+
const indexPath = path.join(outputDir, 'index.html');
|
|
503
|
+
const indexContent = `<!DOCTYPE html>
|
|
504
|
+
<html>
|
|
505
|
+
<head>
|
|
506
|
+
<meta http-equiv="refresh" content="0; url=lilylet-tutorial.html">
|
|
507
|
+
<title>Redirecting...</title>
|
|
508
|
+
</head>
|
|
509
|
+
<body>
|
|
510
|
+
<p>Redirecting to <a href="lilylet-tutorial.html">tutorial</a>...</p>
|
|
511
|
+
</body>
|
|
512
|
+
</html>`;
|
|
513
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
514
|
+
console.log(` Written: ${indexPath} (redirect)`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log('\nDocs build complete!');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
buildDocs().catch(err => {
|
|
521
|
+
console.error('Build failed:', err);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
});
|
package/src/app.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
2
|
+
// for information about these interfaces
|
|
3
|
+
declare global {
|
|
4
|
+
namespace App {
|
|
5
|
+
// interface Error {}
|
|
6
|
+
// interface Locals {}
|
|
7
|
+
// interface PageData {}
|
|
8
|
+
// interface PageState {}
|
|
9
|
+
// interface Platform {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {};
|
package/src/app.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
%sveltekit.head%
|
|
7
|
+
</head>
|
|
8
|
+
<body data-sveltekit-preload-data="hover">
|
|
9
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
3
|
+
import { EditorView, basicSetup } from 'codemirror';
|
|
4
|
+
import { EditorState } from '@codemirror/state';
|
|
5
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
6
|
+
import { editorStore } from '$lib/stores/editor';
|
|
7
|
+
import { lilylet } from '$lib/lilylet/highlight';
|
|
8
|
+
|
|
9
|
+
const dispatch = createEventDispatcher<{ filedrop: File }>();
|
|
10
|
+
|
|
11
|
+
let editorContainer: HTMLDivElement;
|
|
12
|
+
let view: EditorView | null = null;
|
|
13
|
+
let storeUnsubscribe: (() => void) | null = null;
|
|
14
|
+
|
|
15
|
+
// Debounce timer
|
|
16
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
|
|
18
|
+
// Track if the editor is the source of the change
|
|
19
|
+
let isEditorUpdate = false;
|
|
20
|
+
|
|
21
|
+
// Check if a file is a supported music file
|
|
22
|
+
function isMusicFile(fileName: string): boolean {
|
|
23
|
+
const lower = fileName.toLowerCase();
|
|
24
|
+
return lower.endsWith('.ly') || lower.endsWith('.ily') ||
|
|
25
|
+
lower.endsWith('.musicxml') || lower.endsWith('.mxl') ||
|
|
26
|
+
(lower.endsWith('.xml') && !lower.endsWith('.mei.xml'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Extension to intercept drop events for music files
|
|
30
|
+
const dropHandler = EditorView.domEventHandlers({
|
|
31
|
+
drop(event: DragEvent) {
|
|
32
|
+
const files = event.dataTransfer?.files;
|
|
33
|
+
if (files && files.length > 0) {
|
|
34
|
+
const file = files[0];
|
|
35
|
+
if (isMusicFile(file.name)) {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
event.stopPropagation();
|
|
38
|
+
dispatch('filedrop', file);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function handleUpdate(update: any) {
|
|
47
|
+
if (update.docChanged) {
|
|
48
|
+
const code = update.state.doc.toString();
|
|
49
|
+
|
|
50
|
+
// Mark that the editor is updating the store
|
|
51
|
+
isEditorUpdate = true;
|
|
52
|
+
|
|
53
|
+
// Debounce the store update
|
|
54
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
55
|
+
debounceTimer = setTimeout(() => {
|
|
56
|
+
editorStore.setCode(code);
|
|
57
|
+
// Reset flag after the store update is dispatched
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
isEditorUpdate = false;
|
|
60
|
+
}, 0);
|
|
61
|
+
}, 300);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setEditorContent(code: string) {
|
|
66
|
+
if (!view) return;
|
|
67
|
+
|
|
68
|
+
const currentContent = view.state.doc.toString();
|
|
69
|
+
if (currentContent !== code) {
|
|
70
|
+
view.dispatch({
|
|
71
|
+
changes: {
|
|
72
|
+
from: 0,
|
|
73
|
+
to: currentContent.length,
|
|
74
|
+
insert: code
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
onMount(async () => {
|
|
81
|
+
// Get initial code from store
|
|
82
|
+
let initialCode = '';
|
|
83
|
+
const initUnsubscribe = editorStore.subscribe((state) => {
|
|
84
|
+
initialCode = state.code;
|
|
85
|
+
});
|
|
86
|
+
initUnsubscribe();
|
|
87
|
+
|
|
88
|
+
const state = EditorState.create({
|
|
89
|
+
doc: initialCode,
|
|
90
|
+
extensions: [
|
|
91
|
+
basicSetup,
|
|
92
|
+
oneDark,
|
|
93
|
+
lilylet(),
|
|
94
|
+
EditorView.updateListener.of(handleUpdate),
|
|
95
|
+
EditorView.lineWrapping,
|
|
96
|
+
dropHandler
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
view = new EditorView({
|
|
101
|
+
state,
|
|
102
|
+
parent: editorContainer
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Subscribe to store changes to handle external updates (like URL loading)
|
|
106
|
+
storeUnsubscribe = editorStore.subscribe((state) => {
|
|
107
|
+
// Only update if the change came from outside the editor
|
|
108
|
+
if (!isEditorUpdate && view) {
|
|
109
|
+
const currentContent = view.state.doc.toString();
|
|
110
|
+
if (currentContent !== state.code) {
|
|
111
|
+
setEditorContent(state.code);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
onDestroy(() => {
|
|
118
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
119
|
+
if (storeUnsubscribe) storeUnsubscribe();
|
|
120
|
+
view?.destroy();
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<div class="editor-wrapper">
|
|
125
|
+
<div class="editor-header">
|
|
126
|
+
<span class="title">Lilylet Editor</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="editor-container" bind:this={editorContainer}></div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<style>
|
|
132
|
+
.editor-wrapper {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
height: 100%;
|
|
136
|
+
background: #282c34;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.editor-header {
|
|
140
|
+
padding: 8px 16px;
|
|
141
|
+
background: #21252b;
|
|
142
|
+
border-bottom: 1px solid #181a1f;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.title {
|
|
146
|
+
color: #abb2bf;
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.editor-container {
|
|
152
|
+
flex: 1;
|
|
153
|
+
overflow: auto;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.editor-container :global(.cm-editor) {
|
|
157
|
+
height: 100%;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.editor-container :global(.cm-scroller) {
|
|
161
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
162
|
+
font-size: 14px;
|
|
163
|
+
}
|
|
164
|
+
</style>
|