@mzebley/mark-down-cli 1.0.0 → 1.2.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 +17 -2
- package/dist/index.cjs +103 -1
- package/dist/index.js +103 -1
- package/package.json +2 -1
- package/src/compile-page.ts +116 -0
- package/src/index.ts +20 -1
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ npx @mzebley/mark-down-cli build content/snippets
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
mark-down
|
|
35
|
+
mark-down <command> [options]
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
The CLI walks the directory tree, gathers front matter, and writes `snippets-index.json` alongside your Markdown files by default.
|
|
38
|
+
The CLI walks the directory tree, gathers front matter, and writes `snippets-index.json` alongside your Markdown files by default. It can also pre-render HTML files that already contain `data-snippet` placeholders.
|
|
39
39
|
|
|
40
40
|
## Commands
|
|
41
41
|
|
|
@@ -53,12 +53,27 @@ The CLI walks the directory tree, gathers front matter, and writes `snippets-ind
|
|
|
53
53
|
- Logs progress with the familiar `[mark↓]` prefix.
|
|
54
54
|
- Accepts the same options as `build`.
|
|
55
55
|
|
|
56
|
+
### `mark-down compile-page <inputHtml>`
|
|
57
|
+
|
|
58
|
+
- Reads an HTML file that contains elements with `data-snippet` attributes.
|
|
59
|
+
- Resolves snippet metadata from `snippets-index.json` (auto-detected next to the HTML file or provided via `--manifest`).
|
|
60
|
+
- Loads Markdown from disk, strips front matter, and renders HTML with the same `marked` pipeline as the runtime.
|
|
61
|
+
- Injects the rendered HTML as the element `innerHTML` and writes the result to `dist/<file>.html` by default.
|
|
62
|
+
- Use `--outDir` to change the output directory or `--inPlace` to overwrite the source file.
|
|
63
|
+
- Unknown slugs are left untouched and logged as warnings. Table-of-contents generation remains a runtime concern.
|
|
64
|
+
|
|
56
65
|
## Configuration options
|
|
57
66
|
|
|
58
67
|
The CLI stays intentionally small so it can be composed inside any toolchain. Currently supported flags:
|
|
59
68
|
|
|
60
69
|
- `-o, --output <path>` – write the manifest to a custom file instead of `<sourceDir>/snippets-index.json`.
|
|
61
70
|
|
|
71
|
+
### `compile-page` options
|
|
72
|
+
|
|
73
|
+
- `--manifest <path>` – path to `snippets-index.json`. Defaults to the file next to `<inputHtml>`.
|
|
74
|
+
- `--outDir <dir>` – output directory for compiled HTML. Defaults to `dist`.
|
|
75
|
+
- `--inPlace` – overwrite the input HTML file instead of writing to `dist/`.
|
|
76
|
+
|
|
62
77
|
Add flags directly after the command (`mark-down build content/snippets -o public/snippets-index.json`). Package scripts can capture these options as well.
|
|
63
78
|
|
|
64
79
|
## Watching for changes
|
package/dist/index.cjs
CHANGED
|
@@ -231,9 +231,100 @@ function debounce(fn, delay) {
|
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// src/compile-page.ts
|
|
235
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
236
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
237
|
+
var import_cheerio = require("cheerio");
|
|
238
|
+
var import_mark_down2 = require("@mzebley/mark-down");
|
|
239
|
+
var DEFAULT_OUT_DIR = "dist";
|
|
240
|
+
async function compilePage(inputHtml, options = {}) {
|
|
241
|
+
const sourcePath = import_node_path3.default.resolve(inputHtml);
|
|
242
|
+
await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
|
|
243
|
+
const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
|
|
244
|
+
const manifestDir = import_node_path3.default.dirname(manifestPath);
|
|
245
|
+
const manifest = await loadManifest(manifestPath);
|
|
246
|
+
const rawHtml = await import_promises2.default.readFile(sourcePath, "utf8");
|
|
247
|
+
const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\s*)/i);
|
|
248
|
+
const doctype = doctypeMatch?.[1] ?? "";
|
|
249
|
+
const dom = (0, import_cheerio.load)(rawHtml, { decodeEntities: false });
|
|
250
|
+
const targets = dom("[data-snippet]").toArray();
|
|
251
|
+
for (const node of targets) {
|
|
252
|
+
const element = dom(node);
|
|
253
|
+
const slug = element.attr("data-snippet");
|
|
254
|
+
if (!slug) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const entry = manifest.find((item) => item.slug === slug);
|
|
258
|
+
if (!entry) {
|
|
259
|
+
console.warn(`mark\u2193: no snippet found for "${slug}"`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const snippetPath = import_node_path3.default.resolve(manifestDir, entry.path);
|
|
263
|
+
let raw;
|
|
264
|
+
try {
|
|
265
|
+
raw = await import_promises2.default.readFile(snippetPath, "utf8");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.warn(`mark\u2193: failed to read snippet at '${entry.path}'`, error);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
let body = raw;
|
|
271
|
+
let frontMatterSlug;
|
|
272
|
+
try {
|
|
273
|
+
const frontMatter = (0, import_mark_down2.parseFrontMatter)(raw);
|
|
274
|
+
body = frontMatter.content;
|
|
275
|
+
frontMatterSlug = frontMatter.slug;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn(`mark\u2193: failed to parse front matter for '${entry.path}'`, error);
|
|
278
|
+
}
|
|
279
|
+
const html = (0, import_mark_down2.renderMarkdown)(body);
|
|
280
|
+
element.html(html);
|
|
281
|
+
if (!element.attr("id")) {
|
|
282
|
+
element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const outputDir = options.inPlace ? import_node_path3.default.dirname(sourcePath) : import_node_path3.default.resolve(options.outDir ?? DEFAULT_OUT_DIR);
|
|
286
|
+
if (!options.inPlace) {
|
|
287
|
+
await import_promises2.default.mkdir(outputDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
const outputPath = options.inPlace ? sourcePath : import_node_path3.default.join(outputDir, import_node_path3.default.basename(sourcePath));
|
|
290
|
+
const outputHtml = `${doctype}${dom.html() ?? ""}`;
|
|
291
|
+
await import_promises2.default.writeFile(outputPath, outputHtml);
|
|
292
|
+
logEvent("info", "compile_page.written", { outputPath });
|
|
293
|
+
return outputPath;
|
|
294
|
+
}
|
|
295
|
+
async function resolveManifestPath(inputHtml, manifestFlag) {
|
|
296
|
+
const manifestPath = manifestFlag ? import_node_path3.default.resolve(manifestFlag) : import_node_path3.default.join(import_node_path3.default.dirname(import_node_path3.default.resolve(inputHtml)), "snippets-index.json");
|
|
297
|
+
await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
|
|
298
|
+
return manifestPath;
|
|
299
|
+
}
|
|
300
|
+
async function loadManifest(manifestPath) {
|
|
301
|
+
let raw;
|
|
302
|
+
try {
|
|
303
|
+
raw = await import_promises2.default.readFile(manifestPath, "utf8");
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(raw);
|
|
309
|
+
if (!Array.isArray(parsed)) {
|
|
310
|
+
throw new Error("Manifest must be a JSON array.");
|
|
311
|
+
}
|
|
312
|
+
return parsed;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function assertExists(target, message) {
|
|
318
|
+
try {
|
|
319
|
+
await import_promises2.default.access(target);
|
|
320
|
+
} catch {
|
|
321
|
+
throw new Error(message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
234
325
|
// src/index.ts
|
|
235
326
|
var program = new import_commander.Command();
|
|
236
|
-
program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("
|
|
327
|
+
program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.0");
|
|
237
328
|
program.command("build").argument("[sourceDir]", "directory containing snippets", "content/snippets").option("-o, --output <path>", "where to write snippets-index.json").action(async (sourceDir, options) => {
|
|
238
329
|
try {
|
|
239
330
|
const result = await buildManifestFile({ sourceDir, outputPath: options.output });
|
|
@@ -252,6 +343,17 @@ program.command("watch").argument("[sourceDir]", "directory containing snippets"
|
|
|
252
343
|
handleError(error);
|
|
253
344
|
}
|
|
254
345
|
});
|
|
346
|
+
program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option("--inPlace", "overwrite the input HTML file instead of writing to outDir").action(async (inputHtml, options) => {
|
|
347
|
+
try {
|
|
348
|
+
await compilePage(inputHtml, {
|
|
349
|
+
manifest: options.manifest,
|
|
350
|
+
outDir: options.outDir,
|
|
351
|
+
inPlace: options.inPlace
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
handleError(error);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
255
357
|
program.parseAsync(process.argv).catch(handleError);
|
|
256
358
|
function handleError(error) {
|
|
257
359
|
const err = error;
|
package/dist/index.js
CHANGED
|
@@ -208,9 +208,100 @@ function debounce(fn, delay) {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// src/compile-page.ts
|
|
212
|
+
import fs2 from "fs/promises";
|
|
213
|
+
import path3 from "path";
|
|
214
|
+
import { load as loadHtml } from "cheerio";
|
|
215
|
+
import { parseFrontMatter, renderMarkdown } from "@mzebley/mark-down";
|
|
216
|
+
var DEFAULT_OUT_DIR = "dist";
|
|
217
|
+
async function compilePage(inputHtml, options = {}) {
|
|
218
|
+
const sourcePath = path3.resolve(inputHtml);
|
|
219
|
+
await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
|
|
220
|
+
const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
|
|
221
|
+
const manifestDir = path3.dirname(manifestPath);
|
|
222
|
+
const manifest = await loadManifest(manifestPath);
|
|
223
|
+
const rawHtml = await fs2.readFile(sourcePath, "utf8");
|
|
224
|
+
const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\s*)/i);
|
|
225
|
+
const doctype = doctypeMatch?.[1] ?? "";
|
|
226
|
+
const dom = loadHtml(rawHtml, { decodeEntities: false });
|
|
227
|
+
const targets = dom("[data-snippet]").toArray();
|
|
228
|
+
for (const node of targets) {
|
|
229
|
+
const element = dom(node);
|
|
230
|
+
const slug = element.attr("data-snippet");
|
|
231
|
+
if (!slug) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const entry = manifest.find((item) => item.slug === slug);
|
|
235
|
+
if (!entry) {
|
|
236
|
+
console.warn(`mark\u2193: no snippet found for "${slug}"`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const snippetPath = path3.resolve(manifestDir, entry.path);
|
|
240
|
+
let raw;
|
|
241
|
+
try {
|
|
242
|
+
raw = await fs2.readFile(snippetPath, "utf8");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.warn(`mark\u2193: failed to read snippet at '${entry.path}'`, error);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let body = raw;
|
|
248
|
+
let frontMatterSlug;
|
|
249
|
+
try {
|
|
250
|
+
const frontMatter = parseFrontMatter(raw);
|
|
251
|
+
body = frontMatter.content;
|
|
252
|
+
frontMatterSlug = frontMatter.slug;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.warn(`mark\u2193: failed to parse front matter for '${entry.path}'`, error);
|
|
255
|
+
}
|
|
256
|
+
const html = renderMarkdown(body);
|
|
257
|
+
element.html(html);
|
|
258
|
+
if (!element.attr("id")) {
|
|
259
|
+
element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const outputDir = options.inPlace ? path3.dirname(sourcePath) : path3.resolve(options.outDir ?? DEFAULT_OUT_DIR);
|
|
263
|
+
if (!options.inPlace) {
|
|
264
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
265
|
+
}
|
|
266
|
+
const outputPath = options.inPlace ? sourcePath : path3.join(outputDir, path3.basename(sourcePath));
|
|
267
|
+
const outputHtml = `${doctype}${dom.html() ?? ""}`;
|
|
268
|
+
await fs2.writeFile(outputPath, outputHtml);
|
|
269
|
+
logEvent("info", "compile_page.written", { outputPath });
|
|
270
|
+
return outputPath;
|
|
271
|
+
}
|
|
272
|
+
async function resolveManifestPath(inputHtml, manifestFlag) {
|
|
273
|
+
const manifestPath = manifestFlag ? path3.resolve(manifestFlag) : path3.join(path3.dirname(path3.resolve(inputHtml)), "snippets-index.json");
|
|
274
|
+
await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
|
|
275
|
+
return manifestPath;
|
|
276
|
+
}
|
|
277
|
+
async function loadManifest(manifestPath) {
|
|
278
|
+
let raw;
|
|
279
|
+
try {
|
|
280
|
+
raw = await fs2.readFile(manifestPath, "utf8");
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
if (!Array.isArray(parsed)) {
|
|
287
|
+
throw new Error("Manifest must be a JSON array.");
|
|
288
|
+
}
|
|
289
|
+
return parsed;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function assertExists(target, message) {
|
|
295
|
+
try {
|
|
296
|
+
await fs2.access(target);
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
211
302
|
// src/index.ts
|
|
212
303
|
var program = new Command();
|
|
213
|
-
program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("
|
|
304
|
+
program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.0");
|
|
214
305
|
program.command("build").argument("[sourceDir]", "directory containing snippets", "content/snippets").option("-o, --output <path>", "where to write snippets-index.json").action(async (sourceDir, options) => {
|
|
215
306
|
try {
|
|
216
307
|
const result = await buildManifestFile({ sourceDir, outputPath: options.output });
|
|
@@ -229,6 +320,17 @@ program.command("watch").argument("[sourceDir]", "directory containing snippets"
|
|
|
229
320
|
handleError(error);
|
|
230
321
|
}
|
|
231
322
|
});
|
|
323
|
+
program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option("--inPlace", "overwrite the input HTML file instead of writing to outDir").action(async (inputHtml, options) => {
|
|
324
|
+
try {
|
|
325
|
+
await compilePage(inputHtml, {
|
|
326
|
+
manifest: options.manifest,
|
|
327
|
+
outDir: options.outDir,
|
|
328
|
+
inPlace: options.inPlace
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
handleError(error);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
232
334
|
program.parseAsync(process.argv).catch(handleError);
|
|
233
335
|
function handleError(error) {
|
|
234
336
|
const err = error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzebley/mark-down-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "mark↓ CLI for building snippet manifests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@mzebley/mark-down": "file:../core",
|
|
21
|
+
"cheerio": "^1.0.0",
|
|
21
22
|
"chokidar": "^3.6.0",
|
|
22
23
|
"commander": "^11.1.0",
|
|
23
24
|
"fast-glob": "^3.3.2",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { load as loadHtml } from "cheerio";
|
|
4
|
+
import { parseFrontMatter, renderMarkdown, type SnippetMeta } from "@mzebley/mark-down";
|
|
5
|
+
import { logEvent } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
export interface CompilePageOptions {
|
|
8
|
+
manifest?: string;
|
|
9
|
+
outDir?: string;
|
|
10
|
+
inPlace?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OUT_DIR = "dist";
|
|
14
|
+
|
|
15
|
+
export async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {
|
|
16
|
+
const sourcePath = path.resolve(inputHtml);
|
|
17
|
+
await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
|
|
18
|
+
|
|
19
|
+
const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
|
|
20
|
+
const manifestDir = path.dirname(manifestPath);
|
|
21
|
+
const manifest = await loadManifest(manifestPath);
|
|
22
|
+
|
|
23
|
+
const rawHtml = await fs.readFile(sourcePath, "utf8");
|
|
24
|
+
const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\s*)/i);
|
|
25
|
+
const doctype = doctypeMatch?.[1] ?? "";
|
|
26
|
+
const dom = loadHtml(rawHtml, { decodeEntities: false });
|
|
27
|
+
|
|
28
|
+
const targets = dom("[data-snippet]").toArray();
|
|
29
|
+
for (const node of targets) {
|
|
30
|
+
const element = dom(node);
|
|
31
|
+
const slug = element.attr("data-snippet");
|
|
32
|
+
if (!slug) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const entry = manifest.find((item) => item.slug === slug);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
console.warn(`mark↓: no snippet found for "${slug}"`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const snippetPath = path.resolve(manifestDir, entry.path);
|
|
42
|
+
let raw: string;
|
|
43
|
+
try {
|
|
44
|
+
raw = await fs.readFile(snippetPath, "utf8");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let body = raw;
|
|
51
|
+
let frontMatterSlug: string | undefined;
|
|
52
|
+
try {
|
|
53
|
+
const frontMatter = parseFrontMatter(raw);
|
|
54
|
+
body = frontMatter.content;
|
|
55
|
+
frontMatterSlug = frontMatter.slug;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const html = renderMarkdown(body);
|
|
61
|
+
element.html(html);
|
|
62
|
+
|
|
63
|
+
if (!element.attr("id")) {
|
|
64
|
+
element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);
|
|
69
|
+
if (!options.inPlace) {
|
|
70
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
const outputPath = options.inPlace
|
|
73
|
+
? sourcePath
|
|
74
|
+
: path.join(outputDir, path.basename(sourcePath));
|
|
75
|
+
|
|
76
|
+
const outputHtml = `${doctype}${dom.html() ?? ""}`;
|
|
77
|
+
await fs.writeFile(outputPath, outputHtml);
|
|
78
|
+
|
|
79
|
+
logEvent("info", "compile_page.written", { outputPath });
|
|
80
|
+
return outputPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {
|
|
84
|
+
const manifestPath = manifestFlag
|
|
85
|
+
? path.resolve(manifestFlag)
|
|
86
|
+
: path.join(path.dirname(path.resolve(inputHtml)), "snippets-index.json");
|
|
87
|
+
await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
|
|
88
|
+
return manifestPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {
|
|
92
|
+
let raw: string;
|
|
93
|
+
try {
|
|
94
|
+
raw = await fs.readFile(manifestPath, "utf8");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!Array.isArray(parsed)) {
|
|
102
|
+
throw new Error("Manifest must be a JSON array.");
|
|
103
|
+
}
|
|
104
|
+
return parsed as SnippetMeta[];
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function assertExists(target: string, message: string) {
|
|
111
|
+
try {
|
|
112
|
+
await fs.access(target);
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(message);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,12 +4,13 @@ import { buildManifestFile } from "./manifest.js";
|
|
|
4
4
|
import { watch as watchSnippets } from "./watch.js";
|
|
5
5
|
import { brand, logEvent } from "./logger.js";
|
|
6
6
|
import { DuplicateSlugError } from "./errors.js";
|
|
7
|
+
import { compilePage } from "./compile-page.js";
|
|
7
8
|
|
|
8
9
|
const program = new Command();
|
|
9
10
|
program
|
|
10
11
|
.name("mark-down")
|
|
11
12
|
.description(`${brand} CLI for building snippet manifests`)
|
|
12
|
-
.version("
|
|
13
|
+
.version("1.2.0");
|
|
13
14
|
|
|
14
15
|
program
|
|
15
16
|
.command("build")
|
|
@@ -39,6 +40,24 @@ program
|
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
program
|
|
44
|
+
.command("compile-page")
|
|
45
|
+
.argument("<inputHtml>", "HTML file containing data-snippet placeholders")
|
|
46
|
+
.option("--manifest <path>", "path to snippets-index.json")
|
|
47
|
+
.option("--outDir <path>", "output directory for compiled HTML", "dist")
|
|
48
|
+
.option("--inPlace", "overwrite the input HTML file instead of writing to outDir")
|
|
49
|
+
.action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {
|
|
50
|
+
try {
|
|
51
|
+
await compilePage(inputHtml, {
|
|
52
|
+
manifest: options.manifest,
|
|
53
|
+
outDir: options.outDir,
|
|
54
|
+
inPlace: options.inPlace
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
handleError(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
42
61
|
program.parseAsync(process.argv).catch(handleError);
|
|
43
62
|
|
|
44
63
|
function handleError(error: unknown) {
|