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.
Files changed (34) hide show
  1. package/dist/index.d.ts +81 -11
  2. package/dist/index.js +99 -39
  3. package/dist/loader.d.ts +42 -31
  4. package/dist/loader.js +33 -55
  5. package/dist/plugins/rehype.js +5 -6
  6. package/dist/plugins/remark.js +5 -6
  7. package/dist/plugins/satteri.js +5 -6
  8. package/dist/readOutputFile.js +4 -7
  9. package/dist/render.d.ts +4 -29
  10. package/dist/render.js +1 -9
  11. package/dist/renderState.d.ts +29 -0
  12. package/dist/renderState.js +46 -0
  13. package/dist/utils/{emitLilypondAsset.fake.d.ts → __tests__/emitLilypondAsset.fake.d.ts} +1 -1
  14. package/dist/utils/__tests__/emitLilypondPdfAsset.fake.d.ts +5 -0
  15. package/dist/utils/__tests__/emitLilypondPdfAsset.fake.js +6 -0
  16. package/dist/utils/emitLilypondPdfAsset.d.ts +8 -0
  17. package/dist/utils/emitLilypondPdfAsset.js +12 -0
  18. package/dist/utils/index.d.ts +3 -3
  19. package/dist/utils/index.js +2 -2
  20. package/dist/utils/lilypondMetadata.d.ts +10 -0
  21. package/dist/utils/lilypondMetadata.js +19 -0
  22. package/dist/utils/lyTypeDeclarationsFor.d.ts +2 -6
  23. package/dist/utils/lyTypeDeclarationsFor.js +3 -8
  24. package/dist/utils/parseLyHeader.js +1 -2
  25. package/dist/utils/renderedHtml.d.ts +9 -1
  26. package/dist/utils/renderedHtml.js +15 -12
  27. package/dist/utils/resolveDefaults.js +1 -2
  28. package/package.json +4 -4
  29. package/components/LilyPond.astro +0 -52
  30. package/dist/utils/lyImportQuery.d.ts +0 -22
  31. package/dist/utils/lyImportQuery.js +0 -26
  32. package/dist/utils/splitHeaderFields.d.ts +0 -23
  33. package/dist/utils/splitHeaderFields.js +0 -32
  34. /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 AutoInstallOptions } from "./binary/index.js";
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
- export interface LilypondContent {
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
- * Milliseconds to wait for a single `lilypond` invocation before
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. Set to
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 { defaultOptions, render, resolveCrop, } from "./render.js";
5
- import { altTextFor, emitLilypondAsset, includePathsFor, lyTypeDeclarationsFor, parseLyHeader, parseLyImportQuery, prependVersion, RECOGNIZED_QUERY_PARAMS, resolveDefaults, sourceNameFor, titleFor, } from "./utils/index.js";
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 lyFilePlugin(options) {
8
- return {
9
- name: "vite-plugin-astro-lilypond-ly",
10
- enforce: "pre",
11
- async transform(source, id) {
12
- const query = parseLyImportQuery(id);
13
- if (!query)
14
- return;
15
- const { pathname, cropOverride } = query;
16
- if (!LY_EXTENSIONS.some((ext) => pathname.endsWith(ext)))
17
- return;
18
- const { version, resolution, crop: cropSetting, cropScale, } = resolveDefaults(options.defaults);
19
- const crop = cropOverride ?? resolveCrop(cropSetting, "component");
20
- const src = version ? prependVersion(source, version) : source;
21
- const format = options.format ?? defaultOptions.format;
22
- const includePaths = includePathsFor(pathname);
23
- const sourceName = sourceNameFor(pathname);
24
- const title = titleFor(sourceName);
25
- const alt = altTextFor(parseLyHeader(source));
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: src,
33
+ source: score.source,
30
34
  resolution,
31
35
  crop,
32
36
  sizeScale: crop ? cropScale : 1,
33
- binaryPath: options.binaryPath,
34
- render: () => render(src, {
37
+ binaryPath: state.binaryPath,
38
+ render: () => renderScore(score.source, {
35
39
  format,
36
40
  crop,
37
- defaults: options.defaults,
38
- timeout: options.timeout,
39
- binaryPath: options.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
- const content = {
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(content)}`,
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 resolveLilypondBinary({
60
- ...resolveAutoInstallOption(options.autoInstall),
61
- log: (message) => logger?.info(message),
62
- warn: (message) => logger?.warn(message),
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, RECOGNIZED_QUERY_PARAMS),
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 { type AutoInstallOptions } from "./binary/index.js";
4
- import { type LilypondContent } from "./index.js";
5
- import type { PluginOptions } from "./plugins/index.js";
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 extends PluginOptions {
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
- export interface LilypondHeaderData extends KnownLyHeaderFields {
41
- /** Header fields outside LilyPond's standard set (e.g. `mutopiacomposer`). */
42
- extra: Record<string, string>;
43
- }
44
- export interface LilypondCollectionEntry extends LilypondContent, LilypondHeaderData {
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
- pages: z.ZodArray<z.ZodObject<{
48
- src: z.ZodString;
49
- width: z.ZodOptional<z.ZodNumber>;
50
- height: z.ZodOptional<z.ZodNumber>;
51
- }, z.core.$strip>>;
52
- alt: z.ZodOptional<z.ZodString>;
53
- dedication: z.ZodOptional<z.ZodString>;
54
- title: z.ZodOptional<z.ZodString>;
55
- subtitle: z.ZodOptional<z.ZodString>;
56
- subsubtitle: z.ZodOptional<z.ZodString>;
57
- instrument: z.ZodOptional<z.ZodString>;
58
- poet: z.ZodOptional<z.ZodString>;
59
- composer: z.ZodOptional<z.ZodString>;
60
- meter: z.ZodOptional<z.ZodString>;
61
- arranger: z.ZodOptional<z.ZodString>;
62
- piece: z.ZodOptional<z.ZodString>;
63
- opus: z.ZodOptional<z.ZodString>;
64
- copyright: z.ZodOptional<z.ZodString>;
65
- tagline: z.ZodOptional<z.ZodString>;
66
- extra: z.ZodRecord<z.ZodString, z.ZodString>;
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 { defaultOptions, render, resolveCrop } from "./render.js";
8
- import { altTextFor, emitLilypondAsset, includePathsFor, parseLyHeaderFields, prependVersion, resolveDefaults, sourceNameFor, splitHeaderFields, titleFor, } from "./utils/index.js";
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
- pages: z.array(z.object({
12
- src: z.string(),
13
- width: z.number().optional(),
14
- height: z.number().optional(),
15
- })),
16
- alt: z.string().optional(),
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, format, defaults, timeout, autoInstall, } = options;
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
- const binaryPath = await resolveLilypondBinary({
71
- ...resolveAutoInstallOption(autoInstall),
72
- log: (message) => logger.info(message),
73
- warn: (message) => logger.warn(message),
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 title = titleFor(sourceName);
92
- const pages = await emitLilypondAsset({
93
- title,
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
- pages,
114
- alt: altTextFor({
115
- title: headerFields.title,
116
- composer: headerFields.composer,
117
- }),
118
- ...splitHeaderFields(headerFields),
91
+ source: src,
92
+ alt,
93
+ sourceName,
94
+ includePaths,
95
+ assetTitle,
96
+ meta,
119
97
  },
120
98
  filePath,
121
99
  });
@@ -1,5 +1,5 @@
1
1
  import { visit } from "unist-util-visit";
2
- import { defaultOptions, render, resolveCrop } from "../render.js";
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, crop: cropSetting, cropScale, } = resolveDefaults(options.defaults);
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: crop ? cropScale : 1,
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,
@@ -1,5 +1,5 @@
1
1
  import { visit } from "unist-util-visit";
2
- import { defaultOptions, render, resolveCrop } from "../render.js";
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, crop: cropSetting, cropScale, } = resolveDefaults(options.defaults);
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: crop ? cropScale : 1,
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,
@@ -1,4 +1,4 @@
1
- import { defaultOptions, render, resolveCrop } from "../render.js";
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, crop: cropSetting, cropScale, } = resolveDefaults(options.defaults);
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: crop ? cropScale : 1,
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,
@@ -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 (and PDF/PS/EPS, should they
24
- * be added to `FORMATS`) uses `<base>-1.<ext>`; PNG uses `<base>-page1.<ext>`.
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
- // Regular output: single page → <base>.<format>; multi-page → numbered
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
- * and `crop` are resolved by the caller before reaching `render()`
57
- * (`version` by prepending it to source text, `crop` via `resolveCrop`).
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" | "crop">;
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 "./emitLilypondAsset.js";
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,6 @@
1
+ export function fakeEmitLilypondPdfAsset(mock, urlBase = "/_astro") {
2
+ mock.mockImplementation(async (opts) => {
3
+ await opts.render();
4
+ return { src: `${urlBase}/${opts.title}.pdf` };
5
+ });
6
+ }
@@ -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
+ }
@@ -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 { parseLyImportQuery, RECOGNIZED_QUERY_PARAMS, } from "./lyImportQuery.js";
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";
@@ -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 { parseLyImportQuery, RECOGNIZED_QUERY_PARAMS, } from "./lyImportQuery.js";
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
- * Builds the ambient module declarations injected for `.ly`-family imports,
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
- * Builds the ambient module declarations injected for `.ly`-family imports,
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
- .flatMap((ext) => suffixes.map((suffix) => `declare module "*${ext}${suffix}" {\n const content: import("astro-lilypond").LilypondContent;\n export default content;\n}`))
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 declare function renderedHtml(pages: LilypondPage[], alt: string): string;
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 { escapeHtmlAttribute } from "./escapeHtmlAttribute.js";
2
- function imgTag(page, escapedAlt) {
3
- const size = page.width !== undefined && page.height !== undefined
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 escapedAlt = escapeHtmlAttribute(alt);
10
- if (pages.length === 1) {
11
- return imgTag(pages[0], escapedAlt);
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>${pages
14
- .map((page) => `<li>${imgTag(page, escapedAlt)}</li>`)
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, crop, cropScale } = defaultOptions.defaults;
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.14.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
- }