@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 +145 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.js +188 -0
- package/package.json +50 -0
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, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"')
|
|
51
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|