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.
@@ -0,0 +1,93 @@
1
+ // Lilylet conversion utilities
2
+ import * as lilylet from '@k-l-lambda/lilylet';
3
+
4
+ export type ConversionResult = {
5
+ success: true;
6
+ data: string;
7
+ } | {
8
+ success: false;
9
+ error: string;
10
+ };
11
+
12
+ export type MEIResult = {
13
+ success: true;
14
+ mei: string;
15
+ measureCount: number;
16
+ staffCount: number;
17
+ } | {
18
+ success: false;
19
+ error: string;
20
+ };
21
+
22
+ /**
23
+ * Convert Lilylet code to MEI XML
24
+ */
25
+ export function lilyletToMEI(code: string): MEIResult {
26
+ try {
27
+ // Parse Lilylet code to LilyletDoc
28
+ const doc = lilylet.parseCode(code);
29
+
30
+ // Encode to MEI XML
31
+ const mei = lilylet.meiEncoder.encode(doc);
32
+
33
+ // Calculate total staff count
34
+ let staffCount = 1;
35
+ if (doc.measures && doc.measures.length > 0) {
36
+ const firstMeasure = doc.measures[0];
37
+ staffCount = firstMeasure.parts.reduce((total, part) => {
38
+ const maxStaff = part.voices.reduce((max, voice) => Math.max(max, voice.staff || 1), 1);
39
+ return total + maxStaff;
40
+ }, 0) || 1;
41
+ }
42
+
43
+ return {
44
+ success: true,
45
+ mei,
46
+ measureCount: doc.measures?.length || 1,
47
+ staffCount
48
+ };
49
+ } catch (error) {
50
+ const errorMessage = error instanceof Error ? error.message : String(error);
51
+ console.error('Lilylet parsing error:', error);
52
+ return { success: false, error: errorMessage };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Convert MusicXML to Lilylet code
58
+ */
59
+ export function musicXmlToLilylet(xml: string): ConversionResult {
60
+ try {
61
+ const doc = lilylet.musicXmlDecoder.decode(xml);
62
+ const code = lilylet.serializeLilyletDoc(doc);
63
+ return { success: true, data: code };
64
+ } catch (error) {
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
66
+ console.error('MusicXML conversion error:', error);
67
+ return { success: false, error: `MusicXML conversion failed: ${errorMessage}` };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Convert LilyPond to Lilylet code
73
+ * Note: lilypondDecoder requires optional @k-l-lambda/lotus dependency
74
+ */
75
+ export function lilypondToLilylet(source: string): ConversionResult {
76
+ try {
77
+ // lilypondDecoder is not included in the main lilylet export
78
+ // It requires the optional @k-l-lambda/lotus dependency
79
+ const lilypondDecoder = (lilylet as any).lilypondDecoder;
80
+ if (!lilypondDecoder) {
81
+ return { success: false, error: 'LilyPond decoder not available (requires @k-l-lambda/lotus)' };
82
+ }
83
+ const doc = lilypondDecoder.decode(source);
84
+ const code = lilylet.serializeLilyletDoc(doc);
85
+ return { success: true, data: code };
86
+ } catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
+ console.error('LilyPond conversion error:', error);
89
+ return { success: false, error: `LilyPond conversion failed: ${errorMessage}` };
90
+ }
91
+ }
92
+
93
+ export { lilylet } from './highlight';
@@ -0,0 +1,119 @@
1
+ import { writable, derived, type Writable, type Readable } from 'svelte/store';
2
+ import { browser } from '$app/environment';
3
+
4
+ const STORAGE_KEY = 'lilylet-editor-code';
5
+
6
+ export interface LogEntry {
7
+ timestamp: Date;
8
+ level: 'info' | 'warning' | 'error';
9
+ message: string;
10
+ }
11
+
12
+ export interface EditorState {
13
+ code: string;
14
+ error: string | null;
15
+ mei: string | null;
16
+ svg: string | null;
17
+ pageCount: number;
18
+ isRendering: boolean;
19
+ verovioReady: boolean;
20
+ cursorElementId: string | null;
21
+ previewWidth: number;
22
+ logs: LogEntry[];
23
+ logsExpanded: boolean;
24
+ }
25
+
26
+ // Sample Lilylet code demonstrating basic syntax
27
+ const defaultCode = `[title "Jesu, meine Freude"]
28
+ [subtitle "BWV 610"]
29
+ [composer "J.S. Bach"]
30
+
31
+ \\staff "1" \\key c \\minor \\time 4/4 \\clef "treble" \\stemUp g'4 g f ef \\\\
32
+ \\staff "1" \\stemDown ef16[ d ef8]~ ef16[ f ef d] c8[ d]~ d[ c] \\\\
33
+ \\staff "2" \\clef "bass" c16[ b c8]~ c16[ b c g] a8[ g]~ g16[ g af ef] \\\\
34
+ \\staff "3" \\clef "bass" r8 c,16[ d] ef[ d ef8]~ ef16[ a, b g] c[ b c8] | % 1
35
+
36
+ \\staff "1" \\stemUp d2 c\\fermata \\\\
37
+ \\staff "1" \\stemDown c8[ c4 b8] c8.[ \\staff "2" \\stemUp g16] \\staff "1" c[ b c d] \\\\
38
+ \\staff "2" f,16[ ef f d] g[ af g f] ef[ d ef8]~ ef16[ f ef d] \\\\
39
+ \\staff "3" r16 g,[ af f] g[ f g8] c,2 | % 2
40
+ `;
41
+
42
+ // Load saved code from localStorage (browser only)
43
+ function loadSavedCode(): string {
44
+ if (browser) {
45
+ const saved = localStorage.getItem(STORAGE_KEY);
46
+ if (saved) return saved;
47
+ }
48
+ return defaultCode;
49
+ }
50
+
51
+ // Save code to localStorage (browser only)
52
+ function saveCode(code: string): void {
53
+ if (browser) {
54
+ localStorage.setItem(STORAGE_KEY, code);
55
+ }
56
+ }
57
+
58
+ const initialState: EditorState = {
59
+ code: defaultCode, // Will be updated in browser
60
+ error: null,
61
+ mei: null,
62
+ svg: null,
63
+ pageCount: 0,
64
+ isRendering: false,
65
+ verovioReady: false,
66
+ cursorElementId: null,
67
+ previewWidth: 800,
68
+ logs: [],
69
+ logsExpanded: false
70
+ };
71
+
72
+ function createEditorStore() {
73
+ const { subscribe, set, update }: Writable<EditorState> = writable(initialState);
74
+
75
+ // Load saved code in browser
76
+ if (browser) {
77
+ const savedCode = loadSavedCode();
78
+ if (savedCode !== defaultCode) {
79
+ update((s) => ({ ...s, code: savedCode }));
80
+ }
81
+ }
82
+
83
+ return {
84
+ subscribe,
85
+ setCode: (code: string) => {
86
+ saveCode(code);
87
+ update((s) => ({ ...s, code }));
88
+ },
89
+ setMEI: (mei: string) => update((s) => ({ ...s, mei })),
90
+ setSVG: (svg: string, pageCount: number) =>
91
+ update((s) => ({ ...s, svg, pageCount, error: null })),
92
+ setError: (error: string) => update((s) => ({ ...s, error, svg: null })),
93
+ setRendering: (isRendering: boolean) => update((s) => ({ ...s, isRendering })),
94
+ setVerovioReady: (verovioReady: boolean) => update((s) => ({ ...s, verovioReady })),
95
+ setCursorElement: (cursorElementId: string | null) => update((s) => ({ ...s, cursorElementId })),
96
+ setPreviewWidth: (previewWidth: number) => update((s) => ({ ...s, previewWidth })),
97
+ addLog: (level: LogEntry['level'], message: string) => update((s) => {
98
+ const newLog: LogEntry = { timestamp: new Date(), level, message };
99
+ const newLogs = [...s.logs, newLog].slice(-100); // Keep last 100 logs
100
+ // Auto-expand on error
101
+ const shouldExpand = level === 'error' ? true : s.logsExpanded;
102
+ return { ...s, logs: newLogs, logsExpanded: shouldExpand };
103
+ }),
104
+ setLogsExpanded: (expanded: boolean) => update((s) => ({ ...s, logsExpanded: expanded })),
105
+ clearLogs: () => update((s) => ({ ...s, logs: [] })),
106
+ reset: () => set(initialState)
107
+ };
108
+ }
109
+
110
+ export const editorStore = createEditorStore();
111
+
112
+ // Derived store for checking if we have valid output
113
+ export const hasOutput: Readable<boolean> = derived(editorStore, ($state) => $state.svg !== null);
114
+
115
+ // Derived store for checking if there's an error
116
+ export const hasError: Readable<boolean> = derived(
117
+ editorStore,
118
+ ($state) => $state.error !== null
119
+ );
@@ -0,0 +1,68 @@
1
+ // URL sharing utilities with pako compression
2
+ import pako from 'pako';
3
+ import { Base64 } from 'js-base64';
4
+
5
+ export interface ShareState {
6
+ code: string;
7
+ }
8
+
9
+ /**
10
+ * Encode the editor state into a compressed, URL-safe string
11
+ */
12
+ export function encodeState(state: ShareState): string {
13
+ const json = JSON.stringify(state);
14
+ const compressed = pako.deflate(json, { level: 9 });
15
+ return Base64.fromUint8Array(compressed, true); // URL-safe base64
16
+ }
17
+
18
+ /**
19
+ * Decode a compressed, URL-safe string back into editor state
20
+ */
21
+ export function decodeState(encoded: string): ShareState | null {
22
+ try {
23
+ const compressed = Base64.toUint8Array(encoded);
24
+ const json = pako.inflate(compressed, { to: 'string' });
25
+ return JSON.parse(json);
26
+ } catch (e) {
27
+ console.error('Failed to decode state:', e);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get the current URL with the encoded state
34
+ */
35
+ export function getShareUrl(state: ShareState): string {
36
+ const encoded = encodeState(state);
37
+ const url = new URL(window.location.href);
38
+ url.searchParams.set('code', encoded);
39
+ return url.toString();
40
+ }
41
+
42
+ /**
43
+ * Parse state from the current URL
44
+ */
45
+ export function getStateFromUrl(): ShareState | null {
46
+ if (typeof window === 'undefined') return null;
47
+
48
+ const params = new URLSearchParams(window.location.search);
49
+ const encoded = params.get('code');
50
+
51
+ if (!encoded) return null;
52
+
53
+ return decodeState(encoded);
54
+ }
55
+
56
+ /**
57
+ * Copy the share URL to clipboard
58
+ */
59
+ export async function copyShareUrl(state: ShareState): Promise<boolean> {
60
+ try {
61
+ const url = getShareUrl(state);
62
+ await navigator.clipboard.writeText(url);
63
+ return true;
64
+ } catch (e) {
65
+ console.error('Failed to copy URL:', e);
66
+ return false;
67
+ }
68
+ }
@@ -0,0 +1,83 @@
1
+ // Verovio Toolkit wrapper for SvelteKit
2
+ import type { VerovioToolkit } from 'verovio';
3
+
4
+ let toolkit: VerovioToolkit | null = null;
5
+ let initPromise: Promise<VerovioToolkit> | null = null;
6
+
7
+ export interface RenderOptions {
8
+ pageWidth?: number;
9
+ pageHeight?: number;
10
+ scale?: number;
11
+ adjustPageHeight?: boolean;
12
+ breaks?: 'auto' | 'none' | 'encoded';
13
+ }
14
+
15
+ const defaultOptions: RenderOptions = {
16
+ pageWidth: 2100,
17
+ pageHeight: 2970,
18
+ scale: 40,
19
+ adjustPageHeight: true,
20
+ breaks: 'auto'
21
+ };
22
+
23
+ export async function initVerovio(): Promise<VerovioToolkit> {
24
+ if (toolkit) return toolkit;
25
+
26
+ if (initPromise) return initPromise;
27
+
28
+ initPromise = (async () => {
29
+ // Import the WASM module and toolkit separately for browser compatibility
30
+ const createVerovioModule = (await import('verovio/wasm')).default;
31
+ const { VerovioToolkit } = await import('verovio/esm');
32
+
33
+ // Initialize the WASM module
34
+ const VerovioModule = await createVerovioModule();
35
+
36
+ toolkit = new VerovioToolkit(VerovioModule);
37
+ toolkit.setOptions(defaultOptions);
38
+
39
+ console.log('Verovio initialized:', toolkit.getVersion());
40
+ return toolkit;
41
+ })();
42
+
43
+ return initPromise;
44
+ }
45
+
46
+ export function getToolkit(): VerovioToolkit | null {
47
+ return toolkit;
48
+ }
49
+
50
+ export async function renderMEI(
51
+ mei: string,
52
+ options?: Partial<RenderOptions>
53
+ ): Promise<{ svg: string; pageCount: number; midi?: string }> {
54
+ const tk = await initVerovio();
55
+
56
+ if (options) {
57
+ tk.setOptions({ ...defaultOptions, ...options });
58
+ }
59
+
60
+ const success = tk.loadData(mei);
61
+ if (!success) {
62
+ throw new Error('Failed to load MEI data');
63
+ }
64
+
65
+ const pageCount = tk.getPageCount();
66
+ const svg = tk.renderToSVG(1);
67
+
68
+ return { svg, pageCount };
69
+ }
70
+
71
+ export async function renderToMIDI(mei: string): Promise<string> {
72
+ const tk = await initVerovio();
73
+ tk.loadData(mei);
74
+ return tk.renderToMIDI();
75
+ }
76
+
77
+ export async function getElementsAtTime(millisec: number): Promise<{
78
+ notes: string[];
79
+ page: number;
80
+ }> {
81
+ const tk = await initVerovio();
82
+ return tk.getElementsAtTime(millisec);
83
+ }
@@ -0,0 +1,41 @@
1
+ // Module declarations for packages without types
2
+ declare module 'verovio' {
3
+ export interface VerovioToolkit {
4
+ loadData(data: string): boolean;
5
+ getPageCount(): number;
6
+ renderToSVG(page: number): string;
7
+ renderToMIDI(): string;
8
+ getElementsAtTime(millisec: number): { notes: string[]; page: number };
9
+ setOptions(options: any): void;
10
+ getVersion(): string;
11
+ }
12
+ }
13
+
14
+ declare module 'verovio/wasm' {
15
+ const createVerovioModule: () => Promise<any>;
16
+ export default createVerovioModule;
17
+ }
18
+
19
+ declare module 'verovio/esm' {
20
+ import type { VerovioToolkit as VT } from 'verovio';
21
+ export class VerovioToolkit implements VT {
22
+ constructor(module: any);
23
+ loadData(data: string): boolean;
24
+ getPageCount(): number;
25
+ renderToSVG(page: number): string;
26
+ renderToMIDI(): string;
27
+ getElementsAtTime(millisec: number): { notes: string[]; page: number };
28
+ setOptions(options: any): void;
29
+ getVersion(): string;
30
+ }
31
+ }
32
+
33
+ declare module '@k-l-lambda/music-widgets/dist/musicWidgetsBrowser.es.js' {
34
+ export const MidiAudio: {
35
+ loadPlugin(options: { soundfontUrl: string; api: string }): Promise<void>;
36
+ noteOn(channel: number, note: number | undefined, velocity: number | undefined, timestamp: number): void;
37
+ noteOff(channel: number, note: number | undefined, timestamp: number): void;
38
+ programChange(channel: number, program: number | undefined): void;
39
+ stopAllNotes?: () => void;
40
+ };
41
+ }
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ </script>
3
+
4
+ <svelte:head>
5
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎵</text></svg>" />
6
+ </svelte:head>
7
+
8
+ <slot />
@@ -0,0 +1,3 @@
1
+ export const prerender = true;
2
+ export const ssr = false;
3
+ export const trailingSlash = 'never';