@k-l-lambda/lilylet-markdown 0.1.19

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/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # lilylet-markdown
2
+
3
+ A [markdown-it](https://github.com/markdown-it/markdown-it) plugin for rendering [Lilylet](https://github.com/k-l-lambda/lilylet) music notation in Markdown.
4
+
5
+ ## Features
6
+
7
+ - Render Lilylet notation in fenced code blocks
8
+ - Server-side SVG rendering with Verovio
9
+ - Client-side rendering support (placeholder mode)
10
+ - Supports `lilylet` and `lyl` language aliases
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @k-l-lambda/lilylet-markdown
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Basic Usage (Placeholder Mode)
21
+
22
+ When no Verovio toolkit is provided, the plugin outputs placeholders with embedded MEI data for client-side rendering:
23
+
24
+ ```javascript
25
+ import MarkdownIt from 'markdown-it';
26
+ import lilyletPlugin from '@k-l-lambda/lilylet-markdown';
27
+
28
+ const md = new MarkdownIt();
29
+ md.use(lilyletPlugin);
30
+
31
+ const result = md.render(`
32
+ # My Score
33
+
34
+ \`\`\`lilylet
35
+ \\key c \\major
36
+ \\time 4/4
37
+ c'4 d' e' f' | g'1
38
+ \`\`\`
39
+ `);
40
+ ```
41
+
42
+ Output:
43
+ ```html
44
+ <h1>My Score</h1>
45
+ <div class="lilylet-container" data-lilylet data-source="..." data-mei="..."></div>
46
+ ```
47
+
48
+ ### Server-side SVG Rendering
49
+
50
+ For server-side rendering, initialize Verovio and pass it to the plugin:
51
+
52
+ ```javascript
53
+ import MarkdownIt from 'markdown-it';
54
+ import lilyletPlugin, { initVerovio, prerender } from '@k-l-lambda/lilylet-markdown';
55
+
56
+ async function render(content) {
57
+ // Initialize Verovio
58
+ const verovioToolkit = await initVerovio();
59
+
60
+ // Create markdown-it instance with plugin
61
+ const md = new MarkdownIt();
62
+ md.use(lilyletPlugin, { verovioToolkit });
63
+
64
+ // Pre-render all lilylet blocks (async)
65
+ await prerender(md, content, { verovioToolkit });
66
+
67
+ // Render markdown (sync)
68
+ return md.render(content);
69
+ }
70
+ ```
71
+
72
+ ### Options
73
+
74
+ ```typescript
75
+ interface LilyletPluginOptions {
76
+ // Initialized Verovio toolkit instance
77
+ verovioToolkit?: VerovioToolkit;
78
+
79
+ // Verovio rendering options
80
+ verovioOptions?: {
81
+ scale?: number; // Default: 40
82
+ pageWidth?: number; // Default: 2000
83
+ adjustPageHeight?: boolean; // Default: true
84
+ };
85
+
86
+ // Language aliases that trigger rendering
87
+ langAliases?: string[]; // Default: ['lilylet', 'lyl']
88
+
89
+ // CSS class for container
90
+ containerClass?: string; // Default: 'lilylet-container'
91
+
92
+ // CSS class for errors
93
+ errorClass?: string; // Default: 'lilylet-error'
94
+
95
+ // Include source in data attribute
96
+ includeSource?: boolean; // Default: true
97
+ }
98
+ ```
99
+
100
+ ## Markdown Syntax
101
+
102
+ Use fenced code blocks with `lilylet` or `lyl` language identifier:
103
+
104
+ ~~~markdown
105
+ ```lilylet
106
+ \key g \major
107
+ \time 3/4
108
+ d'4 g' b' | d''2.
109
+ ```
110
+ ~~~
111
+
112
+ Or using the short alias:
113
+
114
+ ~~~markdown
115
+ ```lyl
116
+ c'4 d' e' f' | g'1
117
+ ```
118
+ ~~~
119
+
120
+ ## Client-side Rendering
121
+
122
+ For client-side rendering, use the embedded MEI data:
123
+
124
+ ```javascript
125
+ import createVerovioModule from 'verovio/wasm';
126
+
127
+ document.querySelectorAll('[data-lilylet]').forEach(async (container) => {
128
+ const mei = container.dataset.mei;
129
+ if (!mei) return;
130
+
131
+ const verovio = await createVerovioModule();
132
+ const toolkit = new verovio.toolkit();
133
+ toolkit.loadData(mei);
134
+ container.innerHTML = toolkit.renderToSVG(1);
135
+ });
136
+ ```
137
+
138
+ ## Similar Projects
139
+
140
+ - [remark-abcjs](https://github.com/breqdev/remark-abcjs) - Remark plugin for ABC notation
141
+ - [markdown-it-mermaid](https://github.com/tylingsoft/markdown-it-mermaid) - Mermaid diagrams in markdown
142
+
143
+ ## License
144
+
145
+ ISC
package/lib/index.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Lilylet Markdown-it Plugin
3
+ *
4
+ * Renders Lilylet music notation in fenced code blocks.
5
+ *
6
+ * Usage:
7
+ * ```lilylet
8
+ * c'4 d' e' f' | g'1
9
+ * ```
10
+ *
11
+ * Or with alias:
12
+ * ```lyl
13
+ * c'4 d' e' f' | g'1
14
+ * ```
15
+ */
16
+ import type MarkdownIt from 'markdown-it';
17
+ /**
18
+ * Verovio toolkit interface
19
+ */
20
+ export interface VerovioToolkit {
21
+ loadData(data: string): boolean;
22
+ renderToSVG(page?: number, options?: object): string;
23
+ getLog(): string;
24
+ setOptions?(options: object): void;
25
+ }
26
+ /**
27
+ * Plugin options
28
+ */
29
+ export interface LilyletPluginOptions {
30
+ /**
31
+ * Initialized Verovio toolkit instance.
32
+ * If not provided, MEI output will be wrapped in a data attribute for client-side rendering.
33
+ */
34
+ verovioToolkit?: VerovioToolkit;
35
+ /**
36
+ * Verovio rendering options
37
+ */
38
+ verovioOptions?: {
39
+ scale?: number;
40
+ pageWidth?: number;
41
+ pageHeight?: number;
42
+ adjustPageHeight?: boolean;
43
+ border?: number;
44
+ [key: string]: unknown;
45
+ };
46
+ /**
47
+ * Language aliases that trigger lilylet rendering.
48
+ * Default: ['lilylet', 'lyl']
49
+ */
50
+ langAliases?: string[];
51
+ /**
52
+ * CSS class for the container div.
53
+ * Default: 'lilylet-container'
54
+ */
55
+ containerClass?: string;
56
+ /**
57
+ * CSS class for error display.
58
+ * Default: 'lilylet-error'
59
+ */
60
+ errorClass?: string;
61
+ /**
62
+ * Whether to include the source code as a data attribute.
63
+ * Default: true
64
+ */
65
+ includeSource?: boolean;
66
+ }
67
+ /**
68
+ * Create the markdown-it plugin
69
+ */
70
+ export declare function lilyletPlugin(md: MarkdownIt, options?: LilyletPluginOptions): void;
71
+ /**
72
+ * Pre-render all lilylet blocks in markdown content.
73
+ * Call this before md.render() for async rendering.
74
+ */
75
+ export declare function prerender(md: MarkdownIt, content: string, options?: LilyletPluginOptions): Promise<void>;
76
+ /**
77
+ * Initialize Verovio toolkit (helper function)
78
+ */
79
+ export declare function initVerovio(): Promise<VerovioToolkit>;
80
+ export default lilyletPlugin;
package/lib/index.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Lilylet Markdown-it Plugin
3
+ *
4
+ * Renders Lilylet music notation in fenced code blocks.
5
+ *
6
+ * Usage:
7
+ * ```lilylet
8
+ * c'4 d' e' f' | g'1
9
+ * ```
10
+ *
11
+ * Or with alias:
12
+ * ```lyl
13
+ * c'4 d' e' f' | g'1
14
+ * ```
15
+ */
16
+ import { parseCode, meiEncoder } from '@k-l-lambda/lilylet';
17
+ const DEFAULT_OPTIONS = {
18
+ verovioToolkit: undefined,
19
+ verovioOptions: {
20
+ scale: 40,
21
+ adjustPageHeight: true,
22
+ pageWidth: 2000,
23
+ },
24
+ langAliases: ['lilylet', 'lyl'],
25
+ containerClass: 'lilylet-container',
26
+ errorClass: 'lilylet-error',
27
+ includeSource: true,
28
+ };
29
+ /**
30
+ * Check if language tag indicates playable mode
31
+ * e.g., "lyl.play", "lilylet.play"
32
+ */
33
+ function parseLanguageTag(info, aliases) {
34
+ const lower = info.toLowerCase();
35
+ // Check for .play suffix
36
+ if (lower.endsWith('.play')) {
37
+ const base = lower.slice(0, -5); // Remove ".play"
38
+ return { isLilylet: aliases.includes(base), isPlayable: true };
39
+ }
40
+ return { isLilylet: aliases.includes(lower), isPlayable: false };
41
+ }
42
+ /**
43
+ * Escape HTML special characters
44
+ */
45
+ function escapeHtml(str) {
46
+ return str
47
+ .replace(/&/g, '&amp;')
48
+ .replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;')
50
+ .replace(/"/g, '&quot;')
51
+ .replace(/'/g, '&#039;');
52
+ }
53
+ /**
54
+ * Render lilylet code to HTML
55
+ */
56
+ async function renderLilylet(code, options) {
57
+ const { verovioToolkit, verovioOptions, containerClass, errorClass, includeSource } = options;
58
+ try {
59
+ // Parse lilylet code
60
+ const doc = await parseCode(code);
61
+ // Encode to MEI
62
+ const mei = meiEncoder.encode(doc);
63
+ // Build data attributes
64
+ const sourceAttr = includeSource ? ` data-source="${escapeHtml(code)}"` : '';
65
+ const meiAttr = ` data-mei="${escapeHtml(mei)}"`;
66
+ // If no Verovio toolkit, return MEI for client-side rendering
67
+ if (!verovioToolkit) {
68
+ return `<div class="${containerClass}" data-lilylet${sourceAttr}${meiAttr}></div>`;
69
+ }
70
+ // Calculate pageHeight based on measure count
71
+ const measureCount = doc.measures?.length || 1;
72
+ const basePageHeight = 2000;
73
+ const measuresPerPage = 20;
74
+ const pageHeight = Math.max(basePageHeight, Math.ceil(measureCount / measuresPerPage) * basePageHeight);
75
+ // Set Verovio options with dynamic pageHeight
76
+ if (verovioToolkit.setOptions) {
77
+ verovioToolkit.setOptions({
78
+ ...verovioOptions,
79
+ pageHeight,
80
+ });
81
+ }
82
+ // Load MEI data
83
+ const loaded = verovioToolkit.loadData(mei);
84
+ if (!loaded) {
85
+ const log = verovioToolkit.getLog();
86
+ throw new Error(`Verovio failed to load MEI: ${log}`);
87
+ }
88
+ // Render to SVG
89
+ const svg = verovioToolkit.renderToSVG(1);
90
+ return `<div class="${containerClass}" data-lilylet${sourceAttr}${meiAttr}>${svg}</div>`;
91
+ }
92
+ catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : String(error);
94
+ return `<div class="${errorClass}" data-lilylet-error><pre>${escapeHtml(errorMessage)}</pre><pre>${escapeHtml(code)}</pre></div>`;
95
+ }
96
+ }
97
+ /**
98
+ * Synchronous render (for immediate use, returns placeholder if no toolkit)
99
+ */
100
+ function renderLilyletSync(code, options, cache, playable = false) {
101
+ // Check cache first (include playable flag in cache key)
102
+ const cacheKey = playable ? `play:${code}` : code;
103
+ const cached = cache.get(cacheKey);
104
+ if (cached !== undefined) {
105
+ return cached;
106
+ }
107
+ const { containerClass, includeSource } = options;
108
+ const sourceAttr = includeSource ? ` data-source="${escapeHtml(code)}"` : '';
109
+ const playableAttr = playable ? ' data-playable' : '';
110
+ // Return placeholder for async rendering
111
+ return `<div class="${containerClass}" data-lilylet-pending${playableAttr}${sourceAttr}><code>${escapeHtml(code)}</code></div>`;
112
+ }
113
+ /**
114
+ * Create the markdown-it plugin
115
+ */
116
+ export function lilyletPlugin(md, options = {}) {
117
+ const opts = {
118
+ ...DEFAULT_OPTIONS,
119
+ ...options,
120
+ verovioOptions: { ...DEFAULT_OPTIONS.verovioOptions, ...options.verovioOptions },
121
+ };
122
+ // Cache for pre-rendered content
123
+ const renderCache = new Map();
124
+ // Store original fence renderer
125
+ const originalFence = md.renderer.rules.fence || function (tokens, idx, options, env, self) {
126
+ return self.renderToken(tokens, idx, options);
127
+ };
128
+ // Override fence renderer
129
+ md.renderer.rules.fence = function (tokens, idx, mdOptions, env, self) {
130
+ const token = tokens[idx];
131
+ const info = token.info.trim();
132
+ const code = token.content.trim();
133
+ // Check if this is a lilylet block (with optional .play suffix)
134
+ const { isLilylet, isPlayable } = parseLanguageTag(info, opts.langAliases);
135
+ if (isLilylet) {
136
+ return renderLilyletSync(code, opts, renderCache, isPlayable);
137
+ }
138
+ // Fall back to original renderer
139
+ return originalFence(tokens, idx, mdOptions, env, self);
140
+ };
141
+ // Expose async render method for pre-rendering
142
+ md.lilylet = {
143
+ prerenderCode: async function (code) {
144
+ const result = await renderLilylet(code, opts);
145
+ renderCache.set(code, result);
146
+ return result;
147
+ }
148
+ };
149
+ }
150
+ /**
151
+ * Pre-render all lilylet blocks in markdown content.
152
+ * Call this before md.render() for async rendering.
153
+ */
154
+ export async function prerender(md, content, options = {}) {
155
+ const opts = {
156
+ ...DEFAULT_OPTIONS,
157
+ ...options,
158
+ verovioOptions: { ...DEFAULT_OPTIONS.verovioOptions, ...options.verovioOptions },
159
+ };
160
+ // Parse to find all lilylet blocks
161
+ const tokens = md.parse(content, {});
162
+ for (const token of tokens) {
163
+ if (token.type === 'fence') {
164
+ const { isLilylet } = parseLanguageTag(token.info.trim(), opts.langAliases);
165
+ if (isLilylet) {
166
+ const code = token.content.trim();
167
+ const lilyletExt = md.lilylet;
168
+ if (lilyletExt) {
169
+ await lilyletExt.prerenderCode(code);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Initialize Verovio toolkit (helper function)
177
+ */
178
+ export async function initVerovio() {
179
+ const verovioModule = await import('verovio');
180
+ const verovio = verovioModule.default;
181
+ return new Promise((resolve) => {
182
+ verovio.module.onRuntimeInitialized = () => {
183
+ resolve(new verovio.toolkit());
184
+ };
185
+ });
186
+ }
187
+ // Default export
188
+ export default lilyletPlugin;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@k-l-lambda/lilylet-markdown",
3
+ "version": "0.1.19",
4
+ "description": "Markdown-it plugin for rendering Lilylet music notation",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "node --experimental-vm-modules tests/test.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/k-l-lambda/lilylet-markdown.git"
25
+ },
26
+ "keywords": [
27
+ "markdown-it",
28
+ "markdown",
29
+ "music",
30
+ "notation",
31
+ "lilylet",
32
+ "lilypond",
33
+ "sheet-music"
34
+ ],
35
+ "author": "k.l.lambda",
36
+ "license": "ISC",
37
+ "dependencies": {
38
+ "@k-l-lambda/lilylet": "^0.1.30",
39
+ "verovio": "^4.3.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/markdown-it": "^14.0.0",
43
+ "@types/node": "^20.11.0",
44
+ "markdown-it": "^14.0.0",
45
+ "typescript": "^5.3.0"
46
+ },
47
+ "peerDependencies": {
48
+ "markdown-it": ">=12.0.0"
49
+ }
50
+ }