@dogsbay/format-dogsbay-md 0.2.0-beta.130 → 0.2.0-beta.132

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/dist/browser.d.ts CHANGED
@@ -22,3 +22,4 @@ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcess
22
22
  export { extractHeadings, slugifyHeadingText, type HeadingInfo } from "./headings.js";
23
23
  export { splitFrontmatter, type Frontmatter } from "./frontmatter.js";
24
24
  export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN, type TemplatePagesOptions } from "./template-pages.js";
25
+ export { dogsbayMdPageFiles, mountCopies, carriedFrontmatter, MEDIA_EXTS, type AssetMount } from "./project.js";
package/dist/browser.js CHANGED
@@ -22,3 +22,4 @@ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcess
22
22
  export { extractHeadings, slugifyHeadingText } from "./headings.js";
23
23
  export { splitFrontmatter } from "./frontmatter.js";
24
24
  export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN } from "./template-pages.js";
25
+ export { dogsbayMdPageFiles, mountCopies, carriedFrontmatter, MEDIA_EXTS } from "./project.js";
package/dist/cli.d.ts CHANGED
@@ -1,20 +1,3 @@
1
1
  import type { FormatPlugin } from "@dogsbay/types";
2
- /**
3
- * The source page's front matter, carried into the Dogsbay MD it becomes.
4
- * It used to be DROPPED (only title and redirect were written), so a
5
- * converted corpus lost `description`, `tags` and custom fields, and a
6
- * query listing (`::child-links{match=…}`) over it found nothing.
7
- *
8
- * Keys Dogsbay MD gives meaning to keep their value only when the value
9
- * MEANS that here; otherwise the key moves under `imported:` — kept, never
10
- * misread. Starlight's `template: splash` (a page template) must not turn a
11
- * page into a Jinja template page. A Dogsbay MD source round-trips unchanged:
12
- * its values are valid by construction.
13
- *
14
- * `title`, `redirect` and the template keys are written by the caller.
15
- * `layout` is free-form (a theme layout name) and passes as it is.
16
- * `imported:`, not `source:` — Cloudflare pages carry their own `source:`
17
- * (an origin URL, 91 pages), which the moved keys would have overwritten.
18
- */
19
- export declare function carriedFrontmatter(fm: Record<string, unknown> | undefined): Record<string, unknown>;
2
+ export { carriedFrontmatter } from "./project.js";
20
3
  export declare const plugin: FormatPlugin;
package/dist/cli.js CHANGED
@@ -4,15 +4,17 @@
4
4
  * Reads a directory of .md files, parses each via dogsbayMdToTree, builds nav
5
5
  * from directory structure + frontmatter, returns ExportPage[] + NavItem[].
6
6
  */
7
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
7
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join, relative, basename, dirname } from "node:path";
9
9
  import { extractHeadings } from "./headings.js";
10
10
  import { parseMeta } from "@dogsbay/types";
11
11
  import { dogsbayMdToTree } from "./parse.js";
12
- import { treeToDogsbayMd } from "./serialize.js";
13
12
  import { buildNavFromDirectory } from "./nav.js";
14
13
  import { renderDiagrams } from "./renderers/diagrams.js";
15
14
  import { DOGSBAY_MD_CAPABILITIES } from "./capabilities.js";
15
+ import { dogsbayMdPageFiles, mountCopies } from "./project.js";
16
+ import { readFolder } from "@dogsbay/corpus/folder";
17
+ export { carriedFrontmatter } from "./project.js";
16
18
  /**
17
19
  * Detect whether a directory contains a Dogsbay MD source tree.
18
20
  *
@@ -221,79 +223,6 @@ function prettifyName(slug) {
221
223
  * is the include path. Mirrors migrate-asciidoc's fragment convention so the
222
224
  * build-time Minja preprocessor can `{% include %}` them.
223
225
  */
224
- /** Media files worth carrying into `_assets` from a media-only mount. */
225
- const MEDIA_EXTS = new Set([
226
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
227
- ".svg", ".ico", ".pdf", ".mp4", ".webm",
228
- ]);
229
- /** Recursively copy only media files, preserving relative structure. */
230
- function copyMediaTree(srcDir, destDir) {
231
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
232
- const full = join(srcDir, entry.name);
233
- if (entry.isDirectory()) {
234
- copyMediaTree(full, join(destDir, entry.name));
235
- continue;
236
- }
237
- const dot = entry.name.lastIndexOf(".");
238
- if (dot === -1 || !MEDIA_EXTS.has(entry.name.slice(dot).toLowerCase()))
239
- continue;
240
- mkdirSync(destDir, { recursive: true });
241
- cpSync(full, join(destDir, entry.name));
242
- }
243
- }
244
- /**
245
- * The source page's front matter, carried into the Dogsbay MD it becomes.
246
- * It used to be DROPPED (only title and redirect were written), so a
247
- * converted corpus lost `description`, `tags` and custom fields, and a
248
- * query listing (`::child-links{match=…}`) over it found nothing.
249
- *
250
- * Keys Dogsbay MD gives meaning to keep their value only when the value
251
- * MEANS that here; otherwise the key moves under `imported:` — kept, never
252
- * misread. Starlight's `template: splash` (a page template) must not turn a
253
- * page into a Jinja template page. A Dogsbay MD source round-trips unchanged:
254
- * its values are valid by construction.
255
- *
256
- * `title`, `redirect` and the template keys are written by the caller.
257
- * `layout` is free-form (a theme layout name) and passes as it is.
258
- * `imported:`, not `source:` — Cloudflare pages carry their own `source:`
259
- * (an origin URL, 91 pages), which the moved keys would have overwritten.
260
- */
261
- export function carriedFrontmatter(fm) {
262
- if (!fm)
263
- return {};
264
- const out = {};
265
- const source = {};
266
- const valid = {
267
- // `false` is the documented per-page opt-out of a source's `templates: jinja`.
268
- template: (v) => v === "jinja" || v === false,
269
- weight: (v) => typeof v === "number" && Number.isFinite(v),
270
- order: (v) => typeof v === "number" && Number.isFinite(v),
271
- draft: (v) => typeof v === "boolean",
272
- status: (v) => typeof v === "string" && ["draft", "preview", "stable", "deprecated"].includes(v),
273
- // `params:` declares a template PARTIAL's parameters: only with `template: jinja`.
274
- params: (v) => Array.isArray(v) && fm.template === "jinja",
275
- };
276
- for (const [key, value] of Object.entries(fm)) {
277
- if (key === "title" || key === "redirect" || value === undefined)
278
- continue;
279
- // A source that already has `imported:` keeps it: merged if a mapping,
280
- // else nested under its own name.
281
- if (key === "imported") {
282
- if (value && typeof value === "object" && !Array.isArray(value))
283
- Object.assign(source, value);
284
- else
285
- source.imported = value;
286
- continue;
287
- }
288
- if (valid[key] && !valid[key](value))
289
- source[key] = value;
290
- else
291
- out[key] = value;
292
- }
293
- if (Object.keys(source).length)
294
- out.imported = source;
295
- return out;
296
- }
297
226
  async function exportDogsbayMd(pages, nav, output, opts) {
298
227
  mkdirSync(output, { recursive: true });
299
228
  // Copy mounted asset dirs (e.g. Docusaurus `static/`) into `content/_assets/`,
@@ -303,51 +232,27 @@ async function exportDogsbayMd(pages, nav, output, opts) {
303
232
  // `content/_assets/**` through to `/_assets/**`. A mount may target an
304
233
  // `_assets` subdir (`into`) and restrict itself to media files (`mediaOnly`) —
305
234
  // used for docs-co-located images where the mount dir IS the docs tree.
235
+ // The copy is `mountCopies` over the mount read as a corpus — the list the browser import
236
+ // copies from its map, so both hosts copy the same files. A mount is someone else's repo:
237
+ // a symlink out of it (`public/leak.png -> ~/.ssh/id_rsa`) was copied as a link to that
238
+ // file, and `node_modules` / `.git` inside it were copied whole (2026-10-21).
306
239
  if (Array.isArray(opts.assetMounts)) {
307
240
  for (const mount of opts.assetMounts) {
308
241
  if (!existsSync(mount.dir))
309
242
  continue;
310
- // Per-mount root: `_assets` for an imported corpus, `images`/`assets`
311
- // for hand-authored content. A hardcoded root would copy the file
312
- // where the reference does not point.
313
- const assetsRoot = join(output, mount.root ?? "_assets");
314
- const dest = mount.into ? join(assetsRoot, mount.into) : assetsRoot;
315
- if (mount.mediaOnly) {
316
- copyMediaTree(mount.dir, dest);
317
- continue;
243
+ const folder = readFolder(mount.dir);
244
+ for (const { from, to } of mountCopies(folder.paths, [{ ...mount, dir: "" }])) {
245
+ const dst = join(output, to);
246
+ mkdirSync(dirname(dst), { recursive: true });
247
+ copyFileSync(folder.absolute(from), dst);
318
248
  }
319
- const skip = new Set(mount.exclude ?? []);
320
- cpSync(mount.dir, dest, {
321
- recursive: true,
322
- ...(skip.size === 0 ? {} : {
323
- // Relative to the mount, so an exclusion cannot be accidentally
324
- // satisfied by a directory name somewhere above it on disk.
325
- filter: (src) => {
326
- const rel = relative(mount.dir, src);
327
- return rel === "" || !rel.split(/[\\/]/).some((seg) => skip.has(seg));
328
- },
329
- }),
330
- });
331
249
  }
332
250
  }
333
- for (const page of pages) {
334
- const slug = page.slug || "index";
335
- const outPath = join(output, `${slug}.md`);
251
+ for (const [path, text] of dogsbayMdPageFiles(pages, nav)) {
252
+ const outPath = join(output, path);
336
253
  mkdirSync(dirname(outPath), { recursive: true });
337
- // A template partial keeps its `params:` and the template flag, so the
338
- // build treats it as a partial and checks every include against it.
339
- const templateFm = page.template ? { template: "jinja", ...(page.templateParams ? { params: page.templateParams } : {}) } : {};
340
- // A title-less template page still needs `template: jinja`, or the build
341
- // renders it in content mode and shows the directives as text.
342
- const frontmatter = page.fragment || !page.title
343
- ? (page.template ? templateFm : undefined)
344
- : { title: page.title, ...carriedFrontmatter(page.frontmatter), ...(page.redirect ? { redirect: page.redirect } : {}), ...templateFm };
345
- const md = treeToDogsbayMd(page.tree, frontmatter ? { frontmatter } : undefined);
346
- writeFileSync(outPath, md.endsWith("\n") ? md : md + "\n", "utf-8");
254
+ writeFileSync(outPath, text, "utf-8");
347
255
  }
348
- // Nav as nav.json at the content root (the dogsbay-md importer + runtime
349
- // loader both prefer nav.json). Fragments never appear here.
350
- writeFileSync(join(output, "nav.json"), JSON.stringify(nav, null, 2) + "\n", "utf-8");
351
256
  }
352
257
  export const plugin = {
353
258
  name: "dogsbay-md",
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A converted corpus as a Dogsbay MD project, as data: what `convert --to dogsbay-md` writes
3
+ * (each page as `<slug>.md`, the nav as `nav.json`, the importer's asset mounts under
4
+ * `_assets/`), without the file system. The exporter writes these files; the playground's
5
+ * browser imports (`@dogsbay/format-vitepress/migrate`) return them, so a migration in the
6
+ * browser is the one `convert` does.
7
+ */
8
+ import type { ExportPage, NavItem } from "@dogsbay/types";
9
+ /** Media files worth carrying into `_assets` from a media-only mount. */
10
+ export declare const MEDIA_EXTS: Set<string>;
11
+ /**
12
+ * The source page's front matter, carried into the Dogsbay MD it becomes.
13
+ * It used to be DROPPED (only title and redirect were written), so a
14
+ * converted corpus lost `description`, `tags` and custom fields, and a
15
+ * query listing (`::child-links{match=…}`) over it found nothing.
16
+ *
17
+ * Keys Dogsbay MD gives meaning to keep their value only when the value
18
+ * MEANS that here; otherwise the key moves under `imported:` — kept, never
19
+ * misread. Starlight's `template: splash` (a page template) must not turn a
20
+ * page into a Jinja template page. A Dogsbay MD source round-trips unchanged:
21
+ * its values are valid by construction.
22
+ *
23
+ * `title`, `redirect` and the template keys are written by the caller.
24
+ * `layout` is free-form (a theme layout name) and passes as it is.
25
+ * `imported:`, not `source:` — Cloudflare pages carry their own `source:`
26
+ * (an origin URL, 91 pages), which the moved keys would have overwritten.
27
+ */
28
+ export declare function carriedFrontmatter(fm: Record<string, unknown> | undefined): Record<string, unknown>;
29
+ /** Each page as `<slug>.md` and the nav as `nav.json`, relative to the content root. */
30
+ export declare function dogsbayMdPageFiles(pages: ExportPage[], nav: NavItem[]): Map<string, string>;
31
+ /** An importer's asset directory, as `ImportResult.assetMounts` declares it, relative to the corpus. */
32
+ export interface AssetMount {
33
+ dir: string;
34
+ /** A subdirectory of the root to land in (`pages` → `_assets/pages/`). */
35
+ into?: string;
36
+ /** Media files only (`MEDIA_EXTS`): the mount is the docs tree itself. */
37
+ mediaOnly?: boolean;
38
+ /**
39
+ * Directory names to leave behind, matched at ANY depth in the mount.
40
+ *
41
+ * `mediaOnly` is the wrong tool when a mount is mostly wanted: it also drops scripts, YAML
42
+ * and text a corpus may legitimately carry. This skips named subtrees instead — e.g.
43
+ * Calico's `licenses/`, ~35 MB of third-party attribution HTML for versions the corpus does
44
+ * not even import, which nothing references. Matched at any depth on purpose: the first
45
+ * version compared only the FIRST path segment and silently did nothing, because the real
46
+ * tree is `_assets/calico/3.32/licenses/…`.
47
+ */
48
+ exclude?: string[];
49
+ /** Public root the mount lands in; `_assets` unless the source used another. */
50
+ root?: string;
51
+ }
52
+ /**
53
+ * The files the exporter copies for `mounts`, from a corpus's `paths`: corpus path → path in
54
+ * the content root. The same rules as the exporter's copy on disk: the whole mount minus
55
+ * excluded names, or its media only.
56
+ */
57
+ export declare function mountCopies(paths: string[], mounts: AssetMount[]): {
58
+ from: string;
59
+ to: string;
60
+ }[];
@@ -0,0 +1,109 @@
1
+ import { treeToDogsbayMd } from "./serialize.js";
2
+ /** Media files worth carrying into `_assets` from a media-only mount. */
3
+ export const MEDIA_EXTS = new Set([
4
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
5
+ ".svg", ".ico", ".pdf", ".mp4", ".webm",
6
+ ]);
7
+ /**
8
+ * The source page's front matter, carried into the Dogsbay MD it becomes.
9
+ * It used to be DROPPED (only title and redirect were written), so a
10
+ * converted corpus lost `description`, `tags` and custom fields, and a
11
+ * query listing (`::child-links{match=…}`) over it found nothing.
12
+ *
13
+ * Keys Dogsbay MD gives meaning to keep their value only when the value
14
+ * MEANS that here; otherwise the key moves under `imported:` — kept, never
15
+ * misread. Starlight's `template: splash` (a page template) must not turn a
16
+ * page into a Jinja template page. A Dogsbay MD source round-trips unchanged:
17
+ * its values are valid by construction.
18
+ *
19
+ * `title`, `redirect` and the template keys are written by the caller.
20
+ * `layout` is free-form (a theme layout name) and passes as it is.
21
+ * `imported:`, not `source:` — Cloudflare pages carry their own `source:`
22
+ * (an origin URL, 91 pages), which the moved keys would have overwritten.
23
+ */
24
+ export function carriedFrontmatter(fm) {
25
+ if (!fm)
26
+ return {};
27
+ const out = {};
28
+ const source = {};
29
+ const valid = {
30
+ // `false` is the documented per-page opt-out of a source's `templates: jinja`.
31
+ template: (v) => v === "jinja" || v === false,
32
+ weight: (v) => typeof v === "number" && Number.isFinite(v),
33
+ order: (v) => typeof v === "number" && Number.isFinite(v),
34
+ draft: (v) => typeof v === "boolean",
35
+ status: (v) => typeof v === "string" && ["draft", "preview", "stable", "deprecated"].includes(v),
36
+ // `params:` declares a template PARTIAL's parameters: only with `template: jinja`.
37
+ params: (v) => Array.isArray(v) && fm.template === "jinja",
38
+ };
39
+ for (const [key, value] of Object.entries(fm)) {
40
+ if (key === "title" || key === "redirect" || value === undefined)
41
+ continue;
42
+ // A source that already has `imported:` keeps it: merged if a mapping,
43
+ // else nested under its own name.
44
+ if (key === "imported") {
45
+ if (value && typeof value === "object" && !Array.isArray(value))
46
+ Object.assign(source, value);
47
+ else
48
+ source.imported = value;
49
+ continue;
50
+ }
51
+ if (valid[key] && !valid[key](value))
52
+ source[key] = value;
53
+ else
54
+ out[key] = value;
55
+ }
56
+ if (Object.keys(source).length)
57
+ out.imported = source;
58
+ return out;
59
+ }
60
+ /** Each page as `<slug>.md` and the nav as `nav.json`, relative to the content root. */
61
+ export function dogsbayMdPageFiles(pages, nav) {
62
+ const files = new Map();
63
+ for (const page of pages) {
64
+ const slug = page.slug || "index";
65
+ // A template partial keeps its `params:` and the template flag, so the
66
+ // build treats it as a partial and checks every include against it.
67
+ const templateFm = page.template ? { template: "jinja", ...(page.templateParams ? { params: page.templateParams } : {}) } : {};
68
+ // A title-less template page still needs `template: jinja`, or the build
69
+ // renders it in content mode and shows the directives as text.
70
+ const frontmatter = page.fragment || !page.title
71
+ ? (page.template ? templateFm : undefined)
72
+ : { title: page.title, ...carriedFrontmatter(page.frontmatter), ...(page.redirect ? { redirect: page.redirect } : {}), ...templateFm };
73
+ const md = treeToDogsbayMd(page.tree, frontmatter ? { frontmatter } : undefined);
74
+ files.set(`${slug}.md`, md.endsWith("\n") ? md : md + "\n");
75
+ }
76
+ // Nav as nav.json at the content root (the dogsbay-md importer + runtime
77
+ // loader both prefer nav.json). Fragments never appear here.
78
+ files.set("nav.json", JSON.stringify(nav, null, 2) + "\n");
79
+ return files;
80
+ }
81
+ /**
82
+ * The files the exporter copies for `mounts`, from a corpus's `paths`: corpus path → path in
83
+ * the content root. The same rules as the exporter's copy on disk: the whole mount minus
84
+ * excluded names, or its media only.
85
+ */
86
+ export function mountCopies(paths, mounts) {
87
+ const out = [];
88
+ for (const mount of mounts) {
89
+ const dir = mount.dir.replace(/\/+$/, "");
90
+ const dest = [mount.root ?? "_assets", mount.into].filter(Boolean).join("/");
91
+ const skip = new Set(mount.exclude ?? []);
92
+ for (const path of paths) {
93
+ if (dir && !path.startsWith(dir + "/"))
94
+ continue;
95
+ const rel = dir ? path.slice(dir.length + 1) : path;
96
+ const segs = rel.split("/");
97
+ if (mount.mediaOnly) {
98
+ const name = segs[segs.length - 1];
99
+ const dot = name.lastIndexOf(".");
100
+ if (dot === -1 || !MEDIA_EXTS.has(name.slice(dot).toLowerCase()))
101
+ continue;
102
+ }
103
+ else if (segs.some((s) => skip.has(s)))
104
+ continue;
105
+ out.push({ from: path, to: `${dest}/${rel}` });
106
+ }
107
+ }
108
+ return out;
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-dogsbay-md",
3
- "version": "0.2.0-beta.130",
3
+ "version": "0.2.0-beta.132",
4
4
  "description": "Dogsbay Markdown dialect — canonical format for TreeNode[] with directives, YAML bodies, and intelligent fallbacks",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,9 +40,10 @@
40
40
  "markdown-it-footnote": "^4.0.0",
41
41
  "markdown-it-front-matter": "^0.2.4",
42
42
  "pele": "1.6.0",
43
- "@dogsbay/minja": "0.2.0-beta.130",
44
- "@dogsbay/serialize-core": "0.2.0-beta.130",
45
- "@dogsbay/types": "0.2.0-beta.130"
43
+ "@dogsbay/corpus": "0.2.0-beta.132",
44
+ "@dogsbay/minja": "0.2.0-beta.132",
45
+ "@dogsbay/serialize-core": "0.2.0-beta.132",
46
+ "@dogsbay/types": "0.2.0-beta.132"
46
47
  },
47
48
  "license": "MIT",
48
49
  "repository": {