astro-lilypond 0.13.0 → 0.14.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/README.md +3 -7
- package/dist/binary/autoInstall.d.ts +13 -0
- package/dist/binary/autoInstall.js +6 -0
- package/dist/binary/cacheDir.d.ts +1 -0
- package/dist/binary/cacheDir.js +15 -0
- package/dist/binary/downloadLilypond.d.ts +15 -0
- package/dist/binary/downloadLilypond.js +129 -0
- package/dist/binary/index.d.ts +5 -0
- package/dist/binary/index.js +5 -0
- package/dist/binary/platformTarget.d.ts +14 -0
- package/dist/binary/platformTarget.js +35 -0
- package/dist/binary/resolveLilypondBinary.d.ts +9 -0
- package/dist/binary/resolveLilypondBinary.js +41 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +7 -7
- package/dist/loader.d.ts +9 -0
- package/dist/loader.js +9 -1
- package/dist/plugins/rehype.js +2 -0
- package/dist/plugins/remark.js +2 -0
- package/dist/plugins/satteri.js +2 -0
- package/dist/plugins/types.d.ts +1 -0
- package/dist/utils/altTextFor.d.ts +1 -3
- package/dist/utils/altTextFor.js +1 -3
- package/dist/utils/altTextForBlock.d.ts +0 -1
- package/dist/utils/altTextForBlock.js +0 -1
- package/dist/utils/emitLilypondAsset.d.ts +1 -17
- package/dist/utils/emitLilypondAsset.fake.d.ts +0 -4
- package/dist/utils/emitLilypondAsset.fake.js +0 -4
- package/dist/utils/emitLilypondAsset.js +5 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,17 +13,13 @@ Read the docs: https://lilypond.ky.fyi
|
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
|
-
### 1. Install
|
|
17
|
-
|
|
18
|
-
Install the LilyPond binary directly from [lilypond.org](https://lilypond.org/download.html) or with `brew install lilypond` (Mac) or `apt install lilypond` (Linux).
|
|
19
|
-
|
|
20
|
-
### 2. Install `astro-lilypond`
|
|
16
|
+
### 1. Install `astro-lilypond`
|
|
21
17
|
|
|
22
18
|
```sh
|
|
23
19
|
pnpm add astro-lilypond
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
###
|
|
22
|
+
### 2. Add the integration to your Astro config
|
|
27
23
|
|
|
28
24
|
```js
|
|
29
25
|
// astro.config.mjs
|
|
@@ -35,7 +31,7 @@ export default defineConfig({
|
|
|
35
31
|
});
|
|
36
32
|
```
|
|
37
33
|
|
|
38
|
-
###
|
|
34
|
+
### 3. Write your music
|
|
39
35
|
|
|
40
36
|
Write LilyPond code within fenced code blocks with the `lilypond`, `ly`, or `ily` language tag.
|
|
41
37
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LilypondVersion } from "../types/lilypondVersion.js";
|
|
2
|
+
export interface AutoInstallOptions {
|
|
3
|
+
/**
|
|
4
|
+
* LilyPond version to download when none is found on `PATH`. Independent
|
|
5
|
+
* of `defaults.version`, which scores are compiled against.
|
|
6
|
+
* @default "2.26.0"
|
|
7
|
+
*/
|
|
8
|
+
version?: LilypondVersion;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveAutoInstallOption(autoInstall: boolean | AutoInstallOptions | undefined): {
|
|
11
|
+
version?: LilypondVersion;
|
|
12
|
+
autoInstall: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lilypondCacheDir(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function lilypondCacheDir(env = process.env, platform = process.platform) {
|
|
4
|
+
if (env.ASTRO_LILYPOND_CACHE_DIR)
|
|
5
|
+
return env.ASTRO_LILYPOND_CACHE_DIR;
|
|
6
|
+
if (platform === "darwin") {
|
|
7
|
+
return join(homedir(), "Library", "Caches", "astro-lilypond");
|
|
8
|
+
}
|
|
9
|
+
if (platform === "win32") {
|
|
10
|
+
const base = env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
|
|
11
|
+
return join(base, "astro-lilypond", "Cache");
|
|
12
|
+
}
|
|
13
|
+
const base = env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
|
|
14
|
+
return join(base, "astro-lilypond");
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LilypondVersion } from "../types/lilypondVersion.js";
|
|
2
|
+
export interface DownloadLilypondOptions {
|
|
3
|
+
version: LilypondVersion;
|
|
4
|
+
cacheDir?: string;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
log?: (message: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Downloads, verifies, and extracts a prebuilt LilyPond release into the
|
|
10
|
+
* cache dir, then returns the path to its `bin/lilypond[.exe]`. Returns
|
|
11
|
+
* `undefined` when LilyPond publishes no prebuilt for the current
|
|
12
|
+
* platform/arch. A previously-completed install for the same version is
|
|
13
|
+
* reused as-is.
|
|
14
|
+
*/
|
|
15
|
+
export declare function downloadLilypond({ version, cacheDir, fetchImpl, log, }: DownloadLilypondOptions): Promise<string | undefined>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { access, mkdir, mkdtemp, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { lilypondCacheDir } from "./cacheDir.js";
|
|
7
|
+
import { resolvePlatformTarget } from "./platformTarget.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
function downloadUrlFor(version, platform, arch, archiveExt) {
|
|
10
|
+
const filename = `lilypond-${version}-${platform}-${arch}.${archiveExt}`;
|
|
11
|
+
return `https://gitlab.com/api/v4/projects/lilypond%2Flilypond/packages/generic/lilypond/${version}/${filename}`;
|
|
12
|
+
}
|
|
13
|
+
async function pathExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await access(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `.zip` archives (Windows/mingw) use PowerShell's `Expand-Archive` rather
|
|
24
|
+
* than `tar`, since `tar -xf`'s zip support depends on a libarchive/bsdtar
|
|
25
|
+
* build resolving first on `PATH`, which isn't guaranteed on Windows.
|
|
26
|
+
*/
|
|
27
|
+
async function extractArchive(archivePath, extractDir, archiveExt) {
|
|
28
|
+
if (archiveExt === "zip") {
|
|
29
|
+
await execFileAsync("powershell.exe", [
|
|
30
|
+
"-NoProfile",
|
|
31
|
+
"-NonInteractive",
|
|
32
|
+
"-Command",
|
|
33
|
+
"Expand-Archive -LiteralPath $env:ASTRO_LILYPOND_ARCHIVE -DestinationPath $env:ASTRO_LILYPOND_EXTRACT_DIR -Force",
|
|
34
|
+
], {
|
|
35
|
+
env: {
|
|
36
|
+
...process.env,
|
|
37
|
+
ASTRO_LILYPOND_ARCHIVE: archivePath,
|
|
38
|
+
ASTRO_LILYPOND_EXTRACT_DIR: extractDir,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await execFileAsync("tar", ["-xf", archivePath, "-C", extractDir]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Downloads, verifies, and extracts a prebuilt LilyPond release into the
|
|
47
|
+
* cache dir, then returns the path to its `bin/lilypond[.exe]`. Returns
|
|
48
|
+
* `undefined` when LilyPond publishes no prebuilt for the current
|
|
49
|
+
* platform/arch. A previously-completed install for the same version is
|
|
50
|
+
* reused as-is.
|
|
51
|
+
*/
|
|
52
|
+
export async function downloadLilypond({ version, cacheDir = lilypondCacheDir(), fetchImpl = fetch, log = () => { }, }) {
|
|
53
|
+
const target = resolvePlatformTarget();
|
|
54
|
+
if (!target)
|
|
55
|
+
return undefined;
|
|
56
|
+
const { platform, arch, archiveExt, binaryName } = target;
|
|
57
|
+
const installDir = join(cacheDir, `lilypond-${version}-${platform}-${arch}`);
|
|
58
|
+
const markerPath = join(installDir, ".complete");
|
|
59
|
+
const binaryPath = join(installDir, "bin", binaryName);
|
|
60
|
+
if (await pathExists(markerPath))
|
|
61
|
+
return binaryPath;
|
|
62
|
+
const url = downloadUrlFor(version, platform, arch, archiveExt);
|
|
63
|
+
log(`astro-lilypond: downloading LilyPond ${version} (${platform}-${arch})…`);
|
|
64
|
+
const response = await fetchImpl(url);
|
|
65
|
+
if (!response.ok || !response.body) {
|
|
66
|
+
throw new Error(`failed to download LilyPond ${version} from ${url} (HTTP ${response.status})`);
|
|
67
|
+
}
|
|
68
|
+
const expectedSha256 = response.headers.get("x-checksum-sha256");
|
|
69
|
+
await mkdir(cacheDir, { recursive: true });
|
|
70
|
+
const workDir = await mkdtemp(join(cacheDir, ".tmp-"));
|
|
71
|
+
try {
|
|
72
|
+
const archivePath = join(workDir, `archive.${archiveExt}`);
|
|
73
|
+
const hash = createHash("sha256");
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for await (const chunk of response.body) {
|
|
76
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
77
|
+
hash.update(buf);
|
|
78
|
+
chunks.push(buf);
|
|
79
|
+
}
|
|
80
|
+
const data = Buffer.concat(chunks);
|
|
81
|
+
if (expectedSha256) {
|
|
82
|
+
const actualSha256 = hash.digest("hex");
|
|
83
|
+
if (actualSha256 !== expectedSha256) {
|
|
84
|
+
throw new Error(`checksum mismatch downloading LilyPond ${version} from ${url}\n` +
|
|
85
|
+
` expected ${expectedSha256}\n got ${actualSha256}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
log("astro-lilypond: download response had no checksum header — skipping integrity verification");
|
|
90
|
+
}
|
|
91
|
+
await writeFile(archivePath, data);
|
|
92
|
+
const extractDir = join(workDir, "extract");
|
|
93
|
+
await mkdir(extractDir, { recursive: true });
|
|
94
|
+
await extractArchive(archivePath, extractDir, archiveExt);
|
|
95
|
+
// The archive contains a single top-level `lilypond-<version>/` dir.
|
|
96
|
+
const [extractedRoot] = await readdir(extractDir);
|
|
97
|
+
if (!extractedRoot) {
|
|
98
|
+
throw new Error(`downloaded archive for LilyPond ${version} was empty`);
|
|
99
|
+
}
|
|
100
|
+
const extractedPath = join(extractDir, extractedRoot);
|
|
101
|
+
// Writing the marker inside the extracted tree makes the rename below
|
|
102
|
+
// move the binary and its "installed" marker into place atomically
|
|
103
|
+
// together, so a process killed mid-install can't leave a good install
|
|
104
|
+
// looking uninstalled.
|
|
105
|
+
await writeFile(join(extractedPath, ".complete"), "");
|
|
106
|
+
const reuseConcurrentInstall = async () => {
|
|
107
|
+
if (!(await pathExists(markerPath)))
|
|
108
|
+
return false;
|
|
109
|
+
log(`astro-lilypond: LilyPond ${version} was installed concurrently by another process; reusing it`);
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
if (await reuseConcurrentInstall())
|
|
113
|
+
return binaryPath;
|
|
114
|
+
try {
|
|
115
|
+
await rm(installDir, { recursive: true, force: true });
|
|
116
|
+
await rename(extractedPath, installDir);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (await reuseConcurrentInstall())
|
|
120
|
+
return binaryPath;
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
log(`astro-lilypond: installed LilyPond ${version} to ${installDir}`);
|
|
124
|
+
return binaryPath;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await rm(workDir, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type AutoInstallOptions, resolveAutoInstallOption, } from "./autoInstall.js";
|
|
2
|
+
export { lilypondCacheDir } from "./cacheDir.js";
|
|
3
|
+
export { downloadLilypond } from "./downloadLilypond.js";
|
|
4
|
+
export { resolvePlatformTarget } from "./platformTarget.js";
|
|
5
|
+
export { DEFAULT_LILYPOND_VERSION, resolveLilypondBinary, } from "./resolveLilypondBinary.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { resolveAutoInstallOption, } from "./autoInstall.js";
|
|
2
|
+
export { lilypondCacheDir } from "./cacheDir.js";
|
|
3
|
+
export { downloadLilypond } from "./downloadLilypond.js";
|
|
4
|
+
export { resolvePlatformTarget } from "./platformTarget.js";
|
|
5
|
+
export { DEFAULT_LILYPOND_VERSION, resolveLilypondBinary, } from "./resolveLilypondBinary.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A platform/arch combination LilyPond publishes a prebuilt archive for,
|
|
3
|
+
* per https://gitlab.com/lilypond/lilypond/-/releases.
|
|
4
|
+
*/
|
|
5
|
+
export interface PlatformTarget {
|
|
6
|
+
/** LilyPond's own platform token, as used in its release filenames. */
|
|
7
|
+
platform: "linux" | "darwin" | "mingw";
|
|
8
|
+
/** LilyPond's own arch token, as used in its release filenames. */
|
|
9
|
+
arch: "x86_64" | "arm64";
|
|
10
|
+
archiveExt: "tar.gz" | "zip";
|
|
11
|
+
/** Name of the `lilypond` executable inside the archive's `bin/` dir. */
|
|
12
|
+
binaryName: "lilypond" | "lilypond.exe";
|
|
13
|
+
}
|
|
14
|
+
export declare function resolvePlatformTarget(platform?: NodeJS.Platform, arch?: string): PlatformTarget | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function resolvePlatformTarget(platform = process.platform, arch = process.arch) {
|
|
2
|
+
if (platform === "linux" && arch === "x64") {
|
|
3
|
+
return {
|
|
4
|
+
platform: "linux",
|
|
5
|
+
arch: "x86_64",
|
|
6
|
+
archiveExt: "tar.gz",
|
|
7
|
+
binaryName: "lilypond",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
11
|
+
return {
|
|
12
|
+
platform: "darwin",
|
|
13
|
+
arch: "arm64",
|
|
14
|
+
archiveExt: "tar.gz",
|
|
15
|
+
binaryName: "lilypond",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (platform === "darwin" && arch === "x64") {
|
|
19
|
+
return {
|
|
20
|
+
platform: "darwin",
|
|
21
|
+
arch: "x86_64",
|
|
22
|
+
archiveExt: "tar.gz",
|
|
23
|
+
binaryName: "lilypond",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (platform === "win32" && arch === "x64") {
|
|
27
|
+
return {
|
|
28
|
+
platform: "mingw",
|
|
29
|
+
arch: "x86_64",
|
|
30
|
+
archiveExt: "zip",
|
|
31
|
+
binaryName: "lilypond.exe",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LilypondVersion } from "../types/lilypondVersion.js";
|
|
2
|
+
export declare const DEFAULT_LILYPOND_VERSION: LilypondVersion;
|
|
3
|
+
export interface ResolveLilypondBinaryOptions {
|
|
4
|
+
version?: LilypondVersion;
|
|
5
|
+
autoInstall: boolean;
|
|
6
|
+
log?: (message: string) => void;
|
|
7
|
+
warn?: (message: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveLilypondBinary({ version, autoInstall, log, warn, }: ResolveLilypondBinaryOptions): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { downloadLilypond } from "./downloadLilypond.js";
|
|
4
|
+
import { resolvePlatformTarget } from "./platformTarget.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export const DEFAULT_LILYPOND_VERSION = "2.26.0";
|
|
7
|
+
const IS_ON_PATH_TIMEOUT_MS = 10_000;
|
|
8
|
+
async function isOnPath() {
|
|
9
|
+
try {
|
|
10
|
+
await execFileAsync("lilypond", ["--version"], {
|
|
11
|
+
signal: AbortSignal.timeout(IS_ON_PATH_TIMEOUT_MS),
|
|
12
|
+
});
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const code = err?.code;
|
|
17
|
+
return code !== "ENOENT";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function resolveLilypondBinary({ version = DEFAULT_LILYPOND_VERSION, autoInstall, log = () => { }, warn = () => { }, }) {
|
|
21
|
+
if (await isOnPath())
|
|
22
|
+
return "lilypond";
|
|
23
|
+
if (!autoInstall) {
|
|
24
|
+
warn("astro-lilypond: `lilypond` binary not found on PATH — LilyPond blocks will render as errors. " +
|
|
25
|
+
"Install LilyPond and ensure it is on PATH, or enable the `autoInstall` option (on by default).");
|
|
26
|
+
return "lilypond";
|
|
27
|
+
}
|
|
28
|
+
if (!resolvePlatformTarget()) {
|
|
29
|
+
warn(`astro-lilypond: \`lilypond\` binary not found on PATH, and no prebuilt LilyPond ${version} ` +
|
|
30
|
+
`is available for ${process.platform}/${process.arch}. Install LilyPond manually and ensure it is on PATH.`);
|
|
31
|
+
return "lilypond";
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const binaryPath = await downloadLilypond({ version, log });
|
|
35
|
+
return binaryPath ?? "lilypond";
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new Error(`astro-lilypond: automatic LilyPond install failed — ${err instanceof Error ? err.message : String(err)}. ` +
|
|
39
|
+
"Install LilyPond manually and ensure it is on PATH, or set `autoInstall: false` to require a PATH install without attempting a download.", { cause: err });
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
|
+
import { type AutoInstallOptions } from "./binary/index.js";
|
|
2
3
|
import { type PluginOptions } from "./plugins/index.js";
|
|
3
4
|
import { type LilypondDefaults } from "./render.js";
|
|
4
5
|
export declare const LY_EXTENSIONS: readonly [".ly", ".lilypond", ".ily"];
|
|
5
|
-
export type { LilypondDefaults, PluginOptions as LilypondPluginOptions };
|
|
6
|
+
export type { AutoInstallOptions, LilypondDefaults, PluginOptions as LilypondPluginOptions, };
|
|
6
7
|
export interface LilypondPage {
|
|
7
8
|
src: string;
|
|
8
9
|
width?: number;
|
|
@@ -29,5 +30,13 @@ export interface LilypondOptions extends PluginOptions {
|
|
|
29
30
|
* @default 60000
|
|
30
31
|
*/
|
|
31
32
|
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* 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.
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
autoInstall?: boolean | AutoInstallOptions;
|
|
32
41
|
}
|
|
33
42
|
export default function lilypond(options?: LilypondOptions): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
1
|
import emitAssetIntegration from "astro-emit-asset";
|
|
2
|
+
import { resolveAutoInstallOption, resolveLilypondBinary, } from "./binary/index.js";
|
|
4
3
|
import { rehypePlugin, remarkPlugin, satteriPlugin, } from "./plugins/index.js";
|
|
5
4
|
import { defaultOptions, render, resolveCrop, } from "./render.js";
|
|
6
5
|
import { altTextFor, emitLilypondAsset, includePathsFor, lyTypeDeclarationsFor, parseLyHeader, parseLyImportQuery, prependVersion, RECOGNIZED_QUERY_PARAMS, resolveDefaults, sourceNameFor, titleFor, } from "./utils/index.js";
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
6
|
export const LY_EXTENSIONS = [".ly", ".lilypond", ".ily"];
|
|
9
7
|
function lyFilePlugin(options) {
|
|
10
8
|
return {
|
|
@@ -32,11 +30,13 @@ function lyFilePlugin(options) {
|
|
|
32
30
|
resolution,
|
|
33
31
|
crop,
|
|
34
32
|
sizeScale: crop ? cropScale : 1,
|
|
33
|
+
binaryPath: options.binaryPath,
|
|
35
34
|
render: () => render(src, {
|
|
36
35
|
format,
|
|
37
36
|
crop,
|
|
38
37
|
defaults: options.defaults,
|
|
39
38
|
timeout: options.timeout,
|
|
39
|
+
binaryPath: options.binaryPath,
|
|
40
40
|
includePaths,
|
|
41
41
|
sourceName,
|
|
42
42
|
}),
|
|
@@ -56,10 +56,10 @@ export default function lilypond(options = {}) {
|
|
|
56
56
|
name: "astro-lilypond",
|
|
57
57
|
hooks: {
|
|
58
58
|
"astro:config:setup": async ({ config, updateConfig, logger }) => {
|
|
59
|
-
await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
options.binaryPath = await resolveLilypondBinary({
|
|
60
|
+
...resolveAutoInstallOption(options.autoInstall),
|
|
61
|
+
log: (message) => logger?.info(message),
|
|
62
|
+
warn: (message) => logger?.warn(message),
|
|
63
63
|
});
|
|
64
64
|
updateConfig({
|
|
65
65
|
integrations: [emitAssetIntegration()],
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Loader } from "astro/loaders";
|
|
2
2
|
import { z } from "astro/zod";
|
|
3
|
+
import { type AutoInstallOptions } from "./binary/index.js";
|
|
3
4
|
import { type LilypondContent } from "./index.js";
|
|
4
5
|
import type { PluginOptions } from "./plugins/index.js";
|
|
5
6
|
import { type KnownLyHeaderFields } from "./utils/index.js";
|
|
@@ -27,6 +28,14 @@ export interface LilypondLoaderOptions extends PluginOptions {
|
|
|
27
28
|
* @default the file's path relative to `base`, POSIX-separated, with its extension stripped.
|
|
28
29
|
*/
|
|
29
30
|
generateId?: (options: GenerateIdOptions) => string;
|
|
31
|
+
/**
|
|
32
|
+
* When no `lilypond` binary is found on `PATH`, download a matching
|
|
33
|
+
* prebuilt release into a local cache and use that instead. Set to
|
|
34
|
+
* `false` to only ever use a `PATH` install, or pass an object to pick
|
|
35
|
+
* which version gets downloaded.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
autoInstall?: boolean | AutoInstallOptions;
|
|
30
39
|
}
|
|
31
40
|
export interface LilypondHeaderData extends KnownLyHeaderFields {
|
|
32
41
|
/** Header fields outside LilyPond's standard set (e.g. `mutopiacomposer`). */
|
package/dist/loader.js
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
5
6
|
import { LY_EXTENSIONS } from "./index.js";
|
|
6
7
|
import { defaultOptions, render, resolveCrop } from "./render.js";
|
|
7
8
|
import { altTextFor, emitLilypondAsset, includePathsFor, parseLyHeaderFields, prependVersion, resolveDefaults, sourceNameFor, splitHeaderFields, titleFor, } from "./utils/index.js";
|
|
@@ -54,7 +55,7 @@ function posixRelative(from, to) {
|
|
|
54
55
|
* Content Loader to turn a directory of `.ly` files into a content collection.
|
|
55
56
|
*/
|
|
56
57
|
export function lilypondLoader(options) {
|
|
57
|
-
const { pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId, format, defaults, timeout, } = options;
|
|
58
|
+
const { pattern = DEFAULT_PATTERN, base, generateId = defaultGenerateId, format, defaults, timeout, autoInstall, } = options;
|
|
58
59
|
const resolved = resolveDefaults(defaults);
|
|
59
60
|
const crop = resolveCrop(resolved.crop, "component");
|
|
60
61
|
const resolvedFormat = format ?? defaultOptions.format;
|
|
@@ -66,6 +67,11 @@ export function lilypondLoader(options) {
|
|
|
66
67
|
const rootDir = fileURLToPath(config.root);
|
|
67
68
|
const baseUrl = resolveBaseUrl(base, config.root);
|
|
68
69
|
const baseDir = fileURLToPath(baseUrl);
|
|
70
|
+
const binaryPath = await resolveLilypondBinary({
|
|
71
|
+
...resolveAutoInstallOption(autoInstall),
|
|
72
|
+
log: (message) => logger.info(message),
|
|
73
|
+
warn: (message) => logger.warn(message),
|
|
74
|
+
});
|
|
69
75
|
async function syncEntry(entry) {
|
|
70
76
|
const filePath = join(baseDir, entry);
|
|
71
77
|
let source;
|
|
@@ -90,11 +96,13 @@ export function lilypondLoader(options) {
|
|
|
90
96
|
resolution: resolved.resolution,
|
|
91
97
|
crop,
|
|
92
98
|
sizeScale: crop ? resolved.cropScale : 1,
|
|
99
|
+
binaryPath,
|
|
93
100
|
render: () => render(src, {
|
|
94
101
|
format: resolvedFormat,
|
|
95
102
|
crop,
|
|
96
103
|
defaults,
|
|
97
104
|
timeout,
|
|
105
|
+
binaryPath,
|
|
98
106
|
includePaths,
|
|
99
107
|
sourceName,
|
|
100
108
|
}),
|
package/dist/plugins/rehype.js
CHANGED
|
@@ -42,11 +42,13 @@ export function rehypePlugin(options) {
|
|
|
42
42
|
resolution,
|
|
43
43
|
crop,
|
|
44
44
|
sizeScale: crop ? cropScale : 1,
|
|
45
|
+
binaryPath: options.binaryPath,
|
|
45
46
|
render: () => render(source, {
|
|
46
47
|
format,
|
|
47
48
|
crop,
|
|
48
49
|
defaults: options.defaults,
|
|
49
50
|
timeout: options.timeout,
|
|
51
|
+
binaryPath: options.binaryPath,
|
|
50
52
|
includePaths,
|
|
51
53
|
sourceName,
|
|
52
54
|
}),
|
package/dist/plugins/remark.js
CHANGED
|
@@ -22,11 +22,13 @@ export const remarkPlugin = (options) => {
|
|
|
22
22
|
resolution,
|
|
23
23
|
crop,
|
|
24
24
|
sizeScale: crop ? cropScale : 1,
|
|
25
|
+
binaryPath: options.binaryPath,
|
|
25
26
|
render: () => render(source, {
|
|
26
27
|
format,
|
|
27
28
|
crop,
|
|
28
29
|
defaults: options.defaults,
|
|
29
30
|
timeout: options.timeout,
|
|
31
|
+
binaryPath: options.binaryPath,
|
|
30
32
|
includePaths,
|
|
31
33
|
sourceName,
|
|
32
34
|
}),
|
package/dist/plugins/satteri.js
CHANGED
|
@@ -24,11 +24,13 @@ export function satteriPlugin(options) {
|
|
|
24
24
|
resolution,
|
|
25
25
|
crop,
|
|
26
26
|
sizeScale: crop ? cropScale : 1,
|
|
27
|
+
binaryPath: options.binaryPath,
|
|
27
28
|
render: () => render(source, {
|
|
28
29
|
format,
|
|
29
30
|
crop,
|
|
30
31
|
defaults: options.defaults,
|
|
31
32
|
timeout: options.timeout,
|
|
33
|
+
binaryPath: options.binaryPath,
|
|
32
34
|
includePaths,
|
|
33
35
|
sourceName,
|
|
34
36
|
}),
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { LyHeaderFields } from "./parseLyHeader.js";
|
|
2
2
|
/**
|
|
3
|
-
* Composes alt text from parsed `\header` fields
|
|
4
|
-
* when both are present, just the title or `"Sheet music by {composer}"` when
|
|
5
|
-
* only one is, and `""` (decorative) when neither is present.
|
|
3
|
+
* Composes alt text from parsed `\header` fields.
|
|
6
4
|
*/
|
|
7
5
|
export declare function altTextFor(fields: LyHeaderFields): string;
|
package/dist/utils/altTextFor.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composes alt text from parsed `\header` fields
|
|
3
|
-
* when both are present, just the title or `"Sheet music by {composer}"` when
|
|
4
|
-
* only one is, and `""` (decorative) when neither is present.
|
|
2
|
+
* Composes alt text from parsed `\header` fields.
|
|
5
3
|
*/
|
|
6
4
|
export function altTextFor(fields) {
|
|
7
5
|
const { title, composer } = fields;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { altTextFor } from "./altTextFor.js";
|
|
2
2
|
import { parseFenceMeta } from "./parseFenceMeta.js";
|
|
3
3
|
import { parseLyHeader } from "./parseLyHeader.js";
|
|
4
|
-
/** Alt text for a fenced code block: a meta `alt="..."` override, else derived from the block's `\header` title/composer. */
|
|
5
4
|
export function altTextForBlock(meta, source) {
|
|
6
5
|
return parseFenceMeta(meta) ?? altTextFor(parseLyHeader(source));
|
|
7
6
|
}
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
import type { LilypondPage } from "../index.js";
|
|
2
2
|
import type { Format } from "../render.js";
|
|
3
3
|
export interface EmitLilypondAssetOptions {
|
|
4
|
-
/**
|
|
5
|
-
* Human-readable title — becomes the emitted file's name, with the hash
|
|
6
|
-
* `emitAsset()` derives inserted after it (`<title>.<hash>.<format>`).
|
|
7
|
-
*/
|
|
8
4
|
title: string;
|
|
9
|
-
/** Output format — also used as the file extension. */
|
|
10
5
|
format: Format;
|
|
11
|
-
/** The exact LilyPond source rendered, so a change busts the cache. */
|
|
12
6
|
source: string;
|
|
13
|
-
/** Resolution in DPI (PNG only) — a render dependency, so part of the cache key. */
|
|
14
7
|
resolution: number;
|
|
15
|
-
/** Whether output is cropped — a render dependency, so part of the cache key. */
|
|
16
8
|
crop: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Multiplies the `width`/`height` reported on each page. The generated
|
|
19
|
-
* bytes are never touched — this only affects the dimensions handed back
|
|
20
|
-
* for sizing the `<img>` tag.
|
|
21
|
-
*/
|
|
22
9
|
sizeScale: number;
|
|
23
|
-
/**
|
|
24
|
-
* Produces the bytes to render, one Buffer per page. Only invoked on a
|
|
25
|
-
* cache miss.
|
|
26
|
-
*/
|
|
27
10
|
render: () => Promise<Buffer[]>;
|
|
11
|
+
binaryPath: string | undefined;
|
|
28
12
|
}
|
|
29
13
|
export declare function emitLilypondAsset(options: EmitLilypondAssetOptions): Promise<LilypondPage[]>;
|
|
@@ -2,9 +2,5 @@ import type { vi } from "vitest";
|
|
|
2
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
|
-
/**
|
|
6
|
-
* Fake that just awaits `opts.render()` and discards the result — for tests
|
|
7
|
-
* asserting that a `render()` rejection propagates out of the caller.
|
|
8
|
-
*/
|
|
9
5
|
export declare function fakeEmitLilypondAssetPropagatingRenderErrors(mock: EmitLilypondAssetMock): void;
|
|
10
6
|
export {};
|
|
@@ -6,10 +6,6 @@ export function fakeEmitLilypondAsset(mock, urlBase = "/_astro") {
|
|
|
6
6
|
}));
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Fake that just awaits `opts.render()` and discards the result — for tests
|
|
11
|
-
* asserting that a `render()` rejection propagates out of the caller.
|
|
12
|
-
*/
|
|
13
9
|
export function fakeEmitLilypondAssetPropagatingRenderErrors(mock) {
|
|
14
10
|
mock.mockImplementation(async (opts) => {
|
|
15
11
|
await opts.render();
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { emitAsset } from "astro-emit-asset/emit";
|
|
2
2
|
import { imageDimensionsFor } from "./imageDimensions.js";
|
|
3
|
-
const EMIT_ASSET_GLOBAL_KEY = "astro-emit-asset";
|
|
4
|
-
function assertEmitAssetRegistered() {
|
|
5
|
-
if (globalThis[EMIT_ASSET_GLOBAL_KEY])
|
|
6
|
-
return;
|
|
7
|
-
throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
|
|
8
|
-
}
|
|
9
3
|
export async function emitLilypondAsset(options) {
|
|
10
|
-
|
|
4
|
+
if (!options.binaryPath) {
|
|
5
|
+
throw new Error("astro-lilypond: please add the `lilypond()` integration to your Astro config.");
|
|
6
|
+
}
|
|
11
7
|
const { title, format, source, resolution, crop, sizeScale, render } = options;
|
|
12
|
-
const
|
|
8
|
+
const assets = await emitAsset(`${title}.[hash].${format}`, [source, format, resolution, crop, sizeScale], async () => {
|
|
13
9
|
const buffers = await render();
|
|
14
|
-
|
|
10
|
+
return buffers.map((data) => {
|
|
15
11
|
const dimensions = imageDimensionsFor(format, data);
|
|
16
12
|
return {
|
|
17
13
|
data,
|
|
@@ -21,14 +17,7 @@ export async function emitLilypondAsset(options) {
|
|
|
21
17
|
},
|
|
22
18
|
};
|
|
23
19
|
});
|
|
24
|
-
// Returning a 1-element array (instead of the bare object) makes
|
|
25
|
-
// astro-emit-asset insert a page index into the filename even for a
|
|
26
|
-
// single-page score (`title.0.hash.ext`) — return the object
|
|
27
|
-
// directly for that (overwhelmingly common) case so the file is
|
|
28
|
-
// named `title.hash.ext` instead.
|
|
29
|
-
return pages.length === 1 ? pages[0] : pages;
|
|
30
20
|
});
|
|
31
|
-
const assets = Array.isArray(result) ? result : [result];
|
|
32
21
|
return assets.map((asset) => ({
|
|
33
22
|
src: asset.src,
|
|
34
23
|
width: asset.meta.width,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lilypond",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"astro": ">=7.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"astro-emit-asset": "^0.1.
|
|
51
|
+
"astro-emit-asset": "^0.1.4",
|
|
52
52
|
"unist-util-visit": "^5.1.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|