@mzebley/mark-down-cli 1.0.0 → 1.2.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.
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 build <sourceDir> [options]
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("0.1.0");
327
+ program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.1");
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;
@@ -268,3 +370,4 @@ function handleError(error) {
268
370
  });
269
371
  process.exit(1);
270
372
  }
373
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath: options.output });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\"--inPlace\", \"overwrite the input HTML file instead of writing to outDir\")\n .action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack\n });\n process.exit(1);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {}\n }\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(options: BuildOptions): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target = options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA = typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB = typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(relativePath: string, frontMatter: Record<string, unknown>): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource = typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order: typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined\n };\n}\n\nfunction collectExtra(data: Record<string, unknown>): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\")\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50\n }\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(sourceDir: string, outputPath?: string): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(level: LogLevel, event: string, fields: LogFields = {}) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport { parseFrontMatter, renderMarkdown, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);\n }\n\n const html = renderMarkdown(body);\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["YAML","path","fs","fg","matter","import_node_path","path","chokidar","import_promises","import_node_path","import_mark_down","path","fs","loadHtml"]}
@@ -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("0.1.0");
304
+ program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.1");
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;
@@ -245,3 +347,4 @@ function handleError(error) {
245
347
  });
246
348
  process.exit(1);
247
349
  }
350
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath: options.output });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\"--inPlace\", \"overwrite the input HTML file instead of writing to outDir\")\n .action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack\n });\n process.exit(1);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {}\n }\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(options: BuildOptions): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target = options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA = typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB = typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(relativePath: string, frontMatter: Record<string, unknown>): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource = typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order: typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined\n };\n}\n\nfunction collectExtra(data: Record<string, unknown>): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\")\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50\n }\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(sourceDir: string, outputPath?: string): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(level: LogLevel, event: string, fields: LogFields = {}) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport { parseFrontMatter, renderMarkdown, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);\n }\n\n const html = renderMarkdown(body);\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["path","path","fs","path","path","fs"]}
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
1
  {
2
2
  "name": "@mzebley/mark-down-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "mark↓ CLI for building snippet manifests",
5
5
  "type": "module",
6
6
  "bin": {
7
- "mark-down": "dist/index.cjs"
7
+ "mark-down": "dist/index.mjs"
8
8
  },
9
- "main": "dist/index.cjs",
9
+ "main": "dist/index.mjs",
10
10
  "module": "dist/index.mjs",
11
11
  "types": "dist/index.d.ts",
12
12
  "publishConfig": {
13
13
  "access": "public"
14
14
  },
15
15
  "scripts": {
16
- "build": "tsup src/index.ts --format esm,cjs --dts",
17
- "dev": "tsup src/index.ts --watch"
16
+ "build": "tsup --config tsup.config.ts",
17
+ "dev": "tsup --config tsup.config.ts --watch"
18
18
  },
19
19
  "dependencies": {
20
- "@mzebley/mark-down": "file:../core",
20
+ "@mzebley/mark-down": "^1.2.1",
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("0.1.0");
13
+ .version("1.2.1");
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) {
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ sourcemap: true,
8
+ splitting: false,
9
+ clean: true,
10
+ outExtension({ format }) {
11
+ return {
12
+ js: format === "esm" ? ".mjs" : ".cjs"
13
+ };
14
+ }
15
+ });