astro-lilypond 0.16.2 → 0.17.1

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.
@@ -60,7 +60,7 @@ export async function downloadLilypond({ version, cacheDir = lilypondCacheDir(),
60
60
  if (await pathExists(markerPath))
61
61
  return binaryPath;
62
62
  const url = downloadUrlFor(version, platform, arch, archiveExt);
63
- log(`astro-lilypond: downloading LilyPond ${version} (${platform}-${arch})…`);
63
+ log(`Downloading LilyPond ${version} (${platform}-${arch})…`);
64
64
  const response = await fetchImpl(url);
65
65
  if (!response.ok || !response.body) {
66
66
  throw new Error(`failed to download LilyPond ${version} from ${url} (HTTP ${response.status})`);
@@ -86,7 +86,7 @@ export async function downloadLilypond({ version, cacheDir = lilypondCacheDir(),
86
86
  }
87
87
  }
88
88
  else {
89
- log("astro-lilypond: download response had no checksum header — skipping integrity verification");
89
+ log("Download response had no checksum header — skipping integrity verification");
90
90
  }
91
91
  await writeFile(archivePath, data);
92
92
  const extractDir = join(workDir, "extract");
@@ -106,7 +106,7 @@ export async function downloadLilypond({ version, cacheDir = lilypondCacheDir(),
106
106
  const reuseConcurrentInstall = async () => {
107
107
  if (!(await pathExists(markerPath)))
108
108
  return false;
109
- log(`astro-lilypond: LilyPond ${version} was installed concurrently by another process; reusing it`);
109
+ log(`LilyPond ${version} was installed concurrently by another process; reusing it`);
110
110
  return true;
111
111
  };
112
112
  if (await reuseConcurrentInstall())
@@ -120,7 +120,7 @@ export async function downloadLilypond({ version, cacheDir = lilypondCacheDir(),
120
120
  return binaryPath;
121
121
  throw err;
122
122
  }
123
- log(`astro-lilypond: installed LilyPond ${version} to ${installDir}`);
123
+ log(`Installed LilyPond ${version} to ${installDir}`);
124
124
  return binaryPath;
125
125
  }
126
126
  finally {
@@ -21,12 +21,12 @@ export async function resolveLilypondBinary({ version = DEFAULT_LILYPOND_VERSION
21
21
  if (await isOnPath())
22
22
  return "lilypond";
23
23
  if (!autoInstall) {
24
- warn("astro-lilypond: `lilypond` binary not found on PATH — LilyPond blocks will render as errors. " +
24
+ warn("No `lilypond` binary found on PATH — LilyPond blocks will render as errors. " +
25
25
  "Install LilyPond and ensure it is on PATH, or enable the `autoInstall` option (on by default).");
26
26
  return "lilypond";
27
27
  }
28
28
  if (!resolvePlatformTarget()) {
29
- warn(`astro-lilypond: \`lilypond\` binary not found on PATH, and no prebuilt LilyPond ${version} ` +
29
+ warn(`No \`lilypond\` binary found on PATH, and no prebuilt LilyPond ${version} ` +
30
30
  `is available for ${process.platform}/${process.arch}. Install LilyPond manually and ensure it is on PATH.`);
31
31
  return "lilypond";
32
32
  }
@@ -1,3 +1,4 @@
1
+ import type { AstroIntegrationLogger } from "astro";
1
2
  import type { Format } from "./render.js";
2
3
  export interface ExecLilypondOptions {
3
4
  binaryPath: string;
@@ -8,6 +9,7 @@ export interface ExecLilypondOptions {
8
9
  timeout: number;
9
10
  inputPath: string;
10
11
  outputBase: string;
12
+ logger: Pick<AstroIntegrationLogger, "warn" | "error">;
11
13
  }
12
14
  /**
13
15
  * Invokes the `lilypond` binary against an already-written input file.
@@ -8,10 +8,14 @@ const MAX_BUFFER = 16 * 1024 * 1024; // 16 MiB
8
8
  * build output); throws a normalized Error on a non-zero exit or timeout.
9
9
  */
10
10
  export async function execLilyPond(options) {
11
- const { binaryPath, format, crop, resolution, includePaths, timeout, inputPath, outputBase, } = options;
11
+ const { binaryPath, format, crop, resolution, includePaths, timeout, inputPath, outputBase, logger, } = options;
12
12
  const args = [
13
13
  // Render the correct format
14
14
  `--format=${format}`,
15
+ // Suppress LilyPond's own progress trace (Processing/Parsing/Interpreting
16
+ // music/.../Success), which is noise once Astro is reporting the build
17
+ // itself. Warnings and errors still print at this level.
18
+ "--loglevel=WARN",
15
19
  // Define defaults — these can be overridden with the appropriate flags
16
20
  // inside of individual .ly files
17
21
  // Disable point-and-click behavior on SVGs when clicking noteheads
@@ -29,9 +33,10 @@ export async function execLilyPond(options) {
29
33
  outputBase,
30
34
  inputPath,
31
35
  ];
32
- // LilyPond writes its progress log, (and any warnings/errors) to stderr,
33
- // even on success. surface it in the build output so it's visible,
34
- // but let the exit code (not stderr content) decide pass/fail.
36
+ // LilyPond writes any warnings to stderr even on success (errors on
37
+ // failure). Surface it through Astro's logger so it's visible and styled
38
+ // consistently with the rest of the build output, but let the exit code
39
+ // (not stderr content) decide pass/fail.
35
40
  let stderr;
36
41
  try {
37
42
  ({ stderr } = await execFileAsync(binaryPath, args, {
@@ -49,9 +54,9 @@ export async function execLilyPond(options) {
49
54
  ? String(err.stderr)
50
55
  : undefined;
51
56
  if (errStderr)
52
- process.stderr.write(errStderr);
57
+ logger.error(errStderr);
53
58
  throw new Error(errStderr || (err instanceof Error ? err.message : String(err)));
54
59
  }
55
60
  if (stderr)
56
- process.stderr.write(stderr);
61
+ logger.warn(stderr);
57
62
  }
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import emitAssetIntegration from "astro-emit-asset";
3
3
  import { remarkPlugin, satteriPlugin, } from "./plugins/index.js";
4
4
  import { render as renderScore } from "./render.js";
5
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
+ import { altTextFor, emitLilypondAsset, emitLilypondPdfAsset, includePathsFor, lyTypeDeclarationsFor, parseLyHeaderFields, prependVersion, renderedErrorHtml, renderedHtml, resolveDefaults, sourceNameFor, titleFor, toLilypondMetadata, } from "./utils/index.js";
7
7
  export const LY_EXTENSIONS = [".ly", ".lilypond", ".ily"];
8
8
  function createScoreComponent(content) {
9
9
  return createComponent((_result, props) => {
@@ -16,6 +16,15 @@ function createScoreComponent(content) {
16
16
  return renderTemplate `${unescapeHTML(html)}`;
17
17
  });
18
18
  }
19
+ /**
20
+ * Dev-only fallback for `render()`: renders an inline error block
21
+ * instead of the score.
22
+ */
23
+ function createErrorScoreComponent(error, title) {
24
+ return createComponent(() => {
25
+ return renderTemplate `${unescapeHTML(renderedErrorHtml(error, title))}`;
26
+ });
27
+ }
19
28
  /**
20
29
  * Renders a `LilypondScore` (from a `.ly`/`.ily`/`.lilypond` import,
21
30
  * or a `lilypondLoader()` entry) to a renderable `<Score />` component.
@@ -25,49 +34,64 @@ export async function render(score, options = {}) {
25
34
  const { resolution, cropScale } = resolveDefaults(state.defaults);
26
35
  const format = options.format ?? "svg";
27
36
  const crop = options.crop ?? false;
28
- const [{ Score, pageCount }, pdf] = await Promise.all([
29
- (async () => {
30
- const pages = await emitLilypondAsset({
31
- title: score.assetTitle,
32
- format,
33
- source: score.source,
34
- resolution,
35
- crop,
36
- sizeScale: crop ? cropScale : 1,
37
- binaryPath: state.binaryPath,
38
- render: () => renderScore(score.source, {
37
+ try {
38
+ const [{ Score, pageCount }, pdf] = await Promise.all([
39
+ (async () => {
40
+ const pages = await emitLilypondAsset({
41
+ title: score.assetTitle,
39
42
  format,
43
+ source: score.source,
44
+ resolution,
40
45
  crop,
41
- defaults: state.defaults,
42
- timeout: state.timeout,
46
+ sizeScale: crop ? cropScale : 1,
43
47
  binaryPath: state.binaryPath,
44
- includePaths: score.includePaths,
45
- sourceName: score.sourceName,
46
- }),
47
- });
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,
48
+ render: () => renderScore(score.source, {
49
+ format,
50
+ crop,
51
+ defaults: state.defaults,
52
+ timeout: state.timeout,
53
+ binaryPath: state.binaryPath,
54
+ includePaths: score.includePaths,
55
+ sourceName: score.sourceName,
56
+ logger: state.logger,
57
+ }),
58
+ });
59
+ return {
60
+ Score: createScoreComponent({ pages, alt: score.alt }),
61
+ pageCount: pages.length,
62
+ };
63
+ })(),
64
+ options.pdf
65
+ ? emitLilypondPdfAsset({
66
+ title: score.assetTitle,
67
+ source: score.source,
63
68
  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 };
69
+ render: () => renderScore(score.source, {
70
+ format: "pdf",
71
+ crop: false,
72
+ defaults: state.defaults,
73
+ timeout: state.timeout,
74
+ binaryPath: state.binaryPath,
75
+ includePaths: score.includePaths,
76
+ sourceName: score.sourceName,
77
+ logger: state.logger,
78
+ }),
79
+ })
80
+ : Promise.resolve(undefined),
81
+ ]);
82
+ return { Score, pageCount, pdf, meta: score.meta, raw: score.source };
83
+ }
84
+ catch (err) {
85
+ if (!state.isDev)
86
+ throw err;
87
+ return {
88
+ Score: createErrorScoreComponent(err, score.assetTitle),
89
+ pageCount: 0,
90
+ pdf: undefined,
91
+ meta: score.meta,
92
+ raw: score.source,
93
+ };
94
+ }
71
95
  }
72
96
  /**
73
97
  * Renders a `LilypondScore` directly, for the common case where none of
@@ -114,11 +138,15 @@ export default function lilypond(options = {}) {
114
138
  return {
115
139
  name: "astro-lilypond",
116
140
  hooks: {
117
- "astro:config:setup": async ({ config, updateConfig, logger }) => {
141
+ "astro:config:setup": async ({ command, config, updateConfig, logger, }) => {
142
+ const isDev = command === "dev";
143
+ options.isDev = isDev;
144
+ options.logger = logger;
118
145
  options.binaryPath = await resolveAndSetRenderState({
119
146
  autoInstall: options.autoInstall,
120
147
  defaults: options.defaults,
121
148
  timeout: options.timeout,
149
+ isDev,
122
150
  logger,
123
151
  });
124
152
  updateConfig({
@@ -144,7 +172,7 @@ export default function lilypond(options = {}) {
144
172
  }),
145
173
  },
146
174
  });
147
- logger?.info("astro-lilypond: registered Sätteri mdast plugin");
175
+ logger?.info("Registered Sätteri mdast plugin");
148
176
  return;
149
177
  }
150
178
  if (existingProcessor?.name === "unified") {
@@ -165,7 +193,7 @@ export default function lilypond(options = {}) {
165
193
  }),
166
194
  },
167
195
  });
168
- logger?.info("astro-lilypond: registered unified remark plugin");
196
+ logger?.info("Registered unified remark plugin");
169
197
  return;
170
198
  }
171
199
  throw new Error("astro-lilypond requires a processor-based Astro markdown config. " +
package/dist/loader.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
2
  import { z } from "astro/zod";
3
3
  import { type LilypondScore } from "./index.js";
4
- import type { LilypondDefaults } from "./render.js";
5
4
  export interface GenerateIdOptions {
6
5
  /** The path to the entry file, relative to `base`. */
7
6
  entry: string;
@@ -26,11 +25,6 @@ export interface LilypondLoaderOptions {
26
25
  * @default the file's path relative to `base`, POSIX-separated, with its extension stripped.
27
26
  */
28
27
  generateId?: (options: GenerateIdOptions) => string;
29
- /**
30
- * Defaults used when `render()` is later called on one of this
31
- * collection's entries.
32
- */
33
- defaults?: LilypondDefaults;
34
28
  }
35
29
  export type LilypondCollectionEntry = LilypondScore;
36
30
  export declare const lilypondEntrySchema: z.ZodObject<{
@@ -58,7 +52,7 @@ export declare const lilypondEntrySchema: z.ZodObject<{
58
52
  /**
59
53
  * Content Loader to turn a directory of `.ly` files into a content collection.
60
54
  */
61
- export declare function lilypondLoader({ pattern, base, generateId, defaults, }: LilypondLoaderOptions): {
55
+ export declare function lilypondLoader({ pattern, base, generateId, }: LilypondLoaderOptions): {
62
56
  name: string;
63
57
  schema: z.ZodObject<{
64
58
  source: z.ZodString;
package/dist/loader.js CHANGED
@@ -3,6 +3,7 @@ import { join, relative, sep } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { z } from "astro/zod";
5
5
  import { LY_EXTENSIONS } from "./index.js";
6
+ import { getRenderState } from "./renderState.js";
6
7
  import { altTextFor, includePathsFor, parseLyHeaderFields, prependVersion, resolveDefaults, sourceNameFor, titleFor, toLilypondMetadata, } from "./utils/index.js";
7
8
  const DEFAULT_PATTERN = `**/*.{${LY_EXTENSIONS.map((ext) => ext.slice(1)).join(",")}}`;
8
9
  // Field names must match `LilypondMetadata` in `utils/lilypondMetadata.ts`.
@@ -55,13 +56,13 @@ function posixRelative(from, to) {
55
56
  /**
56
57
  * Content Loader to turn a directory of `.ly` files into a content collection.
57
58
  */
58
- export function lilypondLoader({ pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId, defaults, }) {
59
- const resolved = resolveDefaults(defaults);
59
+ export function lilypondLoader({ pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId, }) {
60
60
  return {
61
61
  name: "astro-lilypond-loader",
62
62
  schema: lilypondEntrySchema,
63
63
  async load(context) {
64
64
  const { config, store, logger, watcher, generateDigest, parseData } = context;
65
+ const { version } = resolveDefaults(getRenderState().defaults);
65
66
  const rootDir = fileURLToPath(config.root);
66
67
  const baseUrl = resolveBaseUrl(base, config.root);
67
68
  const baseDir = fileURLToPath(baseUrl);
@@ -72,13 +73,13 @@ export function lilypondLoader({ pattern = DEFAULT_PATTERN, base, generateId = d
72
73
  source = await readFile(filePath, "utf8");
73
74
  }
74
75
  catch (err) {
75
- logger.error(`astro-lilypond: error reading ${entry}: ${err.message}`);
76
+ logger.error(`Error reading ${entry}: ${err.message}`);
76
77
  return undefined;
77
78
  }
78
79
  const headerFields = parseLyHeaderFields(source);
79
80
  const id = generateId({ entry, base: baseUrl, header: headerFields });
80
81
  const digest = generateDigest(source);
81
- const src = prependVersion(source, resolved.version);
82
+ const src = prependVersion(source, version);
82
83
  const includePaths = includePathsFor(filePath);
83
84
  const sourceName = sourceNameFor(filePath);
84
85
  const assetTitle = titleFor(sourceName);
@@ -116,7 +117,7 @@ export function lilypondLoader({ pattern = DEFAULT_PATTERN, base, generateId = d
116
117
  entries.push(entry);
117
118
  }
118
119
  if (entries.length === 0) {
119
- logger.warn(`astro-lilypond: no files matched "${pattern}" in "${baseDir}"`);
120
+ logger.warn(`No files matched "${pattern}" in "${baseDir}"`);
120
121
  }
121
122
  const results = await Promise.all(entries.map(async (entry) => ({ entry, id: await syncEntry(entry) })));
122
123
  for (const { entry, id } of results) {
@@ -146,7 +147,7 @@ export function lilypondLoader({ pattern = DEFAULT_PATTERN, base, generateId = d
146
147
  if (!changedPath.startsWith(baseDir))
147
148
  return;
148
149
  queue = queue.then(() => runSync().catch((err) => {
149
- logger.error(`astro-lilypond: reload failed: ${err.message}`);
150
+ logger.error(`Reload failed: ${err.message}`);
150
151
  }));
151
152
  };
152
153
  watcher.on("add", onFsEvent);
@@ -1,7 +1,11 @@
1
1
  import { visit } from "unist-util-visit";
2
2
  import { defaultOptions, render } from "../render.js";
3
- import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
3
+ import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedErrorHtml, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
4
4
  export const remarkPlugin = (options) => {
5
+ const { logger } = options;
6
+ if (!logger) {
7
+ throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
8
+ }
5
9
  return async (tree, file) => {
6
10
  const promises = [];
7
11
  const includePaths = includePathsFor(file?.path);
@@ -14,30 +18,38 @@ export const remarkPlugin = (options) => {
14
18
  const source = version ? prependVersion(node.value, version) : node.value;
15
19
  const format = options.format ?? defaultOptions.format;
16
20
  const alt = altTextForBlock(node.meta, node.value);
17
- const promise = emitLilypondAsset({
18
- title,
19
- format,
20
- source,
21
- resolution,
22
- crop: true,
23
- sizeScale: cropScale,
24
- binaryPath: options.binaryPath,
25
- render: () => render(source, {
26
- format,
27
- crop: true,
28
- defaults: options.defaults,
29
- timeout: options.timeout,
30
- binaryPath: options.binaryPath,
31
- includePaths,
32
- sourceName,
33
- }),
34
- }).then((pages) => {
35
- const htmlNode = {
36
- type: "html",
37
- value: renderedHtml(pages, alt),
38
- };
21
+ const promise = (async () => {
22
+ let value;
23
+ try {
24
+ const pages = await emitLilypondAsset({
25
+ title,
26
+ format,
27
+ source,
28
+ resolution,
29
+ crop: true,
30
+ sizeScale: cropScale,
31
+ binaryPath: options.binaryPath,
32
+ render: () => render(source, {
33
+ format,
34
+ crop: true,
35
+ defaults: options.defaults,
36
+ timeout: options.timeout,
37
+ binaryPath: options.binaryPath,
38
+ includePaths,
39
+ sourceName,
40
+ logger,
41
+ }),
42
+ });
43
+ value = renderedHtml(pages, alt);
44
+ }
45
+ catch (err) {
46
+ if (!options.isDev)
47
+ throw err;
48
+ value = renderedErrorHtml(err, title);
49
+ }
50
+ const htmlNode = { type: "html", value };
39
51
  parent.children[index] = htmlNode;
40
- });
52
+ })();
41
53
  promises.push(promise);
42
54
  });
43
55
  await Promise.all(promises);
@@ -1,6 +1,10 @@
1
1
  import { defaultOptions, render } from "../render.js";
2
- import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
2
+ import { altTextForBlock, emitLilypondAsset, includePathsFor, isLilypondLang, prependVersion, renderedErrorHtml, renderedHtml, resolveDefaults, sourceNameFor, titleFor, } from "../utils/index.js";
3
3
  export function satteriPlugin(options) {
4
+ const { logger } = options;
5
+ if (!logger) {
6
+ throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
7
+ }
4
8
  return {
5
9
  name: "astro-lilypond",
6
10
  async code(node, ctx) {
@@ -13,25 +17,33 @@ export function satteriPlugin(options) {
13
17
  const sourceName = sourceNameFor(ctx.fileURL);
14
18
  const title = titleFor(sourceName);
15
19
  const alt = altTextForBlock(node.meta, node.value);
16
- const pages = await emitLilypondAsset({
17
- title,
18
- format,
19
- source,
20
- resolution,
21
- crop: true,
22
- sizeScale: cropScale,
23
- binaryPath: options.binaryPath,
24
- render: () => render(source, {
20
+ try {
21
+ const pages = await emitLilypondAsset({
22
+ title,
25
23
  format,
24
+ source,
25
+ resolution,
26
26
  crop: true,
27
- defaults: options.defaults,
28
- timeout: options.timeout,
27
+ sizeScale: cropScale,
29
28
  binaryPath: options.binaryPath,
30
- includePaths,
31
- sourceName,
32
- }),
33
- });
34
- return { rawHtml: renderedHtml(pages, alt) };
29
+ render: () => render(source, {
30
+ format,
31
+ crop: true,
32
+ defaults: options.defaults,
33
+ timeout: options.timeout,
34
+ binaryPath: options.binaryPath,
35
+ includePaths,
36
+ sourceName,
37
+ logger,
38
+ }),
39
+ });
40
+ return { rawHtml: renderedHtml(pages, alt) };
41
+ }
42
+ catch (err) {
43
+ if (!options.isDev)
44
+ throw err;
45
+ return { rawHtml: renderedErrorHtml(err, title) };
46
+ }
35
47
  },
36
48
  };
37
49
  }
@@ -1,7 +1,10 @@
1
+ import type { AstroIntegrationLogger } from "astro";
1
2
  import type { LilypondDefaults } from "../render.js";
2
3
  export interface PluginOptions {
3
4
  format?: "svg" | "png";
4
5
  defaults?: LilypondDefaults;
5
6
  timeout?: number;
6
7
  binaryPath?: string;
8
+ isDev?: boolean;
9
+ logger?: Pick<AstroIntegrationLogger, "warn" | "error">;
7
10
  }
package/dist/render.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AstroIntegrationLogger } from "astro";
1
2
  import type { LilypondVersion } from "./types/lilypondVersion.js";
2
3
  export declare const FORMATS: readonly ["png", "svg", "pdf"];
3
4
  export type Format = (typeof FORMATS)[number];
@@ -32,7 +33,7 @@ export interface LilypondDefaults {
32
33
  * source text.
33
34
  */
34
35
  export type RenderDefaults = Omit<LilypondDefaults, "version">;
35
- export interface RenderOptions {
36
+ export interface InternalRenderOptions {
36
37
  /**
37
38
  * Output format.
38
39
  * @default "svg"
@@ -72,8 +73,12 @@ export interface RenderOptions {
72
73
  * @default 60000
73
74
  */
74
75
  timeout?: number;
76
+ /**
77
+ * Warning and failure logging from LilyPond.
78
+ */
79
+ logger: Pick<AstroIntegrationLogger, "warn" | "error">;
75
80
  }
76
- export declare const defaultOptions: Required<Omit<RenderOptions, "includePaths" | "sourceName" | "defaults">> & {
81
+ export declare const defaultOptions: Required<Omit<InternalRenderOptions, "includePaths" | "sourceName" | "defaults" | "logger">> & {
77
82
  defaults: Required<LilypondDefaults>;
78
83
  };
79
- export declare function render(source: string, options?: RenderOptions): Promise<Buffer[]>;
84
+ export declare function render(source: string, options: InternalRenderOptions): Promise<Buffer[]>;
package/dist/render.js CHANGED
@@ -16,8 +16,8 @@ export const defaultOptions = {
16
16
  cropScale: 1.5,
17
17
  },
18
18
  };
19
- export async function render(source, options = {}) {
20
- const { format = defaultOptions.format, crop = defaultOptions.crop, binaryPath = defaultOptions.binaryPath, timeout = defaultOptions.timeout, includePaths = [], sourceName, } = options;
19
+ export async function render(source, options) {
20
+ const { format = defaultOptions.format, crop = defaultOptions.crop, binaryPath = defaultOptions.binaryPath, timeout = defaultOptions.timeout, includePaths = [], sourceName, logger, } = options;
21
21
  const { resolution } = resolveDefaults(options.defaults);
22
22
  if (!FORMATS.includes(format)) {
23
23
  throw new Error(`${format} is not a supported format`);
@@ -36,6 +36,7 @@ export async function render(source, options = {}) {
36
36
  timeout,
37
37
  inputPath,
38
38
  outputBase,
39
+ logger,
39
40
  });
40
41
  return await readOutputFile(outputBase, format, crop);
41
42
  }
@@ -1,9 +1,12 @@
1
+ import type { AstroIntegrationLogger } from "astro";
1
2
  import { type AutoInstallOptions } from "./binary/index.js";
2
3
  import type { LilypondDefaults } from "./render.js";
3
4
  export interface RenderState {
4
5
  binaryPath: string;
5
6
  defaults: LilypondDefaults | undefined;
6
7
  timeout: number | undefined;
8
+ isDev: boolean;
9
+ logger: Pick<AstroIntegrationLogger, "warn" | "error">;
7
10
  }
8
11
  /** Called by the `lilypond()` integration's `astro:config:setup` hook. */
9
12
  export declare function setRenderState(next: RenderState): void;
@@ -15,10 +18,8 @@ export interface ResolveAndSetRenderStateOptions {
15
18
  autoInstall?: boolean | AutoInstallOptions;
16
19
  defaults?: LilypondDefaults;
17
20
  timeout?: number;
18
- logger: {
19
- info: (message: string) => void;
20
- warn: (message: string) => void;
21
- };
21
+ isDev: boolean;
22
+ logger: Pick<AstroIntegrationLogger, "info" | "warn" | "error">;
22
23
  }
23
24
  /**
24
25
  * Shared by the `lilypond()` integration's `astro:config:setup` hook and
@@ -41,6 +41,8 @@ export async function resolveAndSetRenderState(options) {
41
41
  binaryPath,
42
42
  defaults: options.defaults,
43
43
  timeout: options.timeout,
44
+ isDev: options.isDev,
45
+ logger: options.logger,
44
46
  });
45
47
  return binaryPath;
46
48
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Escape a string for safe interpolation as HTML text-node content.
3
+ */
4
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Escape a string for safe interpolation as HTML text-node content.
3
+ */
4
+ export function escapeHtml(value) {
5
+ return value
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;");
9
+ }
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Escapes a string for safe interpolation inside a double-quoted HTML
3
- * attribute value. Used for alt text, which — unlike the srcs this
4
- * integration already emits — is arbitrary user/header-derived content that
5
- * can contain `"`, `&`, `<`.
2
+ * Escape a string for safe interpolation inside a double-quoted
3
+ * HTML attribute value.
6
4
  */
7
5
  export declare function escapeHtmlAttribute(value: string): string;
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Escapes a string for safe interpolation inside a double-quoted HTML
3
- * attribute value. Used for alt text, which — unlike the srcs this
4
- * integration already emits — is arbitrary user/header-derived content that
5
- * can contain `"`, `&`, `<`.
2
+ * Escape a string for safe interpolation inside a double-quoted
3
+ * HTML attribute value.
6
4
  */
7
5
  export function escapeHtmlAttribute(value) {
8
6
  return value
@@ -2,6 +2,7 @@ export { altTextFor } from "./altTextFor.js";
2
2
  export { altTextForBlock } from "./altTextForBlock.js";
3
3
  export { type EmitLilypondAssetOptions, emitLilypondAsset, } from "./emitLilypondAsset.js";
4
4
  export { type EmitLilypondPdfAssetOptions, emitLilypondPdfAsset, } from "./emitLilypondPdfAsset.js";
5
+ export { escapeHtml } from "./escapeHtml.js";
5
6
  export { escapeHtmlAttribute } from "./escapeHtmlAttribute.js";
6
7
  export type { ImageDimensions } from "./imageDimensions.js";
7
8
  export { imageDimensionsFor } from "./imageDimensions.js";
@@ -14,6 +15,7 @@ export { parseFenceMeta } from "./parseFenceMeta.js";
14
15
  export type { LyHeaderFields } from "./parseLyHeader.js";
15
16
  export { extractMarkupText, parseLyHeader, parseLyHeaderFields, } from "./parseLyHeader.js";
16
17
  export { prependVersion } from "./prependVersion.js";
18
+ export { renderedErrorHtml } from "./renderedErrorHtml.js";
17
19
  export { renderedHtml } from "./renderedHtml.js";
18
20
  export { resolveDefaults } from "./resolveDefaults.js";
19
21
  export { sourceNameFor } from "./sourceNameFor.js";
@@ -2,6 +2,7 @@ export { altTextFor } from "./altTextFor.js";
2
2
  export { altTextForBlock } from "./altTextForBlock.js";
3
3
  export { emitLilypondAsset, } from "./emitLilypondAsset.js";
4
4
  export { emitLilypondPdfAsset, } from "./emitLilypondPdfAsset.js";
5
+ export { escapeHtml } from "./escapeHtml.js";
5
6
  export { escapeHtmlAttribute } from "./escapeHtmlAttribute.js";
6
7
  export { imageDimensionsFor } from "./imageDimensions.js";
7
8
  export { includePathsFor } from "./includePathsFor.js";
@@ -11,6 +12,7 @@ export { lyTypeDeclarationsFor } from "./lyTypeDeclarationsFor.js";
11
12
  export { parseFenceMeta } from "./parseFenceMeta.js";
12
13
  export { extractMarkupText, parseLyHeader, parseLyHeaderFields, } from "./parseLyHeader.js";
13
14
  export { prependVersion } from "./prependVersion.js";
15
+ export { renderedErrorHtml } from "./renderedErrorHtml.js";
14
16
  export { renderedHtml } from "./renderedHtml.js";
15
17
  export { resolveDefaults } from "./resolveDefaults.js";
16
18
  export { sourceNameFor } from "./sourceNameFor.js";
@@ -0,0 +1 @@
1
+ export declare function renderedErrorHtml(error: unknown, title: string): string;
@@ -0,0 +1,22 @@
1
+ import { escapeHtml } from "./escapeHtml.js";
2
+ const ERROR_STYLE = `display: block;
3
+ margin: 1em 0;
4
+ padding: 1em;
5
+ color-scheme: light dark;
6
+ border: 2px solid light-dark(#dc2626, #f87171);
7
+ border-radius: 6px;
8
+ background: light-dark(#fef2f2, #450a0a);
9
+ color: light-dark(#5d1516, #fcd1d2);
10
+ font-family: ui-monospace, monospace;
11
+ font-size: 0.875rem;
12
+ line-height: 1.5;
13
+ white-space: pre-wrap;
14
+ overflow-wrap: break-word;
15
+ text-align: left;`;
16
+ function messageFor(error) {
17
+ return error instanceof Error ? error.message : String(error);
18
+ }
19
+ export function renderedErrorHtml(error, title) {
20
+ const heading = `LilyPond failed to render "${escapeHtml(title)}"`;
21
+ return `<pre style="${ERROR_STYLE}">${heading}\n\n${escapeHtml(messageFor(error))}</pre>`;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-lilypond",
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
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",
@@ -25,6 +25,9 @@
25
25
  "files": [
26
26
  "dist"
27
27
  ],
28
+ "engines": {
29
+ "node": ">=22.12.0"
30
+ },
28
31
  "keywords": [
29
32
  "astro",
30
33
  "astro-integration",