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

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.
Files changed (97) hide show
  1. package/dist/attributes.d.ts +16 -2
  2. package/dist/attributes.js +14 -3
  3. package/dist/browser.d.ts +24 -0
  4. package/dist/browser.js +24 -0
  5. package/dist/capabilities.d.ts +25 -0
  6. package/dist/capabilities.js +91 -0
  7. package/dist/cli.d.ts +18 -1
  8. package/dist/cli.js +271 -30
  9. package/dist/code-regions.d.ts +88 -0
  10. package/dist/code-regions.js +214 -0
  11. package/dist/container-rule.d.ts +83 -0
  12. package/dist/container-rule.js +280 -0
  13. package/dist/diagnostics.d.ts +51 -0
  14. package/dist/diagnostics.js +335 -0
  15. package/dist/directive-guard.d.ts +10 -0
  16. package/dist/directive-guard.js +15 -0
  17. package/dist/directive-preserve.d.ts +71 -0
  18. package/dist/directive-preserve.js +426 -0
  19. package/dist/directives.d.ts +0 -1
  20. package/dist/directives.js +23 -2
  21. package/dist/escape.d.ts +26 -8
  22. package/dist/escape.js +101 -31
  23. package/dist/frontmatter.d.ts +11 -0
  24. package/dist/frontmatter.js +66 -0
  25. package/dist/headings.d.ts +43 -0
  26. package/dist/headings.js +156 -0
  27. package/dist/index.d.ts +13 -3
  28. package/dist/index.js +13 -3
  29. package/dist/inline.d.ts +0 -4
  30. package/dist/inline.js +242 -10
  31. package/dist/minja-mask.d.ts +63 -0
  32. package/dist/minja-mask.js +127 -0
  33. package/dist/nav-file.d.ts +26 -4
  34. package/dist/nav-file.js +91 -15
  35. package/dist/nav.d.ts +7 -1
  36. package/dist/nav.js +23 -5
  37. package/dist/parse-attrs.d.ts +9 -1
  38. package/dist/parse-attrs.js +28 -1
  39. package/dist/parse.d.ts +37 -2
  40. package/dist/parse.js +811 -50
  41. package/dist/plugin-block-leaf.d.ts +0 -1
  42. package/dist/plugin-block-leaf.js +3 -16
  43. package/dist/plugin-containers.d.ts +23 -6
  44. package/dist/plugin-containers.js +69 -4
  45. package/dist/plugin-inline-directives.d.ts +2 -1
  46. package/dist/plugin-inline-directives.js +10 -16
  47. package/dist/plugin-mark.d.ts +12 -0
  48. package/dist/plugin-mark.js +74 -0
  49. package/dist/plugin-math.d.ts +22 -0
  50. package/dist/plugin-math.js +90 -0
  51. package/dist/renderers/diagrams.d.ts +28 -0
  52. package/dist/renderers/diagrams.js +92 -0
  53. package/dist/renderers/mermaid.d.ts +23 -0
  54. package/dist/renderers/mermaid.js +151 -0
  55. package/dist/renderers/pele.d.ts +13 -0
  56. package/dist/renderers/pele.js +42 -0
  57. package/dist/serialize.d.ts +1 -2
  58. package/dist/serialize.js +520 -71
  59. package/dist/template-pages.d.ts +68 -0
  60. package/dist/template-pages.js +248 -0
  61. package/dist/types.d.ts +0 -1
  62. package/dist/types.js +18 -2
  63. package/dist/yaml.d.ts +1 -2
  64. package/dist/yaml.js +78 -3
  65. package/package.json +27 -12
  66. package/dist/attributes.d.ts.map +0 -1
  67. package/dist/attributes.js.map +0 -1
  68. package/dist/cli.d.ts.map +0 -1
  69. package/dist/cli.js.map +0 -1
  70. package/dist/directives.d.ts.map +0 -1
  71. package/dist/directives.js.map +0 -1
  72. package/dist/escape.d.ts.map +0 -1
  73. package/dist/escape.js.map +0 -1
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js.map +0 -1
  76. package/dist/inline.d.ts.map +0 -1
  77. package/dist/inline.js.map +0 -1
  78. package/dist/nav-file.d.ts.map +0 -1
  79. package/dist/nav-file.js.map +0 -1
  80. package/dist/nav.d.ts.map +0 -1
  81. package/dist/nav.js.map +0 -1
  82. package/dist/parse-attrs.d.ts.map +0 -1
  83. package/dist/parse-attrs.js.map +0 -1
  84. package/dist/parse.d.ts.map +0 -1
  85. package/dist/parse.js.map +0 -1
  86. package/dist/plugin-block-leaf.d.ts.map +0 -1
  87. package/dist/plugin-block-leaf.js.map +0 -1
  88. package/dist/plugin-containers.d.ts.map +0 -1
  89. package/dist/plugin-containers.js.map +0 -1
  90. package/dist/plugin-inline-directives.d.ts.map +0 -1
  91. package/dist/plugin-inline-directives.js.map +0 -1
  92. package/dist/serialize.d.ts.map +0 -1
  93. package/dist/serialize.js.map +0 -1
  94. package/dist/types.d.ts.map +0 -1
  95. package/dist/types.js.map +0 -1
  96. package/dist/yaml.d.ts.map +0 -1
  97. package/dist/yaml.js.map +0 -1
@@ -8,12 +8,27 @@
8
8
  export interface RenderedAttrs {
9
9
  id?: string;
10
10
  classes?: string[];
11
- props?: Record<string, string | number | boolean | undefined>;
11
+ /**
12
+ * Props captured from `TreeNode.props`. Primitive values (string,
13
+ * number, boolean) round-trip through inline `{key="value"}` form.
14
+ * Arrays / plain objects are preserved here too — `renderAttrs`
15
+ * skips them (inline form can't represent them) but the
16
+ * directive renderer's `shouldUseYaml` branch picks them up and
17
+ * routes the directive to YAML body form, where complex values
18
+ * survive intact.
19
+ */
20
+ props?: Record<string, unknown>;
12
21
  }
13
22
  /**
14
23
  * Render an attribute block.
15
24
  * Returns `{ #id .a .b key="value" }` or "" if no attrs.
16
25
  * Leading space included so callers can append directly to content.
26
+ *
27
+ * Inline form can only carry primitive values. Arrays / objects in
28
+ * `props` are silently skipped here — the caller is expected to have
29
+ * already routed those to YAML body form (see `shouldUseYaml` in
30
+ * directives.ts). If a non-primitive does reach this point, dropping
31
+ * it is the safest fallback (there's no inline syntax for it).
17
32
  */
18
33
  export declare function renderAttrs(attrs: RenderedAttrs): string;
19
34
  /**
@@ -30,4 +45,3 @@ export declare function escapeAttrValue(value: string): string;
30
45
  * This is the common glue between TreeNode.props and attribute output.
31
46
  */
32
47
  export declare function splitProps(props: Record<string, unknown> | undefined, excludeKeys?: string[]): RenderedAttrs;
33
- //# sourceMappingURL=attributes.d.ts.map
@@ -9,6 +9,12 @@
9
9
  * Render an attribute block.
10
10
  * Returns `{ #id .a .b key="value" }` or "" if no attrs.
11
11
  * Leading space included so callers can append directly to content.
12
+ *
13
+ * Inline form can only carry primitive values. Arrays / objects in
14
+ * `props` are silently skipped here — the caller is expected to have
15
+ * already routed those to YAML body form (see `shouldUseYaml` in
16
+ * directives.ts). If a non-primitive does reach this point, dropping
17
+ * it is the safest fallback (there's no inline syntax for it).
12
18
  */
13
19
  export function renderAttrs(attrs) {
14
20
  const parts = [];
@@ -29,6 +35,10 @@ export function renderAttrs(attrs) {
29
35
  parts.push(key);
30
36
  continue;
31
37
  }
38
+ // Skip arrays / plain objects — inline form can't represent
39
+ // them. They survive via the YAML body branch upstream.
40
+ if (typeof value === "object")
41
+ continue;
32
42
  parts.push(`${key}=${escapeAttrValue(String(value))}`);
33
43
  }
34
44
  }
@@ -71,8 +81,10 @@ export function splitProps(props, excludeKeys = []) {
71
81
  }
72
82
  if (value === null || value === undefined)
73
83
  continue;
74
- if (typeof value === "object")
75
- continue; // skip nested structures
84
+ // Pass arrays / objects through. Inline-form rendering will skip
85
+ // them (`renderAttrs`); YAML-body rendering preserves them
86
+ // (`attrsToYaml`). The `shouldUseYaml` heuristic in directives.ts
87
+ // detects their presence and routes the directive accordingly.
76
88
  cleanProps[key] = value;
77
89
  }
78
90
  if (Object.keys(cleanProps).length > 0) {
@@ -80,4 +92,3 @@ export function splitProps(props, excludeKeys = []) {
80
92
  }
81
93
  return result;
82
94
  }
83
- //# sourceMappingURL=attributes.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Browser-safe entry point — the pure TreeNode round-trip and text
3
+ * helpers, with NO `node:fs` in the module graph.
4
+ *
5
+ * The main barrel (`.`) re-exports nav / nav-file / mermaid, which
6
+ * import `node:fs` at module load and so break browser/extension
7
+ * bundling. Consumers that only need parse/serialize (the editing
8
+ * engine, extension contexts) import from `@dogsbay/format-dogsbay-md/browser`
9
+ * instead. `parse.ts` and `serialize.ts` transit only browser-safe deps
10
+ * (markdown-it, js-yaml). Frontmatter is split by the local `frontmatter.ts`
11
+ * (js-yaml only) rather than gray-matter, whose entry `require('fs')`s at
12
+ * module load and would drag `node:fs` back into this graph.
13
+ */
14
+ export { dogsbayMdToTree, parseInline, containerOpenBefore, type ParseResult, type ParseOptions } from "./parse.js";
15
+ export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
16
+ export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
17
+ export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
18
+ export type { SerializeOptions } from "./types.js";
19
+ export { inlineToDogsbayMd } from "./inline.js";
20
+ export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
21
+ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
22
+ export { extractHeadings, slugifyHeadingText, type HeadingInfo } from "./headings.js";
23
+ export { splitFrontmatter, type Frontmatter } from "./frontmatter.js";
24
+ export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN, type TemplatePagesOptions } from "./template-pages.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Browser-safe entry point — the pure TreeNode round-trip and text
3
+ * helpers, with NO `node:fs` in the module graph.
4
+ *
5
+ * The main barrel (`.`) re-exports nav / nav-file / mermaid, which
6
+ * import `node:fs` at module load and so break browser/extension
7
+ * bundling. Consumers that only need parse/serialize (the editing
8
+ * engine, extension contexts) import from `@dogsbay/format-dogsbay-md/browser`
9
+ * instead. `parse.ts` and `serialize.ts` transit only browser-safe deps
10
+ * (markdown-it, js-yaml). Frontmatter is split by the local `frontmatter.ts`
11
+ * (js-yaml only) rather than gray-matter, whose entry `require('fs')`s at
12
+ * module load and would drag `node:fs` back into this graph.
13
+ */
14
+ export { dogsbayMdToTree, parseInline, containerOpenBefore } from "./parse.js";
15
+ export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
16
+ // The `:::name` blocks the parser knows (an editor completes names from it)
17
+ export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
18
+ export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
19
+ export { inlineToDogsbayMd } from "./inline.js";
20
+ export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
21
+ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
22
+ export { extractHeadings, slugifyHeadingText } from "./headings.js";
23
+ export { splitFrontmatter } from "./frontmatter.js";
24
+ export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN } from "./template-pages.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What the Dogsbay MD exporter can and cannot represent — a CONTRACT (the
3
+ * format-docusaurus pattern): one cell per `MATRIX_ROWS` type of the
4
+ * component × format matrix, checked by `validateCapabilities` and by
5
+ * tests/capabilities.test.ts.
6
+ *
7
+ * Dogsbay MD is the CANONICAL source format, so the bar for `full` is a
8
+ * round trip: `treeToDogsbayMd` → `dogsbayMdToTree` gives back the same tree
9
+ * (modulo canonical spelling — `:::tab[One]` comes back as the deflist tab
10
+ * form, a trailing newline on raw HTML). Each `full` below was measured that
11
+ * way, not assumed.
12
+ *
13
+ * Support levels: full — round-trips · partial — survives with reduced
14
+ * fidelity (the note says how) · none — dropped or flattened (the note says
15
+ * what is lost).
16
+ *
17
+ * Types with no `case` in `renderNode` fall to `renderUnknown`: with the
18
+ * default `unknownNodes: "html"` it writes `node.html` if present, else the
19
+ * children (the wrapper and its props are lost), else
20
+ * `<!-- unsupported: <type> -->`. Nothing is recorded — there is no export
21
+ * ledger in this package yet — so every partial/none here is a loss the
22
+ * exporter does not report.
23
+ */
24
+ import type { CapabilityDeclaration } from "@dogsbay/serialize-core";
25
+ export declare const DOGSBAY_MD_CAPABILITIES: CapabilityDeclaration[];
@@ -0,0 +1,91 @@
1
+ export const DOGSBAY_MD_CAPABILITIES = [
2
+ // ── Blocks ───────────────────────────────────────────────
3
+ { feature: "heading", support: "full", rendersAs: "`#`–`######` with `{#id .class}` attrs" },
4
+ { feature: "paragraph", support: "full" },
5
+ { feature: "blockquote", support: "full", rendersAs: "`> …`" },
6
+ { feature: "unordered-list", support: "full", rendersAs: "`- item`" },
7
+ { feature: "ordered-list", support: "full", rendersAs: "`1. item`" },
8
+ { feature: "hr", support: "full", rendersAs: "`---`" },
9
+ {
10
+ feature: "table",
11
+ support: "partial",
12
+ rendersAs: "GFM pipe table + `{caption=\"…\"}`; an HTML `<table>` when a cell holds blocks, a row has `th` cells, or the table is headerless",
13
+ note: "A simple table round-trips. The HTML fallback re-parses as a raw `html` node, not a `table`; inside it blocks other than paragraph/list/code/callout/table collapse to their content and kbd/icon/math/footnote-ref degrade to text.",
14
+ },
15
+ { feature: "code", support: "full", rendersAs: "fenced block with language, `title=\"…\"` and line-highlight meta" },
16
+ { feature: "math-block", support: "full", rendersAs: "`$$ … $$`" },
17
+ { feature: "deflist", support: "full", rendersAs: "`Term` / `: definition`" },
18
+ { feature: "figure", support: "full", rendersAs: "`:::figure{caption=\"…\"}`; a captionless figure is written as its content" },
19
+ { feature: "footnote-list", support: "full", rendersAs: "`[^label]: …` definitions" },
20
+ { feature: "html", support: "full", rendersAs: "the raw HTML, verbatim" },
21
+ {
22
+ feature: "html-container",
23
+ support: "partial",
24
+ rendersAs: "`node.html` verbatim, or the container's children as blocks",
25
+ note: "Content is kept; a children-only container (the shape the mkdocs md_in_html, asciidoc and mdx importers produce) loses its wrapper tag and attributes.",
26
+ },
27
+ {
28
+ feature: "diagram",
29
+ support: "partial",
30
+ rendersAs: "a ```mermaid fence with the diagram source",
31
+ note: "The source round-trips (it re-parses as a code fence, which the build turns back into a diagram); a pre-rendered SVG is not kept.",
32
+ },
33
+ { feature: "youtube", support: "full", rendersAs: "`::youtube{#id title=\"…\"}`" },
34
+ // ── Components ───────────────────────────────────────────
35
+ { feature: "callout", support: "full", rendersAs: "GitHub alert `> [!NOTE]`; `:::type[Title]{…}` for a custom title or a non-GitHub type" },
36
+ { feature: "details", support: "full", rendersAs: "`:::details{title=\"…\"}`" },
37
+ { feature: "details-sheet", support: "full", rendersAs: "`:::details{title=\"…\" sheet}`" },
38
+ { feature: "theme", support: "full", rendersAs: "`:::theme{name=\"dark\"}` around the content" },
39
+ { feature: "tabs-variant", support: "full", rendersAs: "`:::tabs{variant=\"pills\"}`" },
40
+ { feature: "tone", support: "full", rendersAs: "`tone=\"…\"` on `:::cards` or `:::card`" },
41
+ { feature: "tabs", support: "full", rendersAs: "`:::tabs` with the deflist tab form (`Label` / `: body`)" },
42
+ { feature: "steps", support: "full", rendersAs: "`:::steps` around an ordered list" },
43
+ { feature: "accordion", support: "full", rendersAs: "`:::accordion` with deflist items" },
44
+ { feature: "cards", support: "full", rendersAs: "`:::cards` — compact YAML list when every card body is paragraphs, `:::card` directives otherwise" },
45
+ { feature: "card", support: "full", rendersAs: "`:::card{title href …}`" },
46
+ { feature: "link-card", support: "full", rendersAs: "`:::link-card{title href}` with the description as body" },
47
+ { feature: "files", support: "full", rendersAs: "`:::files` around the nested list" },
48
+ { feature: "grid", support: "full", rendersAs: "`::::grid` / `:::grid-item`" },
49
+ { feature: "link-button", support: "full", rendersAs: "`:::button{href}`" },
50
+ { feature: "related", support: "full", rendersAs: "bold caption + `{._additional-resources}` + the link list (the shape the parser folds back)" },
51
+ {
52
+ feature: "example",
53
+ support: "partial",
54
+ rendersAs: "its children, unwrapped",
55
+ note: "No case: `renderUnknown` writes the children only. The `:::example` wrapper (live preview + shown source) is lost and the content re-parses as ordinary blocks; unrecorded.",
56
+ },
57
+ {
58
+ feature: "container",
59
+ support: "partial",
60
+ rendersAs: "`:::name` for this format's own generic containers; the children otherwise",
61
+ note: "A container parsed from Dogsbay MD (`meta.container` + `meta.name`) round-trips. A `container` from another importer has no name to write: its wrapper and props (class, id) are dropped and only the children survive; unrecorded.",
62
+ },
63
+ {
64
+ feature: "annotation-list",
65
+ support: "partial",
66
+ rendersAs: "each item's content as a separate paragraph",
67
+ note: "No case: `renderUnknown` writes the children, and the list items render without list markers — numbering and the link to the code block's `(n)` markers are lost; unrecorded.",
68
+ },
69
+ // ── API reference ────────────────────────────────────────
70
+ {
71
+ feature: "endpoint",
72
+ support: "partial",
73
+ rendersAs: "`## METHOD /path` + summary/description + Parameters table + request-body/response lists + code-sample fences",
74
+ note: "Readable Markdown, but not a structure: it re-parses as headings/tables/lists, never an `endpoint`. Schemas beyond name/type/required/description, and spec raw HTML (escaped as `&lt;`), are not carried.",
75
+ },
76
+ { feature: "api-doc", support: "full", rendersAs: "`:::api-doc{…}`" },
77
+ { feature: "api-class", support: "full", rendersAs: "`:::api-class{…}`" },
78
+ { feature: "api-member", support: "full", rendersAs: "`:::api-member{…}`" },
79
+ { feature: "api-params", support: "full", rendersAs: "`:::api-params` with a YAML `params:` body" },
80
+ { feature: "api-source", support: "full", rendersAs: "`:::api-source{…}`" },
81
+ // ── Inline ───────────────────────────────────────────────
82
+ { feature: "link", support: "full", rendersAs: "`[text](href \"title\")`" },
83
+ { feature: "image", support: "full", rendersAs: "`![alt](src \"title\"){attrs}`" },
84
+ { feature: "kbd", support: "full", rendersAs: "`:kbd[Ctrl+S]`" },
85
+ { feature: "math", support: "full", rendersAs: "`$…$`" },
86
+ { feature: "highlight", support: "full", rendersAs: "`==…==`" },
87
+ { feature: "badge", support: "full", rendersAs: "`:badge[Text]{variant=…}`" },
88
+ { feature: "icon", support: "full", rendersAs: "`:icon[name]`" },
89
+ { feature: "footnote-ref", support: "full", rendersAs: "`[^label]`" },
90
+ { feature: "html-inline", support: "full", rendersAs: "the raw HTML, verbatim" },
91
+ ];
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,20 @@
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
20
  export declare const plugin: FormatPlugin;
3
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -4,11 +4,15 @@
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 { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
- import { join, relative, basename } from "node:path";
7
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { join, relative, basename, dirname } from "node:path";
9
+ import { extractHeadings } from "./headings.js";
9
10
  import { parseMeta } from "@dogsbay/types";
10
11
  import { dogsbayMdToTree } from "./parse.js";
12
+ import { treeToDogsbayMd } from "./serialize.js";
11
13
  import { buildNavFromDirectory } from "./nav.js";
14
+ import { renderDiagrams } from "./renderers/diagrams.js";
15
+ import { DOGSBAY_MD_CAPABILITIES } from "./capabilities.js";
12
16
  /**
13
17
  * Detect whether a directory contains a Dogsbay MD source tree.
14
18
  *
@@ -30,16 +34,76 @@ function detectDogsbayMdSource(path) {
30
34
  return false;
31
35
  }
32
36
  }
37
+ /**
38
+ * Conventional content-asset roots, in probe order. Mounted under their own
39
+ * name so an author's `/images/logo.png` resolves, and excluded from the page
40
+ * walk so their contents are assets rather than also being pages.
41
+ */
42
+ const ASSET_ROOTS = ["images", "assets", "_assets"];
43
+ /**
44
+ * Does this directory hold any markdown, at any depth?
45
+ *
46
+ * Distinguishes a media-only asset root (skip it: its files are assets, and a
47
+ * stray .md would become both a page and a raw copy in public/) from a docs
48
+ * SECTION that merely shares the name (walk it: those are pages).
49
+ */
50
+ function containsMarkdown(dir) {
51
+ let entries;
52
+ try {
53
+ entries = readdirSync(dir, { withFileTypes: true });
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ for (const e of entries) {
59
+ if (e.isDirectory()) {
60
+ if (containsMarkdown(join(dir, e.name)))
61
+ return true;
62
+ }
63
+ else if (e.name.endsWith(".md")) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
33
69
  function walkMdFiles(dir, root, out = []) {
34
70
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
71
+ // `_`-prefixed files/dirs are include targets (fragments), not routable
72
+ // pages — the standard Jekyll/Docusaurus convention. The build-time Minja
73
+ // preprocessor `{% include %}`s them into pages; they get no route of their
74
+ // own. (e.g. structural Docusaurus import writes `_includes/**` fragments.)
75
+ if (entry.name.startsWith("_"))
76
+ continue;
77
+ // A conventional asset root carrying only media is not a page tree, and
78
+ // leaving it in the walk would make a stray .md there BOTH a routable
79
+ // page and a file copied verbatim into public/.
80
+ //
81
+ // But `images/` is also a perfectly ordinary section name. Skipping it
82
+ // unconditionally silently dropped every page under it — "Imported 1
83
+ // pages" where there were 2, no warning. So skip only a root that
84
+ // carries NO markdown at all, and only at the content root.
85
+ if (dir === root &&
86
+ ASSET_ROOTS.includes(entry.name) &&
87
+ !containsMarkdown(join(dir, entry.name))) {
88
+ continue;
89
+ }
35
90
  const full = join(dir, entry.name);
36
91
  if (entry.isDirectory()) {
37
92
  walkMdFiles(full, root, out);
38
93
  }
39
94
  else if (entry.name.endsWith(".md")) {
40
95
  const rel = relative(root, full).replace(/\\/g, "/");
41
- const slug = rel.replace(/\.md$/, "");
42
- out.push({ filePath: full, slug });
96
+ let slug = rel.replace(/\.md$/, "");
97
+ // A `<dir>/index.md` page is the directory's landing page: its slug
98
+ // is the directory, not `<dir>/index`. This MUST match how the nav
99
+ // layer normalizes the same file (nav-file.ts `fileToHref` strips
100
+ // `/index`), or `routablesFrom: nav`'s `filterPagesToNav` drops the
101
+ // page and the section landing 404s. The bare root `index.md` keeps
102
+ // slug `index` — format-astro's root-home + `.md`-mirror special-case
103
+ // keys off `slug === "index"`. See plans/dir-index-slug-nav-drop.md.
104
+ if (slug.endsWith("/index"))
105
+ slug = slug.slice(0, -"/index".length);
106
+ out.push({ filePath: full, slug, sourcePath: rel });
43
107
  }
44
108
  }
45
109
  return out;
@@ -61,30 +125,38 @@ async function importDogsbayMd(source, opts) {
61
125
  const taxonomyNames = Array.isArray(opts.taxonomyNames)
62
126
  ? opts.taxonomyNames
63
127
  : undefined;
64
- for (const { filePath, slug } of files) {
128
+ // Pre-parse pass — opt-out via `diagrams: false`. When enabled
129
+ // (default), `code` fences with `lang: mermaid` get transformed
130
+ // to `diagram` nodes with pre-rendered SVG. Fail-open: if mmdc
131
+ // (mermaid-cli) isn't installed the code block is left as-is and
132
+ // a one-line warning is printed. See plans/mermaid-dogsbay-md.md.
133
+ const diagramsOn = opts.diagrams !== false;
134
+ // `content.diagrams: pele` draws them with Pele; `true` (the default) with mmdc
135
+ const diagramEngine = opts.diagrams === "pele" ? "pele" : "mermaid-cli";
136
+ for (const { filePath, slug, sourcePath } of files) {
65
137
  const content = readFileSync(filePath, "utf-8");
66
138
  const { tree, frontmatter } = dogsbayMdToTree(content);
139
+ if (diagramsOn) {
140
+ await renderDiagrams(tree, { engine: diagramEngine });
141
+ }
67
142
  const title = String(frontmatter.title
68
143
  ?? prettifyName(basename(slug)));
69
- // Extract heading metadata for TOC
70
- const headings = [];
71
- for (const node of tree) {
72
- if (node.type === "heading" && node.inline) {
73
- const text = node.inline
74
- .map((n) => (n.type === "text" ? n.text : ""))
75
- .join("")
76
- .trim();
77
- headings.push({
78
- depth: node.props?.level ?? 1,
79
- slug: node.props?.slug ?? slugify(text),
80
- text,
81
- });
82
- }
83
- }
144
+ // Extract heading metadata for TOC AND backfill `node.props.slug`
145
+ // so format-astro's headingToAstro emits a matching `id` on the
146
+ // rendered `<h2>` / `<h3>`. Without this, the TOC's
147
+ // `<a href="#quick-start">` had nothing to scroll to — the
148
+ // heading was emitted without an id because the tree didn't
149
+ // carry the slug. Mirrors format-mkdocs/src/loader.ts:312.
150
+ //
151
+ // Duplicate slugs get `-1`, `-2` suffixes so two `## Foo`
152
+ // headings on the same page produce distinct anchors. Same
153
+ // collision-handling shape as MkDocs.
154
+ const headings = extractHeadings(tree);
84
155
  const redirect = frontmatter.redirect;
85
156
  const meta = parseMeta(frontmatter, { slug, taxonomyNames });
86
157
  pages.push({
87
158
  slug,
159
+ sourcePath,
88
160
  title,
89
161
  tree,
90
162
  headings,
@@ -95,8 +167,42 @@ async function importDogsbayMd(source, opts) {
95
167
  }
96
168
  const navFilePath = opts.nav || undefined;
97
169
  const hrefPrefix = typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : undefined;
98
- const nav = buildNavFromDirectory(source, { navFilePath, hrefPrefix });
99
- return { pages, nav };
170
+ // `--strict` at the CLI layer maps to "fail" here. `loadNavFile`
171
+ // already throws on the first miss in fail mode; the build
172
+ // surfaces the throw as a fatal error and exits non-zero.
173
+ // See plans/site-build-strict.md.
174
+ const missingNavTargets = opts.missingNavTargets === "fail"
175
+ ? "fail"
176
+ : undefined;
177
+ const nav = buildNavFromDirectory(source, {
178
+ navFilePath,
179
+ hrefPrefix,
180
+ missingFile: missingNavTargets,
181
+ });
182
+ // Declare where this corpus's media lives, so an exporter copies the FILES
183
+ // that the `_assets/…` references in the pages point at.
184
+ //
185
+ // Without this the reference survives the conversion and the file does not,
186
+ // which looks exactly like success: the page count, the retention score and
187
+ // a clean exit are all unaffected while every image 404s. `dogsbay site
188
+ // build` has its own asset pass, so this was invisible there and only bit
189
+ // `convert` — an OpenShift vault shipped with 294 dead image references.
190
+ // Conventional asset roots, in probe order. `_assets` is what every
191
+ // importer rewrites its refs to and stays the documented convention
192
+ // (docs/images.md). Hand-authored content is not written by an
193
+ // importer, though: someone starting a site types `/images/logo.png`,
194
+ // which silently resolved nowhere because only `_assets` was probed.
195
+ //
196
+ // Each root that exists mounts under its OWN name, so the reference the
197
+ // author wrote is the path the file lands at. Recognising several costs
198
+ // nothing: an absent directory is simply not mounted.
199
+ const assetMounts = [];
200
+ for (const root of ASSET_ROOTS) {
201
+ const dir = join(source, root);
202
+ if (existsSync(dir))
203
+ assetMounts.push({ dir, root });
204
+ }
205
+ return assetMounts.length ? { pages, nav, assetMounts } : { pages, nav };
100
206
  }
101
207
  function prettifyName(slug) {
102
208
  return slug
@@ -104,17 +210,150 @@ function prettifyName(slug) {
104
210
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
105
211
  .join(" ");
106
212
  }
107
- function slugify(text) {
108
- return text
109
- .toLowerCase()
110
- .replace(/[^\w\s-]/g, "")
111
- .trim()
112
- .replace(/\s+/g, "-");
213
+ /**
214
+ * Export ExportPage[] + nav to a directory of Dogsbay-MD files.
215
+ *
216
+ * Each page's tree is serialized via `treeToDogsbayMd` (which preserves any
217
+ * Minja directives via `escapePreservingMinja`) and written to `<output>/<slug>.md`.
218
+ *
219
+ * Fragment pages (`page.fragment`) are include targets, not routable pages:
220
+ * written with NO frontmatter, NO title, and excluded from nav — their identity
221
+ * is the include path. Mirrors migrate-asciidoc's fragment convention so the
222
+ * build-time Minja preprocessor can `{% include %}` them.
223
+ */
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
+ async function exportDogsbayMd(pages, nav, output, opts) {
298
+ mkdirSync(output, { recursive: true });
299
+ // Copy mounted asset dirs (e.g. Docusaurus `static/`) into `content/_assets/`,
300
+ // the Dogsbay standard (docs/images.md). Preserves internal structure
301
+ // (static/img/x → <output>/_assets/img/x) so the parser's rewritten
302
+ // `/_assets/img/x` refs resolve, and a later `site build`'s asset pass copies
303
+ // `content/_assets/**` through to `/_assets/**`. A mount may target an
304
+ // `_assets` subdir (`into`) and restrict itself to media files (`mediaOnly`) —
305
+ // used for docs-co-located images where the mount dir IS the docs tree.
306
+ if (Array.isArray(opts.assetMounts)) {
307
+ for (const mount of opts.assetMounts) {
308
+ if (!existsSync(mount.dir))
309
+ 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;
318
+ }
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
+ }
332
+ }
333
+ for (const page of pages) {
334
+ const slug = page.slug || "index";
335
+ const outPath = join(output, `${slug}.md`);
336
+ 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");
347
+ }
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");
113
351
  }
114
352
  export const plugin = {
115
353
  name: "dogsbay-md",
116
354
  canImport: true,
117
- canExport: false,
355
+ canExport: true,
356
+ capabilities: DOGSBAY_MD_CAPABILITIES,
118
357
  detectSource: detectDogsbayMdSource,
119
358
  importOptions: [
120
359
  {
@@ -125,5 +364,7 @@ export const plugin = {
125
364
  async import(source, opts) {
126
365
  return importDogsbayMd(source, opts);
127
366
  },
367
+ async export(pages, nav, output, opts) {
368
+ return exportDogsbayMd(pages, nav, output, opts);
369
+ },
128
370
  };
129
- //# sourceMappingURL=cli.js.map