astro-lilypond 0.14.0 → 0.16.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.
- package/dist/index.d.ts +81 -11
- package/dist/index.js +99 -39
- package/dist/loader.d.ts +42 -31
- package/dist/loader.js +33 -55
- package/dist/plugins/rehype.js +5 -6
- package/dist/plugins/remark.js +5 -6
- package/dist/plugins/satteri.js +5 -6
- package/dist/readOutputFile.js +4 -7
- package/dist/render.d.ts +4 -29
- package/dist/render.js +1 -9
- package/dist/renderState.d.ts +29 -0
- package/dist/renderState.js +46 -0
- package/dist/utils/{emitLilypondAsset.fake.d.ts → __tests__/emitLilypondAsset.fake.d.ts} +1 -1
- package/dist/utils/__tests__/emitLilypondPdfAsset.fake.d.ts +5 -0
- package/dist/utils/__tests__/emitLilypondPdfAsset.fake.js +6 -0
- package/dist/utils/emitLilypondPdfAsset.d.ts +8 -0
- package/dist/utils/emitLilypondPdfAsset.js +12 -0
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +2 -2
- package/dist/utils/lilypondMetadata.d.ts +10 -0
- package/dist/utils/lilypondMetadata.js +19 -0
- package/dist/utils/lyTypeDeclarationsFor.d.ts +2 -6
- package/dist/utils/lyTypeDeclarationsFor.js +3 -8
- package/dist/utils/parseLyHeader.js +1 -2
- package/dist/utils/renderedHtml.d.ts +9 -1
- package/dist/utils/renderedHtml.js +15 -12
- package/dist/utils/resolveDefaults.js +1 -2
- package/package.json +4 -4
- package/components/LilyPond.astro +0 -52
- package/dist/utils/lyImportQuery.d.ts +0 -22
- package/dist/utils/lyImportQuery.js +0 -26
- package/dist/utils/splitHeaderFields.d.ts +0 -23
- package/dist/utils/splitHeaderFields.js +0 -32
- /package/dist/utils/{emitLilypondAsset.fake.js → __tests__/emitLilypondAsset.fake.js} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,40 +1,110 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
|
-
import { type
|
|
2
|
+
import { type AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
3
|
+
import type { AutoInstallOptions } from "./binary/index.js";
|
|
3
4
|
import { type PluginOptions } from "./plugins/index.js";
|
|
4
5
|
import { type LilypondDefaults } from "./render.js";
|
|
6
|
+
import { type LilypondMetadata } from "./utils/index.js";
|
|
5
7
|
export declare const LY_EXTENSIONS: readonly [".ly", ".lilypond", ".ily"];
|
|
6
|
-
export type { AutoInstallOptions, LilypondDefaults, PluginOptions as LilypondPluginOptions, };
|
|
8
|
+
export type { AstroComponentFactory, AutoInstallOptions, LilypondDefaults, LilypondMetadata, PluginOptions as LilypondPluginOptions, };
|
|
7
9
|
export interface LilypondPage {
|
|
8
10
|
src: string;
|
|
9
11
|
width?: number;
|
|
10
12
|
height?: number;
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The result of a `.ly`/`.ily`/`.lilypond` import
|
|
16
|
+
* or `lilypondLoader()` collection entry.
|
|
17
|
+
*/
|
|
18
|
+
export interface LilypondScore {
|
|
19
|
+
source: string;
|
|
20
|
+
alt: string;
|
|
21
|
+
sourceName: string | undefined;
|
|
22
|
+
includePaths: string[];
|
|
23
|
+
assetTitle: string;
|
|
24
|
+
meta: LilypondMetadata;
|
|
25
|
+
}
|
|
26
|
+
export interface RenderOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Output format for `Score`.
|
|
29
|
+
* @default "svg"
|
|
30
|
+
*/
|
|
31
|
+
format?: "svg" | "png";
|
|
32
|
+
/**
|
|
33
|
+
* Crop `Score` to a single tightly-fit image instead of full pages.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
crop?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Render a downloadable PDF of the same score, returned as `pdf`.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
pdf?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface LilypondImageResult {
|
|
13
44
|
pages: LilypondPage[];
|
|
14
45
|
alt?: string;
|
|
15
46
|
}
|
|
47
|
+
export interface LilypondPdfResult {
|
|
48
|
+
src: string;
|
|
49
|
+
}
|
|
50
|
+
export interface RenderResult {
|
|
51
|
+
Score: AstroComponentFactory;
|
|
52
|
+
pageCount: number;
|
|
53
|
+
pdf?: LilypondPdfResult;
|
|
54
|
+
meta: LilypondMetadata;
|
|
55
|
+
raw: string;
|
|
56
|
+
}
|
|
57
|
+
interface ScoreImageProps {
|
|
58
|
+
pageLimit?: number;
|
|
59
|
+
class?: string;
|
|
60
|
+
style?: string;
|
|
61
|
+
alt?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Renders a `LilypondScore` (from a `.ly`/`.ily`/`.lilypond` import,
|
|
65
|
+
* or a `lilypondLoader()` entry) to a renderable `<Score />` component.
|
|
66
|
+
*/
|
|
67
|
+
export declare function render(score: LilypondScore, options?: RenderOptions): Promise<RenderResult>;
|
|
68
|
+
export interface ScoreProps extends ScoreImageProps {
|
|
69
|
+
/**
|
|
70
|
+
* A `LilypondScore` from a `.ly`/`.ily`/`.lilypond` import
|
|
71
|
+
* or a `lilypondLoader()` entry.
|
|
72
|
+
*/
|
|
73
|
+
content: LilypondScore;
|
|
74
|
+
/**
|
|
75
|
+
* @default "svg"
|
|
76
|
+
*/
|
|
77
|
+
format?: "svg" | "png";
|
|
78
|
+
/**
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
crop?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Renders a `LilypondScore` directly, for the common case where none of
|
|
85
|
+
* `render()`'s `pageCount`, `meta`, `raw`, or `pdf` are needed. Use
|
|
86
|
+
* `render()` instead when you need those.
|
|
87
|
+
*/
|
|
88
|
+
export declare const Score: AstroComponentFactory;
|
|
89
|
+
export declare function renderAll(scores: LilypondScore[], options?: RenderOptions): Promise<RenderResult[]>;
|
|
16
90
|
export interface LilypondOptions extends PluginOptions {
|
|
17
91
|
/**
|
|
18
|
-
* Output format.
|
|
92
|
+
* Output format used by Markdown fences and `lilypondLoader()` entries.
|
|
19
93
|
* @default "svg"
|
|
20
94
|
*/
|
|
21
95
|
format?: "svg" | "png";
|
|
22
96
|
/**
|
|
23
|
-
* Defaults passed to each score.
|
|
24
|
-
* Defaults can still be overridden by individual `.ly` files.
|
|
97
|
+
* Defaults passed to each score; can be overridden at render time.
|
|
25
98
|
*/
|
|
26
99
|
defaults?: LilypondDefaults;
|
|
27
100
|
/**
|
|
28
|
-
*
|
|
29
|
-
* aborting it.
|
|
101
|
+
* Ms to wait for a single `lilypond` invocation before aborting.
|
|
30
102
|
* @default 60000
|
|
31
103
|
*/
|
|
32
104
|
timeout?: number;
|
|
33
105
|
/**
|
|
34
106
|
* When no `lilypond` binary is found on `PATH`, download a matching
|
|
35
|
-
* prebuilt release into a local cache and use that instead.
|
|
36
|
-
* `false` to only ever use a `PATH` install, or pass an object to pick
|
|
37
|
-
* which version gets downloaded.
|
|
107
|
+
* prebuilt release into a local cache and use that instead.
|
|
38
108
|
* @default true
|
|
39
109
|
*/
|
|
40
110
|
autoInstall?: boolean | AutoInstallOptions;
|
package/dist/index.js
CHANGED
|
@@ -1,52 +1,111 @@
|
|
|
1
|
+
import { createComponent, renderComponent, renderTemplate, unescapeHTML, } from "astro/runtime/server/index.js";
|
|
1
2
|
import emitAssetIntegration from "astro-emit-asset";
|
|
2
|
-
import { resolveAutoInstallOption, resolveLilypondBinary, } from "./binary/index.js";
|
|
3
3
|
import { rehypePlugin, remarkPlugin, satteriPlugin, } from "./plugins/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { render as renderScore } from "./render.js";
|
|
5
|
+
import { getRenderState, resolveAndSetRenderState } from "./renderState.js";
|
|
6
|
+
import { altTextFor, emitLilypondAsset, emitLilypondPdfAsset, includePathsFor, lyTypeDeclarationsFor, parseLyHeaderFields, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, toLilypondMetadata, } from "./utils/index.js";
|
|
6
7
|
export const LY_EXTENSIONS = [".ly", ".lilypond", ".ily"];
|
|
7
|
-
function
|
|
8
|
-
return {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
function createScoreComponent(content) {
|
|
9
|
+
return createComponent((_result, props) => {
|
|
10
|
+
const alt = props.alt ?? content.alt ?? "";
|
|
11
|
+
const html = renderedHtml(content.pages, alt, {
|
|
12
|
+
class: props.class,
|
|
13
|
+
style: props.style,
|
|
14
|
+
pageLimit: props.pageLimit,
|
|
15
|
+
});
|
|
16
|
+
return renderTemplate `${unescapeHTML(html)}`;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Renders a `LilypondScore` (from a `.ly`/`.ily`/`.lilypond` import,
|
|
21
|
+
* or a `lilypondLoader()` entry) to a renderable `<Score />` component.
|
|
22
|
+
*/
|
|
23
|
+
export async function render(score, options = {}) {
|
|
24
|
+
const state = getRenderState();
|
|
25
|
+
const { resolution, cropScale } = resolveDefaults(state.defaults);
|
|
26
|
+
const format = options.format ?? "svg";
|
|
27
|
+
const crop = options.crop ?? false;
|
|
28
|
+
const [{ Score, pageCount }, pdf] = await Promise.all([
|
|
29
|
+
(async () => {
|
|
26
30
|
const pages = await emitLilypondAsset({
|
|
27
|
-
title,
|
|
31
|
+
title: score.assetTitle,
|
|
28
32
|
format,
|
|
29
|
-
source:
|
|
33
|
+
source: score.source,
|
|
30
34
|
resolution,
|
|
31
35
|
crop,
|
|
32
36
|
sizeScale: crop ? cropScale : 1,
|
|
33
|
-
binaryPath:
|
|
34
|
-
render: () =>
|
|
37
|
+
binaryPath: state.binaryPath,
|
|
38
|
+
render: () => renderScore(score.source, {
|
|
35
39
|
format,
|
|
36
40
|
crop,
|
|
37
|
-
defaults:
|
|
38
|
-
timeout:
|
|
39
|
-
binaryPath:
|
|
40
|
-
includePaths,
|
|
41
|
-
sourceName,
|
|
41
|
+
defaults: state.defaults,
|
|
42
|
+
timeout: state.timeout,
|
|
43
|
+
binaryPath: state.binaryPath,
|
|
44
|
+
includePaths: score.includePaths,
|
|
45
|
+
sourceName: score.sourceName,
|
|
42
46
|
}),
|
|
43
47
|
});
|
|
44
|
-
|
|
45
|
-
pages,
|
|
48
|
+
return {
|
|
49
|
+
Score: createScoreComponent({ pages, alt: score.alt }),
|
|
50
|
+
pageCount: pages.length,
|
|
51
|
+
};
|
|
52
|
+
})(),
|
|
53
|
+
options.pdf
|
|
54
|
+
? emitLilypondPdfAsset({
|
|
55
|
+
title: score.assetTitle,
|
|
56
|
+
source: score.source,
|
|
57
|
+
binaryPath: state.binaryPath,
|
|
58
|
+
render: () => renderScore(score.source, {
|
|
59
|
+
format: "pdf",
|
|
60
|
+
crop: false,
|
|
61
|
+
defaults: state.defaults,
|
|
62
|
+
timeout: state.timeout,
|
|
63
|
+
binaryPath: state.binaryPath,
|
|
64
|
+
includePaths: score.includePaths,
|
|
65
|
+
sourceName: score.sourceName,
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
: Promise.resolve(undefined),
|
|
69
|
+
]);
|
|
70
|
+
return { Score, pageCount, pdf, meta: score.meta, raw: score.source };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Renders a `LilypondScore` directly, for the common case where none of
|
|
74
|
+
* `render()`'s `pageCount`, `meta`, `raw`, or `pdf` are needed. Use
|
|
75
|
+
* `render()` instead when you need those.
|
|
76
|
+
*/
|
|
77
|
+
export const Score = createComponent(async (result, props) => {
|
|
78
|
+
const { content, format, crop, ...imageProps } = props;
|
|
79
|
+
const { Score: ContentScore } = await render(content, { format, crop });
|
|
80
|
+
return renderTemplate `${renderComponent(result, "Score", ContentScore, imageProps)}`;
|
|
81
|
+
});
|
|
82
|
+
export async function renderAll(scores, options = {}) {
|
|
83
|
+
return Promise.all(scores.map((score) => render(score, options)));
|
|
84
|
+
}
|
|
85
|
+
function lyFilePlugin(options) {
|
|
86
|
+
return {
|
|
87
|
+
name: "vite-plugin-astro-lilypond-ly",
|
|
88
|
+
enforce: "pre",
|
|
89
|
+
async transform(source, id) {
|
|
90
|
+
if (!LY_EXTENSIONS.some((ext) => id.endsWith(ext)))
|
|
91
|
+
return;
|
|
92
|
+
const { version } = resolveDefaults(options.defaults);
|
|
93
|
+
const src = version ? prependVersion(source, version) : source;
|
|
94
|
+
const includePaths = includePathsFor(id);
|
|
95
|
+
const sourceName = sourceNameFor(id);
|
|
96
|
+
const assetTitle = titleFor(sourceName);
|
|
97
|
+
const meta = toLilypondMetadata(parseLyHeaderFields(source));
|
|
98
|
+
const alt = altTextFor(meta);
|
|
99
|
+
const score = {
|
|
100
|
+
source: src,
|
|
46
101
|
alt,
|
|
102
|
+
sourceName,
|
|
103
|
+
includePaths,
|
|
104
|
+
assetTitle,
|
|
105
|
+
meta,
|
|
47
106
|
};
|
|
48
107
|
return {
|
|
49
|
-
code: `export default ${JSON.stringify(
|
|
108
|
+
code: `export default ${JSON.stringify(score)}`,
|
|
50
109
|
};
|
|
51
110
|
},
|
|
52
111
|
};
|
|
@@ -56,10 +115,11 @@ export default function lilypond(options = {}) {
|
|
|
56
115
|
name: "astro-lilypond",
|
|
57
116
|
hooks: {
|
|
58
117
|
"astro:config:setup": async ({ config, updateConfig, logger }) => {
|
|
59
|
-
options.binaryPath = await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
118
|
+
options.binaryPath = await resolveAndSetRenderState({
|
|
119
|
+
autoInstall: options.autoInstall,
|
|
120
|
+
defaults: options.defaults,
|
|
121
|
+
timeout: options.timeout,
|
|
122
|
+
logger,
|
|
63
123
|
});
|
|
64
124
|
updateConfig({
|
|
65
125
|
integrations: [emitAssetIntegration()],
|
|
@@ -120,7 +180,7 @@ export default function lilypond(options = {}) {
|
|
|
120
180
|
"astro:config:done": ({ injectTypes }) => {
|
|
121
181
|
injectTypes({
|
|
122
182
|
filename: "ly-types.d.ts",
|
|
123
|
-
content: lyTypeDeclarationsFor(LY_EXTENSIONS
|
|
183
|
+
content: lyTypeDeclarationsFor(LY_EXTENSIONS),
|
|
124
184
|
});
|
|
125
185
|
},
|
|
126
186
|
},
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Loader } from "astro/loaders";
|
|
2
2
|
import { z } from "astro/zod";
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
5
|
-
import type {
|
|
6
|
-
import { type KnownLyHeaderFields } from "./utils/index.js";
|
|
3
|
+
import type { AutoInstallOptions } from "./binary/index.js";
|
|
4
|
+
import { type LilypondScore } from "./index.js";
|
|
5
|
+
import type { LilypondDefaults } from "./render.js";
|
|
7
6
|
export interface GenerateIdOptions {
|
|
8
7
|
/** The path to the entry file, relative to `base`. */
|
|
9
8
|
entry: string;
|
|
@@ -12,7 +11,7 @@ export interface GenerateIdOptions {
|
|
|
12
11
|
/** The entry's parsed `\header` fields (see `parseLyHeaderFields`). */
|
|
13
12
|
header: Record<string, string>;
|
|
14
13
|
}
|
|
15
|
-
export interface LilypondLoaderOptions
|
|
14
|
+
export interface LilypondLoaderOptions {
|
|
16
15
|
/**
|
|
17
16
|
* Glob pattern(s) matching score files, relative to `base`.
|
|
18
17
|
* @default "**\/*.{ly,ily,lilypond}"
|
|
@@ -28,6 +27,18 @@ export interface LilypondLoaderOptions extends PluginOptions {
|
|
|
28
27
|
* @default the file's path relative to `base`, POSIX-separated, with its extension stripped.
|
|
29
28
|
*/
|
|
30
29
|
generateId?: (options: GenerateIdOptions) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Defaults used when `render()` is later called on one of this
|
|
32
|
+
* collection's entries.
|
|
33
|
+
*/
|
|
34
|
+
defaults?: LilypondDefaults;
|
|
35
|
+
/**
|
|
36
|
+
* Milliseconds to wait for a single `lilypond` invocation before
|
|
37
|
+
* aborting it, when `render()` is later called on one of this
|
|
38
|
+
* collection's entries.
|
|
39
|
+
* @default 60000
|
|
40
|
+
*/
|
|
41
|
+
timeout?: number;
|
|
31
42
|
/**
|
|
32
43
|
* When no `lilypond` binary is found on `PATH`, download a matching
|
|
33
44
|
* prebuilt release into a local cache and use that instead. Set to
|
|
@@ -37,33 +48,33 @@ export interface LilypondLoaderOptions extends PluginOptions {
|
|
|
37
48
|
*/
|
|
38
49
|
autoInstall?: boolean | AutoInstallOptions;
|
|
39
50
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
/**
|
|
52
|
+
* A collection entry is itself a `LilypondScore` (its `meta` carries the
|
|
53
|
+
* file's `\header` fields) — pass `entry.data` directly to the exported
|
|
54
|
+
* `render()`, exactly as you would a plain `.ly` import.
|
|
55
|
+
*/
|
|
56
|
+
export type LilypondCollectionEntry = LilypondScore;
|
|
46
57
|
export declare const lilypondEntrySchema: z.ZodObject<{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
source: z.ZodString;
|
|
59
|
+
alt: z.ZodString;
|
|
60
|
+
sourceName: z.ZodOptional<z.ZodString>;
|
|
61
|
+
includePaths: z.ZodArray<z.ZodString>;
|
|
62
|
+
assetTitle: z.ZodString;
|
|
63
|
+
meta: z.ZodObject<{
|
|
64
|
+
arranger: z.ZodOptional<z.ZodString>;
|
|
65
|
+
composer: z.ZodOptional<z.ZodString>;
|
|
66
|
+
copyright: z.ZodOptional<z.ZodString>;
|
|
67
|
+
dedication: z.ZodOptional<z.ZodString>;
|
|
68
|
+
instrument: z.ZodOptional<z.ZodString>;
|
|
69
|
+
meter: z.ZodOptional<z.ZodString>;
|
|
70
|
+
opus: z.ZodOptional<z.ZodString>;
|
|
71
|
+
piece: z.ZodOptional<z.ZodString>;
|
|
72
|
+
poet: z.ZodOptional<z.ZodString>;
|
|
73
|
+
subsubtitle: z.ZodOptional<z.ZodString>;
|
|
74
|
+
subtitle: z.ZodOptional<z.ZodString>;
|
|
75
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
76
|
+
title: z.ZodOptional<z.ZodString>;
|
|
77
|
+
}, z.core.$catchall<z.ZodString>>;
|
|
67
78
|
}, z.core.$strip>;
|
|
68
79
|
/**
|
|
69
80
|
* Content Loader to turn a directory of `.ly` files into a content collection.
|
package/dist/loader.js
CHANGED
|
@@ -2,32 +2,24 @@ import { glob, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { z } from "astro/zod";
|
|
5
|
-
import { resolveAutoInstallOption, resolveLilypondBinary, } from "./binary/index.js";
|
|
6
5
|
import { LY_EXTENSIONS } from "./index.js";
|
|
7
|
-
import {
|
|
8
|
-
import { altTextFor,
|
|
6
|
+
import { resolveAndSetRenderState } from "./renderState.js";
|
|
7
|
+
import { altTextFor, includePathsFor, parseLyHeaderFields, prependVersion, resolveDefaults, STANDARD_HEADER_FIELDS, sourceNameFor, titleFor, toLilypondMetadata, } from "./utils/index.js";
|
|
9
8
|
const DEFAULT_PATTERN = `**/*.{${LY_EXTENSIONS.map((ext) => ext.slice(1)).join(",")}}`;
|
|
9
|
+
// Built from the same `STANDARD_HEADER_FIELDS` list `KnownLyHeaderFields` is
|
|
10
|
+
// derived from, so the two can't drift. `.catchall()` lets any other header
|
|
11
|
+
// field (e.g. `mutopiacomposer`) sit alongside the standard ones, typed as
|
|
12
|
+
// `string` — matching `LilypondMetadata`'s index signature.
|
|
13
|
+
const lilypondMetadataSchema = z
|
|
14
|
+
.object(Object.fromEntries(STANDARD_HEADER_FIELDS.map((field) => [field, z.string().optional()])))
|
|
15
|
+
.catchall(z.string());
|
|
10
16
|
export const lilypondEntrySchema = z.object({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
dedication: z.string().optional(),
|
|
18
|
-
title: z.string().optional(),
|
|
19
|
-
subtitle: z.string().optional(),
|
|
20
|
-
subsubtitle: z.string().optional(),
|
|
21
|
-
instrument: z.string().optional(),
|
|
22
|
-
poet: z.string().optional(),
|
|
23
|
-
composer: z.string().optional(),
|
|
24
|
-
meter: z.string().optional(),
|
|
25
|
-
arranger: z.string().optional(),
|
|
26
|
-
piece: z.string().optional(),
|
|
27
|
-
opus: z.string().optional(),
|
|
28
|
-
copyright: z.string().optional(),
|
|
29
|
-
tagline: z.string().optional(),
|
|
30
|
-
extra: z.record(z.string(), z.string()),
|
|
17
|
+
source: z.string(),
|
|
18
|
+
alt: z.string(),
|
|
19
|
+
sourceName: z.string().optional(),
|
|
20
|
+
includePaths: z.array(z.string()),
|
|
21
|
+
assetTitle: z.string(),
|
|
22
|
+
meta: lilypondMetadataSchema,
|
|
31
23
|
});
|
|
32
24
|
function stripLyExtension(entryPath) {
|
|
33
25
|
for (const ext of LY_EXTENSIONS) {
|
|
@@ -55,10 +47,8 @@ function posixRelative(from, to) {
|
|
|
55
47
|
* Content Loader to turn a directory of `.ly` files into a content collection.
|
|
56
48
|
*/
|
|
57
49
|
export function lilypondLoader(options) {
|
|
58
|
-
const { pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId,
|
|
50
|
+
const { pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId, defaults, timeout, autoInstall, } = options;
|
|
59
51
|
const resolved = resolveDefaults(defaults);
|
|
60
|
-
const crop = resolveCrop(resolved.crop, "component");
|
|
61
|
-
const resolvedFormat = format ?? defaultOptions.format;
|
|
62
52
|
return {
|
|
63
53
|
name: "astro-lilypond-loader",
|
|
64
54
|
schema: lilypondEntrySchema,
|
|
@@ -67,10 +57,14 @@ export function lilypondLoader(options) {
|
|
|
67
57
|
const rootDir = fileURLToPath(config.root);
|
|
68
58
|
const baseUrl = resolveBaseUrl(base, config.root);
|
|
69
59
|
const baseDir = fileURLToPath(baseUrl);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
// Populates the shared render() state, so this collection's entries
|
|
61
|
+
// can be rendered even when the `lilypond()` integration itself
|
|
62
|
+
// isn't registered.
|
|
63
|
+
await resolveAndSetRenderState({
|
|
64
|
+
autoInstall,
|
|
65
|
+
defaults,
|
|
66
|
+
timeout,
|
|
67
|
+
logger,
|
|
74
68
|
});
|
|
75
69
|
async function syncEntry(entry) {
|
|
76
70
|
const filePath = join(baseDir, entry);
|
|
@@ -88,34 +82,18 @@ export function lilypondLoader(options) {
|
|
|
88
82
|
const src = prependVersion(source, resolved.version);
|
|
89
83
|
const includePaths = includePathsFor(filePath);
|
|
90
84
|
const sourceName = sourceNameFor(filePath);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
format: resolvedFormat,
|
|
95
|
-
source: src,
|
|
96
|
-
resolution: resolved.resolution,
|
|
97
|
-
crop,
|
|
98
|
-
sizeScale: crop ? resolved.cropScale : 1,
|
|
99
|
-
binaryPath,
|
|
100
|
-
render: () => render(src, {
|
|
101
|
-
format: resolvedFormat,
|
|
102
|
-
crop,
|
|
103
|
-
defaults,
|
|
104
|
-
timeout,
|
|
105
|
-
binaryPath,
|
|
106
|
-
includePaths,
|
|
107
|
-
sourceName,
|
|
108
|
-
}),
|
|
109
|
-
});
|
|
85
|
+
const assetTitle = titleFor(sourceName);
|
|
86
|
+
const meta = toLilypondMetadata(headerFields);
|
|
87
|
+
const alt = altTextFor(meta);
|
|
110
88
|
const data = await parseData({
|
|
111
89
|
id,
|
|
112
90
|
data: {
|
|
113
|
-
|
|
114
|
-
alt
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
91
|
+
source: src,
|
|
92
|
+
alt,
|
|
93
|
+
sourceName,
|
|
94
|
+
includePaths,
|
|
95
|
+
assetTitle,
|
|
96
|
+
meta,
|
|
119
97
|
},
|
|
120
98
|
filePath,
|
|
121
99
|
});
|
package/dist/plugins/rehype.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
-
import { defaultOptions, render
|
|
2
|
+
import { defaultOptions, render } from "../render.js";
|
|
3
3
|
import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
|
|
4
4
|
// Typed loosely so it's assignable to both RehypePlugin and the unified
|
|
5
5
|
// Plugin generic regardless of which @types/hast version the host project pins.
|
|
@@ -30,22 +30,21 @@ export function rehypePlugin(options) {
|
|
|
30
30
|
.filter((c) => c.type === "text")
|
|
31
31
|
.map((c) => c.value)
|
|
32
32
|
.join("");
|
|
33
|
-
const { version, resolution,
|
|
33
|
+
const { version, resolution, cropScale } = resolveDefaults(options.defaults);
|
|
34
34
|
const source = version ? prependVersion(raw, version) : raw;
|
|
35
35
|
const format = options.format ?? defaultOptions.format;
|
|
36
|
-
const crop = resolveCrop(cropSetting, "markdown");
|
|
37
36
|
const alt = altTextForBlock(codeNode.data?.meta, raw);
|
|
38
37
|
const promise = emitLilypondAsset({
|
|
39
38
|
title,
|
|
40
39
|
format,
|
|
41
40
|
source,
|
|
42
41
|
resolution,
|
|
43
|
-
crop,
|
|
44
|
-
sizeScale:
|
|
42
|
+
crop: true,
|
|
43
|
+
sizeScale: cropScale,
|
|
45
44
|
binaryPath: options.binaryPath,
|
|
46
45
|
render: () => render(source, {
|
|
47
46
|
format,
|
|
48
|
-
crop,
|
|
47
|
+
crop: true,
|
|
49
48
|
defaults: options.defaults,
|
|
50
49
|
timeout: options.timeout,
|
|
51
50
|
binaryPath: options.binaryPath,
|
package/dist/plugins/remark.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
-
import { defaultOptions, render
|
|
2
|
+
import { defaultOptions, render } from "../render.js";
|
|
3
3
|
import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
|
|
4
4
|
export const remarkPlugin = (options) => {
|
|
5
5
|
return async (tree, file) => {
|
|
@@ -10,22 +10,21 @@ export const remarkPlugin = (options) => {
|
|
|
10
10
|
visit(tree, "code", (node, index, parent) => {
|
|
11
11
|
if (!isLilypondLang(node.lang) || index === undefined || !parent)
|
|
12
12
|
return;
|
|
13
|
-
const { version, resolution,
|
|
13
|
+
const { version, resolution, cropScale } = resolveDefaults(options.defaults);
|
|
14
14
|
const source = version ? prependVersion(node.value, version) : node.value;
|
|
15
15
|
const format = options.format ?? defaultOptions.format;
|
|
16
|
-
const crop = resolveCrop(cropSetting, "markdown");
|
|
17
16
|
const alt = altTextForBlock(node.meta, node.value);
|
|
18
17
|
const promise = emitLilypondAsset({
|
|
19
18
|
title,
|
|
20
19
|
format,
|
|
21
20
|
source,
|
|
22
21
|
resolution,
|
|
23
|
-
crop,
|
|
24
|
-
sizeScale:
|
|
22
|
+
crop: true,
|
|
23
|
+
sizeScale: cropScale,
|
|
25
24
|
binaryPath: options.binaryPath,
|
|
26
25
|
render: () => render(source, {
|
|
27
26
|
format,
|
|
28
|
-
crop,
|
|
27
|
+
crop: true,
|
|
29
28
|
defaults: options.defaults,
|
|
30
29
|
timeout: options.timeout,
|
|
31
30
|
binaryPath: options.binaryPath,
|
package/dist/plugins/satteri.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultOptions, render
|
|
1
|
+
import { defaultOptions, render } from "../render.js";
|
|
2
2
|
import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
|
|
3
3
|
export function satteriPlugin(options) {
|
|
4
4
|
return {
|
|
@@ -9,25 +9,24 @@ export function satteriPlugin(options) {
|
|
|
9
9
|
async code(node, ctx) {
|
|
10
10
|
if (!isLilypondLang(node.lang))
|
|
11
11
|
return undefined;
|
|
12
|
-
const { version, resolution,
|
|
12
|
+
const { version, resolution, cropScale } = resolveDefaults(options.defaults);
|
|
13
13
|
const source = version ? prependVersion(node.value, version) : node.value;
|
|
14
14
|
const format = options.format ?? defaultOptions.format;
|
|
15
15
|
const includePaths = includePathsFor(ctx.fileURL);
|
|
16
16
|
const sourceName = sourceNameFor(ctx.fileURL);
|
|
17
17
|
const title = titleFor(sourceName);
|
|
18
|
-
const crop = resolveCrop(cropSetting, "markdown");
|
|
19
18
|
const alt = altTextForBlock(node.meta, node.value);
|
|
20
19
|
const pages = await emitLilypondAsset({
|
|
21
20
|
title,
|
|
22
21
|
format,
|
|
23
22
|
source,
|
|
24
23
|
resolution,
|
|
25
|
-
crop,
|
|
26
|
-
sizeScale:
|
|
24
|
+
crop: true,
|
|
25
|
+
sizeScale: cropScale,
|
|
27
26
|
binaryPath: options.binaryPath,
|
|
28
27
|
render: () => render(source, {
|
|
29
28
|
format,
|
|
30
|
-
crop,
|
|
29
|
+
crop: true,
|
|
31
30
|
defaults: options.defaults,
|
|
32
31
|
timeout: options.timeout,
|
|
33
32
|
binaryPath: options.binaryPath,
|
package/dist/readOutputFile.js
CHANGED
|
@@ -20,14 +20,13 @@ function byPageNumber(a, b) {
|
|
|
20
20
|
return Number(a[1]) - Number(b[1]);
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Multi-page naming convention per format: SVG
|
|
24
|
-
*
|
|
25
|
-
* Listed explicitly so adding a format means stating its convention here,
|
|
26
|
-
* not guessing among known patterns.
|
|
23
|
+
* Multi-page naming convention per format: SVG uses `<base>-1.<ext>`; PNG
|
|
24
|
+
* uses `<base>-page1.<ext>`. `pdf` always renders to one `<base>.pdf`.
|
|
27
25
|
*/
|
|
28
26
|
const PAGE_NUMBER_INFIX = {
|
|
29
27
|
svg: "-",
|
|
30
28
|
png: "-page",
|
|
29
|
+
pdf: "-",
|
|
31
30
|
};
|
|
32
31
|
export async function readOutputFile(base, format, crop) {
|
|
33
32
|
// --define-default crop writes <base>.cropped.<format> alongside <base>.<format>
|
|
@@ -39,9 +38,7 @@ export async function readOutputFile(base, format, crop) {
|
|
|
39
38
|
throw new Error(`expected cropped output at ${base}.cropped.${format} but it was not found: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
|
-
//
|
|
43
|
-
// per `PAGE_NUMBER_INFIX`. List the directory once so the full page
|
|
44
|
-
// count is known up front, rather than probing paths one at a time.
|
|
41
|
+
// Single page → <base>.<format>; multi-page → numbered per PAGE_NUMBER_INFIX.
|
|
45
42
|
const dir = dirname(base);
|
|
46
43
|
const prefix = basename(base);
|
|
47
44
|
const entries = await readdir(dir);
|
package/dist/render.d.ts
CHANGED
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
import type { LilypondVersion } from "./types/lilypondVersion.js";
|
|
2
|
-
export declare const FORMATS: readonly ["png", "svg"];
|
|
2
|
+
export declare const FORMATS: readonly ["png", "svg", "pdf"];
|
|
3
3
|
export type Format = (typeof FORMATS)[number];
|
|
4
|
-
/**
|
|
5
|
-
* Controls whether rendered output is cropped tightly to the content
|
|
6
|
-
* bounding box, or left as full, potentially multi-page, output.
|
|
7
|
-
*
|
|
8
|
-
* - `true` — crop everywhere: Markdown fences and `<LilyPond>` `.ly` imports.
|
|
9
|
-
* - `false` — never crop by default (a `.ly` import can still opt in with
|
|
10
|
-
* `?crop`).
|
|
11
|
-
* - `"markdown-only"` — crop Markdown fences, but leave `<LilyPond>` `.ly`
|
|
12
|
-
* imports uncropped unless the import opts in with `?crop`.
|
|
13
|
-
*/
|
|
14
|
-
export type CropSetting = boolean | "markdown-only";
|
|
15
|
-
/** Which kind of consumer is resolving a `CropSetting` — see `resolveCrop`. */
|
|
16
|
-
export type CropContext = "markdown" | "component";
|
|
17
|
-
/**
|
|
18
|
-
* Resolves the three-way `CropSetting` into the plain boolean a given
|
|
19
|
-
* consumer needs, per the semantics documented on `CropSetting`.
|
|
20
|
-
*/
|
|
21
|
-
export declare function resolveCrop(cropSetting: CropSetting, context: CropContext): boolean;
|
|
22
4
|
/**
|
|
23
5
|
* Defaults passed to each score for rendering.
|
|
24
|
-
* Individual instances can override.
|
|
25
6
|
*/
|
|
26
7
|
export interface LilypondDefaults {
|
|
27
8
|
/**
|
|
@@ -35,12 +16,6 @@ export interface LilypondDefaults {
|
|
|
35
16
|
* @default 144
|
|
36
17
|
*/
|
|
37
18
|
resolution?: number;
|
|
38
|
-
/**
|
|
39
|
-
* Crop the output tightly to the content bounding box, producing one
|
|
40
|
-
* continuous image instead of paginated output.
|
|
41
|
-
* @default "markdown-only"
|
|
42
|
-
*/
|
|
43
|
-
crop?: CropSetting;
|
|
44
19
|
/**
|
|
45
20
|
* Multiplies the `width`/`height` on a cropped score's `<img>` tag.
|
|
46
21
|
* Helps compensate for LilyPond's internal size units (points/mm)
|
|
@@ -53,10 +28,10 @@ export interface LilypondDefaults {
|
|
|
53
28
|
}
|
|
54
29
|
/**
|
|
55
30
|
* The subset of `LilypondDefaults` that `render()` itself reads. `version`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
31
|
+
* is resolved by the caller before reaching `render()`, by prepending it to
|
|
32
|
+
* source text.
|
|
58
33
|
*/
|
|
59
|
-
export type RenderDefaults = Omit<LilypondDefaults, "version"
|
|
34
|
+
export type RenderDefaults = Omit<LilypondDefaults, "version">;
|
|
60
35
|
export interface RenderOptions {
|
|
61
36
|
/**
|
|
62
37
|
* Output format.
|
package/dist/render.js
CHANGED
|
@@ -4,14 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { execLilyPond } from "./execLilyPond.js";
|
|
5
5
|
import { readOutputFile, safeInputFileName } from "./readOutputFile.js";
|
|
6
6
|
import { resolveDefaults } from "./utils/resolveDefaults.js";
|
|
7
|
-
export const FORMATS = ["png", "svg"];
|
|
8
|
-
/**
|
|
9
|
-
* Resolves the three-way `CropSetting` into the plain boolean a given
|
|
10
|
-
* consumer needs, per the semantics documented on `CropSetting`.
|
|
11
|
-
*/
|
|
12
|
-
export function resolveCrop(cropSetting, context) {
|
|
13
|
-
return context === "markdown" ? cropSetting !== false : cropSetting === true;
|
|
14
|
-
}
|
|
7
|
+
export const FORMATS = ["png", "svg", "pdf"];
|
|
15
8
|
export const defaultOptions = {
|
|
16
9
|
format: "svg",
|
|
17
10
|
crop: true,
|
|
@@ -20,7 +13,6 @@ export const defaultOptions = {
|
|
|
20
13
|
defaults: {
|
|
21
14
|
version: "2.26.0",
|
|
22
15
|
resolution: 144,
|
|
23
|
-
crop: "markdown-only",
|
|
24
16
|
cropScale: 1.5,
|
|
25
17
|
},
|
|
26
18
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AutoInstallOptions } from "./binary/index.js";
|
|
2
|
+
import type { LilypondDefaults } from "./render.js";
|
|
3
|
+
export interface RenderState {
|
|
4
|
+
binaryPath: string;
|
|
5
|
+
defaults: LilypondDefaults | undefined;
|
|
6
|
+
timeout: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Called by the `lilypond()` integration's `astro:config:setup` hook. */
|
|
9
|
+
export declare function setRenderState(next: RenderState): void;
|
|
10
|
+
/** Read by the public `render()`, called from user code rather than the integration's own closures. */
|
|
11
|
+
export declare function getRenderState(): RenderState;
|
|
12
|
+
/** Test-only: resets the shared singleton between test cases. */
|
|
13
|
+
export declare function resetRenderStateForTests(): void;
|
|
14
|
+
export interface ResolveAndSetRenderStateOptions {
|
|
15
|
+
autoInstall?: boolean | AutoInstallOptions;
|
|
16
|
+
defaults?: LilypondDefaults;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
logger: {
|
|
19
|
+
info: (message: string) => void;
|
|
20
|
+
warn: (message: string) => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared by the `lilypond()` integration's `astro:config:setup` hook and
|
|
25
|
+
* `lilypondLoader()`'s `load()`: resolves the `lilypond` binary and
|
|
26
|
+
* populates the render state from it. Returns the resolved binary path for
|
|
27
|
+
* callers (e.g. Markdown plugins) that also need it directly.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveAndSetRenderState(options: ResolveAndSetRenderStateOptions): Promise<string>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveAutoInstallOption, resolveLilypondBinary, } from "./binary/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keyed on `globalThis` rather than module-scope state — some Astro adapters
|
|
4
|
+
* bundle the render step as a separate chunk with its own copy of this
|
|
5
|
+
* module, so a plain module `let` wouldn't survive from
|
|
6
|
+
* `astro:config:setup` into an actual render.
|
|
7
|
+
*/
|
|
8
|
+
const GLOBAL_KEY = "astro-lilypond:renderState";
|
|
9
|
+
function globalStore() {
|
|
10
|
+
return globalThis;
|
|
11
|
+
}
|
|
12
|
+
/** Called by the `lilypond()` integration's `astro:config:setup` hook. */
|
|
13
|
+
export function setRenderState(next) {
|
|
14
|
+
globalStore()[GLOBAL_KEY] = next;
|
|
15
|
+
}
|
|
16
|
+
/** Read by the public `render()`, called from user code rather than the integration's own closures. */
|
|
17
|
+
export function getRenderState() {
|
|
18
|
+
const state = globalStore()[GLOBAL_KEY];
|
|
19
|
+
if (!state) {
|
|
20
|
+
throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
|
|
21
|
+
}
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
24
|
+
/** Test-only: resets the shared singleton between test cases. */
|
|
25
|
+
export function resetRenderStateForTests() {
|
|
26
|
+
globalStore()[GLOBAL_KEY] = undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Shared by the `lilypond()` integration's `astro:config:setup` hook and
|
|
30
|
+
* `lilypondLoader()`'s `load()`: resolves the `lilypond` binary and
|
|
31
|
+
* populates the render state from it. Returns the resolved binary path for
|
|
32
|
+
* callers (e.g. Markdown plugins) that also need it directly.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveAndSetRenderState(options) {
|
|
35
|
+
const binaryPath = await resolveLilypondBinary({
|
|
36
|
+
...resolveAutoInstallOption(options.autoInstall),
|
|
37
|
+
log: (message) => options.logger.info(message),
|
|
38
|
+
warn: (message) => options.logger.warn(message),
|
|
39
|
+
});
|
|
40
|
+
setRenderState({
|
|
41
|
+
binaryPath,
|
|
42
|
+
defaults: options.defaults,
|
|
43
|
+
timeout: options.timeout,
|
|
44
|
+
});
|
|
45
|
+
return binaryPath;
|
|
46
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { vi } from "vitest";
|
|
2
|
-
import type { emitLilypondAsset } from "
|
|
2
|
+
import type { emitLilypondAsset } from "../emitLilypondAsset.js";
|
|
3
3
|
type EmitLilypondAssetMock = ReturnType<typeof vi.mocked<typeof emitLilypondAsset>>;
|
|
4
4
|
export declare function fakeEmitLilypondAsset(mock: EmitLilypondAssetMock, urlBase?: string): void;
|
|
5
5
|
export declare function fakeEmitLilypondAssetPropagatingRenderErrors(mock: EmitLilypondAssetMock): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { vi } from "vitest";
|
|
2
|
+
import type { emitLilypondPdfAsset } from "../emitLilypondPdfAsset.js";
|
|
3
|
+
type EmitLilypondPdfAssetMock = ReturnType<typeof vi.mocked<typeof emitLilypondPdfAsset>>;
|
|
4
|
+
export declare function fakeEmitLilypondPdfAsset(mock: EmitLilypondPdfAssetMock, urlBase?: string): void;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LilypondPdfResult } from "../index.js";
|
|
2
|
+
export interface EmitLilypondPdfAssetOptions {
|
|
3
|
+
title: string;
|
|
4
|
+
source: string;
|
|
5
|
+
binaryPath: string | undefined;
|
|
6
|
+
render: () => Promise<Buffer[]>;
|
|
7
|
+
}
|
|
8
|
+
export declare function emitLilypondPdfAsset(options: EmitLilypondPdfAssetOptions): Promise<LilypondPdfResult>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { emitAsset } from "astro-emit-asset/emit";
|
|
2
|
+
export async function emitLilypondPdfAsset(options) {
|
|
3
|
+
if (!options.binaryPath) {
|
|
4
|
+
throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
|
|
5
|
+
}
|
|
6
|
+
const { title, source, render } = options;
|
|
7
|
+
const asset = await emitAsset(`${title}.[hash].pdf`, [source, "pdf"], async () => {
|
|
8
|
+
const [data] = await render();
|
|
9
|
+
return { data };
|
|
10
|
+
});
|
|
11
|
+
return { src: asset.src };
|
|
12
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { altTextFor } from "./altTextFor.js";
|
|
2
2
|
export { altTextForBlock } from "./altTextForBlock.js";
|
|
3
3
|
export { type EmitLilypondAssetOptions, emitLilypondAsset, } from "./emitLilypondAsset.js";
|
|
4
|
+
export { type EmitLilypondPdfAssetOptions, emitLilypondPdfAsset, } from "./emitLilypondPdfAsset.js";
|
|
4
5
|
export { escapeHtmlAttribute } from "./escapeHtmlAttribute.js";
|
|
5
6
|
export type { ImageDimensions } from "./imageDimensions.js";
|
|
6
7
|
export { imageDimensionsFor } from "./imageDimensions.js";
|
|
7
8
|
export { includePathsFor } from "./includePathsFor.js";
|
|
8
9
|
export { isLilypondLang } from "./isLilypondLang.js";
|
|
9
|
-
export {
|
|
10
|
+
export type { KnownLyHeaderFields, LilypondMetadata, } from "./lilypondMetadata.js";
|
|
11
|
+
export { STANDARD_HEADER_FIELDS, toLilypondMetadata, } from "./lilypondMetadata.js";
|
|
10
12
|
export { lyTypeDeclarationsFor } from "./lyTypeDeclarationsFor.js";
|
|
11
13
|
export { parseFenceMeta } from "./parseFenceMeta.js";
|
|
12
14
|
export type { LyHeaderFields } from "./parseLyHeader.js";
|
|
@@ -15,7 +17,5 @@ export { prependVersion } from "./prependVersion.js";
|
|
|
15
17
|
export { renderedHtml } from "./renderedHtml.js";
|
|
16
18
|
export { resolveDefaults } from "./resolveDefaults.js";
|
|
17
19
|
export { sourceNameFor } from "./sourceNameFor.js";
|
|
18
|
-
export type { KnownLyHeaderFields } from "./splitHeaderFields.js";
|
|
19
|
-
export { STANDARD_HEADER_FIELDS, splitHeaderFields, } from "./splitHeaderFields.js";
|
|
20
20
|
export { titleFor } from "./titleFor.js";
|
|
21
21
|
export { unescapeQuoted } from "./unescapeQuoted.js";
|
package/dist/utils/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { altTextFor } from "./altTextFor.js";
|
|
2
2
|
export { altTextForBlock } from "./altTextForBlock.js";
|
|
3
3
|
export { emitLilypondAsset, } from "./emitLilypondAsset.js";
|
|
4
|
+
export { emitLilypondPdfAsset, } from "./emitLilypondPdfAsset.js";
|
|
4
5
|
export { escapeHtmlAttribute } from "./escapeHtmlAttribute.js";
|
|
5
6
|
export { imageDimensionsFor } from "./imageDimensions.js";
|
|
6
7
|
export { includePathsFor } from "./includePathsFor.js";
|
|
7
8
|
export { isLilypondLang } from "./isLilypondLang.js";
|
|
8
|
-
export {
|
|
9
|
+
export { STANDARD_HEADER_FIELDS, toLilypondMetadata, } from "./lilypondMetadata.js";
|
|
9
10
|
export { lyTypeDeclarationsFor } from "./lyTypeDeclarationsFor.js";
|
|
10
11
|
export { parseFenceMeta } from "./parseFenceMeta.js";
|
|
11
12
|
export { extractMarkupText, parseLyHeader, parseLyHeaderFields, } from "./parseLyHeader.js";
|
|
@@ -13,6 +14,5 @@ export { prependVersion } from "./prependVersion.js";
|
|
|
13
14
|
export { renderedHtml } from "./renderedHtml.js";
|
|
14
15
|
export { resolveDefaults } from "./resolveDefaults.js";
|
|
15
16
|
export { sourceNameFor } from "./sourceNameFor.js";
|
|
16
|
-
export { STANDARD_HEADER_FIELDS, splitHeaderFields, } from "./splitHeaderFields.js";
|
|
17
17
|
export { titleFor } from "./titleFor.js";
|
|
18
18
|
export { unescapeQuoted } from "./unescapeQuoted.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const STANDARD_HEADER_FIELDS: readonly ["arranger", "composer", "copyright", "dedication", "instrument", "meter", "opus", "piece", "poet", "subsubtitle", "subtitle", "tagline", "title"];
|
|
2
|
+
export type KnownLyHeaderFields = {
|
|
3
|
+
[K in (typeof STANDARD_HEADER_FIELDS)[number]]?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Header metadata extracted from a score's `\header` block(s). */
|
|
6
|
+
export interface LilypondMetadata extends KnownLyHeaderFields {
|
|
7
|
+
[field: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Tags a bag of parsed `\header` fields (see `parseLyHeaderFields`) as `LilypondMetadata`. */
|
|
10
|
+
export declare function toLilypondMetadata(fields: Record<string, string>): LilypondMetadata;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const STANDARD_HEADER_FIELDS = [
|
|
2
|
+
"arranger",
|
|
3
|
+
"composer",
|
|
4
|
+
"copyright",
|
|
5
|
+
"dedication",
|
|
6
|
+
"instrument",
|
|
7
|
+
"meter",
|
|
8
|
+
"opus",
|
|
9
|
+
"piece",
|
|
10
|
+
"poet",
|
|
11
|
+
"subsubtitle",
|
|
12
|
+
"subtitle",
|
|
13
|
+
"tagline",
|
|
14
|
+
"title",
|
|
15
|
+
];
|
|
16
|
+
/** Tags a bag of parsed `\header` fields (see `parseLyHeaderFields`) as `LilypondMetadata`. */
|
|
17
|
+
export function toLilypondMetadata(fields) {
|
|
18
|
+
return fields;
|
|
19
|
+
}
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* so `import content from "./score.ly"` (and its `?crop`/`?nocrop` query
|
|
4
|
-
* variants) type-check as a default-exported `LilypondContent`.
|
|
5
|
-
*/
|
|
6
|
-
export declare function lyTypeDeclarationsFor(extensions: readonly string[], queryParams: readonly string[]): string;
|
|
1
|
+
/** Build the ambient module declarations injected for `.ly`-family imports. */
|
|
2
|
+
export declare function lyTypeDeclarationsFor(extensions: readonly string[]): string;
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* so `import content from "./score.ly"` (and its `?crop`/`?nocrop` query
|
|
4
|
-
* variants) type-check as a default-exported `LilypondContent`.
|
|
5
|
-
*/
|
|
6
|
-
export function lyTypeDeclarationsFor(extensions, queryParams) {
|
|
7
|
-
const suffixes = ["", ...queryParams.map((param) => `?${param}`)];
|
|
1
|
+
/** Build the ambient module declarations injected for `.ly`-family imports. */
|
|
2
|
+
export function lyTypeDeclarationsFor(extensions) {
|
|
8
3
|
return extensions
|
|
9
|
-
.
|
|
4
|
+
.map((ext) => `declare module "*${ext}" {\n const score: import("astro-lilypond").LilypondScore;\n export default score;\n}`)
|
|
10
5
|
.join("\n");
|
|
11
6
|
}
|
|
@@ -209,9 +209,8 @@ function parseFieldValue(body, start) {
|
|
|
209
209
|
i++;
|
|
210
210
|
if (body[i] !== "{")
|
|
211
211
|
return null;
|
|
212
|
+
// body is itself a properly-nested brace sequence (see allHeaderBodies), so this `{` always has a matching `}`.
|
|
212
213
|
const end = matchBalancedBraces(body, i);
|
|
213
|
-
if (end === -1)
|
|
214
|
-
return null;
|
|
215
214
|
const value = extractMarkupText(body.slice(i + 1, end));
|
|
216
215
|
return value ? { value, end: end + 1 } : null;
|
|
217
216
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { LilypondPage } from "../index.js";
|
|
2
|
-
export
|
|
2
|
+
export interface RenderedHtmlOptions {
|
|
3
|
+
/** Class applied to the outer `<img>` or `<ol>` tag. */
|
|
4
|
+
class?: string;
|
|
5
|
+
/** Inline styles applied to the outer `<img>` or `<ol>`. */
|
|
6
|
+
style?: string;
|
|
7
|
+
/** Render only the first `n` pages. */
|
|
8
|
+
pageLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function renderedHtml(pages: LilypondPage[], alt: string, options?: RenderedHtmlOptions): string;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
function imgTag(page,
|
|
3
|
-
|
|
4
|
-
? ` width="${page.width}" height="${page.height}"`
|
|
5
|
-
: "";
|
|
6
|
-
return `<img data-lilypond-image src="${page.src}"${size} alt="${escapedAlt}">`;
|
|
1
|
+
import { addAttribute } from "astro/runtime/server/index.js";
|
|
2
|
+
function imgTag(page, alt) {
|
|
3
|
+
return `<img data-lilypond-image${addAttribute(page.src, "src")}${addAttribute(page.width, "width")}${addAttribute(page.height, "height")}${addAttribute(alt, "alt")}>`;
|
|
7
4
|
}
|
|
8
|
-
export function renderedHtml(pages, alt) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export function renderedHtml(pages, alt, options = {}) {
|
|
6
|
+
const { class: className, style, pageLimit } = options;
|
|
7
|
+
const limitedPages = pageLimit === undefined ? pages : pages.slice(0, pageLimit);
|
|
8
|
+
if (limitedPages.length === 0)
|
|
9
|
+
return "";
|
|
10
|
+
const classAttr = addAttribute(className, "class");
|
|
11
|
+
const styleAttr = addAttribute(style, "style");
|
|
12
|
+
if (limitedPages.length === 1) {
|
|
13
|
+
const page = limitedPages[0];
|
|
14
|
+
return `<img data-lilypond-image${classAttr}${addAttribute(page.src, "src")}${addAttribute(page.width, "width")}${addAttribute(page.height, "height")}${addAttribute(alt, "alt")}${styleAttr}>`;
|
|
12
15
|
}
|
|
13
|
-
return `<ol data-lilypond-group>${
|
|
14
|
-
.map((page) => `<li>${imgTag(page,
|
|
16
|
+
return `<ol data-lilypond-group${classAttr}${styleAttr}>${limitedPages
|
|
17
|
+
.map((page) => `<li>${imgTag(page, alt)}</li>`)
|
|
15
18
|
.join("")}</ol>`;
|
|
16
19
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { defaultOptions } from "../render.js";
|
|
2
2
|
/** Fills in any `defaults` fields left unset with `render.ts`'s own defaults. */
|
|
3
3
|
export function resolveDefaults(defaults) {
|
|
4
|
-
const { version, resolution,
|
|
4
|
+
const { version, resolution, cropScale } = defaultOptions.defaults;
|
|
5
5
|
return {
|
|
6
6
|
version: defaults?.version ?? version,
|
|
7
7
|
resolution: defaults?.resolution ?? resolution,
|
|
8
|
-
crop: defaults?.crop ?? crop,
|
|
9
8
|
cropScale: defaults?.cropScale ?? cropScale,
|
|
10
9
|
};
|
|
11
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lilypond",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "An Astro integration for rendering Lilypond markup to music notation.",
|
|
5
5
|
"homepage": "https://lilypond.ky.fyi",
|
|
6
6
|
"author": "Ky Decker",
|
|
@@ -17,15 +17,13 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
19
|
},
|
|
20
|
-
"./component": "./components/LilyPond.astro",
|
|
21
20
|
"./loader": {
|
|
22
21
|
"types": "./dist/loader.d.ts",
|
|
23
22
|
"import": "./dist/loader.js"
|
|
24
23
|
}
|
|
25
24
|
},
|
|
26
25
|
"files": [
|
|
27
|
-
"dist"
|
|
28
|
-
"components/*.astro"
|
|
26
|
+
"dist"
|
|
29
27
|
],
|
|
30
28
|
"keywords": [
|
|
31
29
|
"astro",
|
|
@@ -56,6 +54,7 @@
|
|
|
56
54
|
"@astrojs/markdown-satteri": "^0.3.4",
|
|
57
55
|
"@types/mdast": "^4.0.4",
|
|
58
56
|
"@types/node": "^26.1.1",
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
59
58
|
"astro": "^7.0.8",
|
|
60
59
|
"satteri": "^0.9.5",
|
|
61
60
|
"typescript": "^7.0.2",
|
|
@@ -66,6 +65,7 @@
|
|
|
66
65
|
"scripts": {
|
|
67
66
|
"build": "tsc --project tsconfig.build.json",
|
|
68
67
|
"test": "vitest --project unit",
|
|
68
|
+
"test:coverage": "vitest run --project unit --coverage",
|
|
69
69
|
"test:integration": "vitest run --project integration"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
content: {
|
|
4
|
-
pages: { src: string; width?: number; height?: number }[];
|
|
5
|
-
alt?: string;
|
|
6
|
-
};
|
|
7
|
-
pageLimit?: number;
|
|
8
|
-
class?: string | undefined;
|
|
9
|
-
style?: string | undefined;
|
|
10
|
-
alt?: string | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
content,
|
|
15
|
-
pageLimit,
|
|
16
|
-
class: className,
|
|
17
|
-
style,
|
|
18
|
-
alt: altProp,
|
|
19
|
-
} = Astro.props;
|
|
20
|
-
const { alt: contentAlt } = content;
|
|
21
|
-
const pages =
|
|
22
|
-
pageLimit === undefined ? content.pages : content.pages.slice(0, pageLimit);
|
|
23
|
-
const alt = altProp ?? contentAlt ?? "";
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
pages.length === 1 ? (
|
|
28
|
-
<img
|
|
29
|
-
data-lilypond-image
|
|
30
|
-
class={className}
|
|
31
|
-
src={pages[0].src}
|
|
32
|
-
width={pages[0].width}
|
|
33
|
-
height={pages[0].height}
|
|
34
|
-
alt={alt}
|
|
35
|
-
style={style}
|
|
36
|
-
/>
|
|
37
|
-
) : (
|
|
38
|
-
<ol data-lilypond-group class={className} style={style}>
|
|
39
|
-
{pages.map((page) => (
|
|
40
|
-
<li>
|
|
41
|
-
<img
|
|
42
|
-
data-lilypond-image
|
|
43
|
-
src={page.src}
|
|
44
|
-
width={page.width}
|
|
45
|
-
height={page.height}
|
|
46
|
-
alt={alt}
|
|
47
|
-
/>
|
|
48
|
-
</li>
|
|
49
|
-
))}
|
|
50
|
-
</ol>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** Query params `lyFilePlugin` recognizes as its own on a `.ly`-family import. */
|
|
2
|
-
export declare const RECOGNIZED_QUERY_PARAMS: readonly ["crop", "nocrop"];
|
|
3
|
-
export interface LyImportQuery {
|
|
4
|
-
/** The import's module id with any query string stripped. */
|
|
5
|
-
pathname: string;
|
|
6
|
-
/**
|
|
7
|
-
* The crop override the import's query string requests, if any:
|
|
8
|
-
* `true` for `?crop`, `false` for `?nocrop`, `undefined` if neither is
|
|
9
|
-
* present (the caller's own default applies).
|
|
10
|
-
*/
|
|
11
|
-
cropOverride: boolean | undefined;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Parses a Vite module id for a `.ly`/`.lilypond`/`.ily` import into its
|
|
15
|
-
* pathname and this plugin's `?crop`/`?nocrop` override.
|
|
16
|
-
*
|
|
17
|
-
* Returns `undefined` when the id carries a query param this plugin
|
|
18
|
-
* doesn't own (e.g. Vite's built-in `?raw`, `?url`) — such imports aren't
|
|
19
|
-
* ours to render, and the caller should let them fall through to whichever
|
|
20
|
-
* plugin does own them.
|
|
21
|
-
*/
|
|
22
|
-
export declare function parseLyImportQuery(id: string): LyImportQuery | undefined;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** Query params `lyFilePlugin` recognizes as its own on a `.ly`-family import. */
|
|
2
|
-
export const RECOGNIZED_QUERY_PARAMS = ["crop", "nocrop"];
|
|
3
|
-
const RECOGNIZED_PARAMS = new Set(RECOGNIZED_QUERY_PARAMS);
|
|
4
|
-
/**
|
|
5
|
-
* Parses a Vite module id for a `.ly`/`.lilypond`/`.ily` import into its
|
|
6
|
-
* pathname and this plugin's `?crop`/`?nocrop` override.
|
|
7
|
-
*
|
|
8
|
-
* Returns `undefined` when the id carries a query param this plugin
|
|
9
|
-
* doesn't own (e.g. Vite's built-in `?raw`, `?url`) — such imports aren't
|
|
10
|
-
* ours to render, and the caller should let them fall through to whichever
|
|
11
|
-
* plugin does own them.
|
|
12
|
-
*/
|
|
13
|
-
export function parseLyImportQuery(id) {
|
|
14
|
-
const queryIndex = id.indexOf("?");
|
|
15
|
-
const pathname = queryIndex === -1 ? id : id.slice(0, queryIndex);
|
|
16
|
-
const params = new URLSearchParams(queryIndex === -1 ? "" : id.slice(queryIndex + 1));
|
|
17
|
-
if ([...params.keys()].some((key) => !RECOGNIZED_PARAMS.has(key))) {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
const cropOverride = params.has("crop")
|
|
21
|
-
? true
|
|
22
|
-
: params.has("nocrop")
|
|
23
|
-
? false
|
|
24
|
-
: undefined;
|
|
25
|
-
return { pathname, cropOverride };
|
|
26
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LilyPond's standard `\header` field names
|
|
3
|
-
*/
|
|
4
|
-
export declare const STANDARD_HEADER_FIELDS: readonly ["dedication", "title", "subtitle", "subsubtitle", "instrument", "poet", "composer", "meter", "arranger", "piece", "opus", "copyright", "tagline"];
|
|
5
|
-
export interface KnownLyHeaderFields {
|
|
6
|
-
dedication?: string;
|
|
7
|
-
title?: string;
|
|
8
|
-
subtitle?: string;
|
|
9
|
-
subsubtitle?: string;
|
|
10
|
-
instrument?: string;
|
|
11
|
-
poet?: string;
|
|
12
|
-
composer?: string;
|
|
13
|
-
meter?: string;
|
|
14
|
-
arranger?: string;
|
|
15
|
-
piece?: string;
|
|
16
|
-
opus?: string;
|
|
17
|
-
copyright?: string;
|
|
18
|
-
tagline?: string;
|
|
19
|
-
}
|
|
20
|
-
/** Splits parsed header fields into LilyPond's standard named fields and everything else (e.g. `mutopiacomposer`, `maintainerEmail`). */
|
|
21
|
-
export declare function splitHeaderFields(fields: Record<string, string>): KnownLyHeaderFields & {
|
|
22
|
-
extra: Record<string, string>;
|
|
23
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LilyPond's standard `\header` field names
|
|
3
|
-
*/
|
|
4
|
-
export const STANDARD_HEADER_FIELDS = [
|
|
5
|
-
"dedication",
|
|
6
|
-
"title",
|
|
7
|
-
"subtitle",
|
|
8
|
-
"subsubtitle",
|
|
9
|
-
"instrument",
|
|
10
|
-
"poet",
|
|
11
|
-
"composer",
|
|
12
|
-
"meter",
|
|
13
|
-
"arranger",
|
|
14
|
-
"piece",
|
|
15
|
-
"opus",
|
|
16
|
-
"copyright",
|
|
17
|
-
"tagline",
|
|
18
|
-
];
|
|
19
|
-
/** Splits parsed header fields into LilyPond's standard named fields and everything else (e.g. `mutopiacomposer`, `maintainerEmail`). */
|
|
20
|
-
export function splitHeaderFields(fields) {
|
|
21
|
-
const known = {};
|
|
22
|
-
const extra = {};
|
|
23
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
24
|
-
if (STANDARD_HEADER_FIELDS.includes(key)) {
|
|
25
|
-
known[key] = value;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
extra[key] = value;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { ...known, extra };
|
|
32
|
-
}
|
|
File without changes
|