@dogsbay/format-dogsbay-md 0.2.0-beta.132 → 0.2.0-beta.134
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 +1 -1
- package/dist/browser.js +1 -1
- package/dist/escape.js +3 -1
- package/dist/nav.js +13 -1
- package/dist/project.d.ts +2 -0
- package/dist/project.js +5 -2
- package/dist/serialize.js +173 -15
- package/dist/yaml.d.ts +0 -3
- package/dist/yaml.js +27 -0
- package/package.json +5 -5
package/dist/browser.d.ts
CHANGED
|
@@ -22,4 +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";
|
|
25
|
+
export { dogsbayMdPageFiles, dogsbayMdPagePath, mountCopies, carriedFrontmatter, MEDIA_EXTS, type AssetMount } from "./project.js";
|
package/dist/browser.js
CHANGED
|
@@ -22,4 +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";
|
|
25
|
+
export { dogsbayMdPageFiles, dogsbayMdPagePath, mountCopies, carriedFrontmatter, MEDIA_EXTS } from "./project.js";
|
package/dist/escape.js
CHANGED
|
@@ -145,5 +145,7 @@ export function normalizeTrailingWhitespace(content) {
|
|
|
145
145
|
if (blankRun < 2)
|
|
146
146
|
out.push(trimmed);
|
|
147
147
|
}
|
|
148
|
-
|
|
148
|
+
// trimEnd removes what `\s*$` matched, in one pass: the regex, tried at every whitespace run,
|
|
149
|
+
// was 1.8 s of pan.dev's 9,203 pages
|
|
150
|
+
return out.join("\n").trimEnd() + "\n";
|
|
149
151
|
}
|
package/dist/nav.js
CHANGED
|
@@ -14,6 +14,18 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
14
14
|
import { join, relative, basename, dirname } from "node:path";
|
|
15
15
|
import matter from "gray-matter";
|
|
16
16
|
import { loadNavFile } from "./nav-file.js";
|
|
17
|
+
/**
|
|
18
|
+
* gray-matter EVALUATES front matter that opens with `---js` / `---javascript` (its javascript
|
|
19
|
+
* engine is `eval`), so a page in someone else's repo could run code on this machine. Every
|
|
20
|
+
* call passes this, which refuses that engine; YAML and JSON are unchanged.
|
|
21
|
+
*/
|
|
22
|
+
const NO_JS_FRONT_MATTER = {
|
|
23
|
+
engines: {
|
|
24
|
+
javascript: () => {
|
|
25
|
+
throw new Error("JavaScript front matter is not run");
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
17
29
|
/**
|
|
18
30
|
* File names tried, in priority order, when auto-detecting a nav
|
|
19
31
|
* file in a content directory. The first existing file wins.
|
|
@@ -85,7 +97,7 @@ function collectPages(dir, rootDir) {
|
|
|
85
97
|
const content = readFileSync(fullPath, "utf-8");
|
|
86
98
|
let frontmatter = {};
|
|
87
99
|
try {
|
|
88
|
-
frontmatter = (matter(content).data ?? {});
|
|
100
|
+
frontmatter = (matter(content, NO_JS_FRONT_MATTER).data ?? {});
|
|
89
101
|
}
|
|
90
102
|
catch {
|
|
91
103
|
// Malformed frontmatter — treat as no frontmatter
|
package/dist/project.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare const MEDIA_EXTS: Set<string>;
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function carriedFrontmatter(fm: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
29
29
|
/** Each page as `<slug>.md` and the nav as `nav.json`, relative to the content root. */
|
|
30
|
+
/** The file a page is written to, relative to the content root (`dogsbayMdPageFiles`). */
|
|
31
|
+
export declare function dogsbayMdPagePath(page: Pick<ExportPage, "slug">): string;
|
|
30
32
|
export declare function dogsbayMdPageFiles(pages: ExportPage[], nav: NavItem[]): Map<string, string>;
|
|
31
33
|
/** An importer's asset directory, as `ImportResult.assetMounts` declares it, relative to the corpus. */
|
|
32
34
|
export interface AssetMount {
|
package/dist/project.js
CHANGED
|
@@ -58,10 +58,13 @@ export function carriedFrontmatter(fm) {
|
|
|
58
58
|
return out;
|
|
59
59
|
}
|
|
60
60
|
/** Each page as `<slug>.md` and the nav as `nav.json`, relative to the content root. */
|
|
61
|
+
/** The file a page is written to, relative to the content root (`dogsbayMdPageFiles`). */
|
|
62
|
+
export function dogsbayMdPagePath(page) {
|
|
63
|
+
return `${page.slug || "index"}.md`;
|
|
64
|
+
}
|
|
61
65
|
export function dogsbayMdPageFiles(pages, nav) {
|
|
62
66
|
const files = new Map();
|
|
63
67
|
for (const page of pages) {
|
|
64
|
-
const slug = page.slug || "index";
|
|
65
68
|
// A template partial keeps its `params:` and the template flag, so the
|
|
66
69
|
// build treats it as a partial and checks every include against it.
|
|
67
70
|
const templateFm = page.template ? { template: "jinja", ...(page.templateParams ? { params: page.templateParams } : {}) } : {};
|
|
@@ -71,7 +74,7 @@ export function dogsbayMdPageFiles(pages, nav) {
|
|
|
71
74
|
? (page.template ? templateFm : undefined)
|
|
72
75
|
: { title: page.title, ...carriedFrontmatter(page.frontmatter), ...(page.redirect ? { redirect: page.redirect } : {}), ...templateFm };
|
|
73
76
|
const md = treeToDogsbayMd(page.tree, frontmatter ? { frontmatter } : undefined);
|
|
74
|
-
files.set(
|
|
77
|
+
files.set(dogsbayMdPagePath(page), md.endsWith("\n") ? md : md + "\n");
|
|
75
78
|
}
|
|
76
79
|
// Nav as nav.json at the content root (the dogsbay-md importer + runtime
|
|
77
80
|
// loader both prefer nav.json). Fragments never appear here.
|
package/dist/serialize.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { walkInline, applyTextFlags, renderAroundGroup } from "@dogsbay/serialize-core";
|
|
1
|
+
import { walkInline, applyTextFlags, renderAroundGroup, neutralizeMarkdown, mapOutsideCodeSpans } from "@dogsbay/serialize-core";
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
import { transformFenceInfo, transformProseSegments } from "./code-regions.js";
|
|
2
4
|
import { inlineToDogsbayMd } from "./inline.js";
|
|
3
5
|
import { assertDirectiveField } from "./directive-guard.js";
|
|
4
6
|
import { renderAttrs, splitProps } from "./attributes.js";
|
|
@@ -192,13 +194,60 @@ function renderConditionalDirective(node, ctx) {
|
|
|
192
194
|
.join("\n\n");
|
|
193
195
|
return body ? `${opener}\n\n${body}\n\n{% endif %}` : `${opener}\n\n{% endif %}`;
|
|
194
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Markdown we did not write (an OpenAPI description) kept as Markdown but not as Dogsbay MD.
|
|
199
|
+
* Each piece of syntax is made inert by the grammar of whatever would act on it, never by a
|
|
200
|
+
* guess at it (review 2026-10-21: three guesses disagreed with the readers, and template tags
|
|
201
|
+
* and containers leaked):
|
|
202
|
+
* - template tags, by minja's own code/prose split (`transformProseSegments`): `{` before
|
|
203
|
+
* `%`, `{` or `#` in what minja reads as prose becomes `{`; and a fence opener's `{…}`
|
|
204
|
+
* attribute group is dropped wherever minja sees an opener (`transformFenceInfo`: indented,
|
|
205
|
+
* after a definition-list `:`) — `{minja}` there opts the block INTO template processing;
|
|
206
|
+
* - directives (`:::name`, `::name`, `:name[…]`), by markdown-it's: outside the lines it
|
|
207
|
+
* parses as code and outside code spans, every colon of a colon run and a colon that could
|
|
208
|
+
* start a directive becomes `:`.
|
|
209
|
+
* Entities read the same as the characters; code is left as written (neither reader acts on
|
|
210
|
+
* it there).
|
|
211
|
+
*/
|
|
212
|
+
function inertDogsbaySyntax(md) {
|
|
213
|
+
let out = transformFenceInfo(md, (info) => info.replace(/\s*\{[^{}]*\}[\s\S]*$/, ""));
|
|
214
|
+
out = transformProseSegments(out, (p) => p.replace(/\{(?=[%{#])/g, "{"));
|
|
215
|
+
const code = new Set();
|
|
216
|
+
for (const t of CODE_LINES_PARSER.parse(out, {})) {
|
|
217
|
+
if ((t.type === "fence" || t.type === "code_block") && t.map)
|
|
218
|
+
for (let i = t.map[0]; i < t.map[1]; i++)
|
|
219
|
+
code.add(i);
|
|
220
|
+
}
|
|
221
|
+
return out
|
|
222
|
+
.split("\n")
|
|
223
|
+
.map((line, i) => code.has(i)
|
|
224
|
+
? line
|
|
225
|
+
: mapOutsideCodeSpans(line, (p) => p.replace(/:(?=:)|(?<=:):|(?<![\p{L}\p{N}:]):(?=[A-Za-z])/gu, ":")))
|
|
226
|
+
.join("\n");
|
|
227
|
+
}
|
|
228
|
+
/** Only to find the lines markdown-it reads as code (fences, indented blocks). */
|
|
229
|
+
const CODE_LINES_PARSER = new MarkdownIt();
|
|
230
|
+
/** How deep a schema is written, how many field lines an endpoint gets, how long an example is. */
|
|
231
|
+
const ENDPOINT_FIELD_DEPTH = 8;
|
|
232
|
+
const ENDPOINT_FIELD_LINES = 400;
|
|
233
|
+
const ENDPOINT_EXAMPLE_CHARS = 8000;
|
|
195
234
|
/**
|
|
196
235
|
* Serialize an OpenAPI `endpoint` TreeNode (produced by
|
|
197
236
|
* @dogsbay/format-openapi) as agent-readable markdown. The HTML
|
|
198
237
|
* version uses ApiLayout + EndpointCard + friends; the markdown
|
|
199
|
-
* mirror
|
|
200
|
-
* blocks so an agent fetching the `.md` URL
|
|
201
|
-
* operation reference
|
|
238
|
+
* mirror writes the same data as headings, tables, lists and code
|
|
239
|
+
* blocks so an agent fetching the `.md` URL (or a site converted to
|
|
240
|
+
* Dogsbay MD) gets the full operation reference: parameters with their
|
|
241
|
+
* enums and defaults, the request body's fields to any depth (dotted
|
|
242
|
+
* names: `user.address.city`, `items[].id`), and each response's
|
|
243
|
+
* content type, fields and example.
|
|
244
|
+
*
|
|
245
|
+
* The shapes are read back by `@dogsbay/transition`'s endpoint
|
|
246
|
+
* extractor: a `- \`name\` (\`type\`) …` line is a body field, a table
|
|
247
|
+
* headed Name / Type / Required is parameters, `- **200** — …` under
|
|
248
|
+
* `### Responses` is a response. Response fields are therefore a table
|
|
249
|
+
* headed FIELD, and body fields stay flat list lines (a nested list's
|
|
250
|
+
* text would run into its parent's).
|
|
202
251
|
*/
|
|
203
252
|
function renderEndpoint(node) {
|
|
204
253
|
const props = (node.props ?? {});
|
|
@@ -213,12 +262,96 @@ function renderEndpoint(node) {
|
|
|
213
262
|
const responses = props.responses ?? [];
|
|
214
263
|
const codeSamples = props.codeSamples ?? [];
|
|
215
264
|
// Spec text is Markdown (CommonMark, per OpenAPI) from a file the site's
|
|
216
|
-
// author may not have written: its Markdown is kept
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
265
|
+
// author may not have written: its Markdown is kept; its raw HTML and
|
|
266
|
+
// unsafe links are not (neutralizeMarkdown), nor is Dogsbay MD's own
|
|
267
|
+
// syntax — a `:::` container or a `{% %}` / `{{ }}` / `{# #}` template
|
|
268
|
+
// tag would be read as one. Names and paths go into code spans, which a
|
|
269
|
+
// backtick would end.
|
|
270
|
+
const text = (v) => inertDogsbaySyntax(neutralizeMarkdown(String(v ?? "").replace(/\r\n?/g, "\n")));
|
|
271
|
+
// One line: joined BEFORE it is made inert (code that spans lines is prose once joined)
|
|
272
|
+
const line = (v) => text(String(v ?? "").replace(/\s*[\r\n]+\s*/g, " ").trim());
|
|
273
|
+
const cell = (v) => line(v).replace(/\|/g, "\\|");
|
|
221
274
|
const code = (v) => String(v ?? "").replace(/[`\r\n]/g, " ");
|
|
275
|
+
/** A code span in a table cell: a `|` would end the cell. */
|
|
276
|
+
const codeCell = (v) => code(v).replace(/\|/g, "\\|");
|
|
277
|
+
const value = (v) => code(typeof v === "string" ? v : JSON.stringify(v));
|
|
278
|
+
/** The description, then what a reader needs to send a valid value. */
|
|
279
|
+
const detail = (p) => {
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (p.description)
|
|
282
|
+
parts.push(line(p.description));
|
|
283
|
+
if (p.format)
|
|
284
|
+
parts.push(`Format: \`${code(p.format)}\`.`);
|
|
285
|
+
if (Array.isArray(p.enum) && p.enum.length) {
|
|
286
|
+
const shown = p.enum.slice(0, 20).map((e) => `\`${value(e)}\``).join(", ");
|
|
287
|
+
parts.push(`One of: ${shown}${p.enum.length > 20 ? `, … (${p.enum.length} values)` : ""}.`);
|
|
288
|
+
}
|
|
289
|
+
if (p.default !== undefined && p.default !== "")
|
|
290
|
+
parts.push(`Default: \`${value(p.default)}\`.`);
|
|
291
|
+
if (p.nullable === true)
|
|
292
|
+
parts.push("Nullable.");
|
|
293
|
+
if (p.deprecated === true)
|
|
294
|
+
parts.push("Deprecated.");
|
|
295
|
+
return parts.join(" ");
|
|
296
|
+
};
|
|
297
|
+
/** Field lines left for this endpoint: shared by the request body and every response. */
|
|
298
|
+
let fieldLines = ENDPOINT_FIELD_LINES;
|
|
299
|
+
/**
|
|
300
|
+
* Every field, flattened to dotted names, depth- and count-capped. `more` counts what is
|
|
301
|
+
* not written for either reason, descendants included (counting stops at 100,000).
|
|
302
|
+
*/
|
|
303
|
+
const flatten = (fields) => {
|
|
304
|
+
const rows = [];
|
|
305
|
+
let more = 0;
|
|
306
|
+
const count = (list, depth) => {
|
|
307
|
+
if (!Array.isArray(list) || more >= 100_000 || depth > 64)
|
|
308
|
+
return;
|
|
309
|
+
for (const f of list) {
|
|
310
|
+
if (!f || typeof f !== "object")
|
|
311
|
+
continue;
|
|
312
|
+
more++;
|
|
313
|
+
count(f.properties, depth + 1);
|
|
314
|
+
count(f.items, depth + 1);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const walk = (list, prefix, depth) => {
|
|
318
|
+
if (!Array.isArray(list))
|
|
319
|
+
return;
|
|
320
|
+
if (depth > ENDPOINT_FIELD_DEPTH)
|
|
321
|
+
return count(list, depth);
|
|
322
|
+
for (const f of list) {
|
|
323
|
+
if (!f || typeof f !== "object")
|
|
324
|
+
continue;
|
|
325
|
+
if (fieldLines <= 0) {
|
|
326
|
+
count([f], depth);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
fieldLines--;
|
|
330
|
+
const name = `${prefix}${String(f.name ?? "")}`;
|
|
331
|
+
rows.push({ name, field: f });
|
|
332
|
+
walk(f.properties, `${name}.`, depth + 1);
|
|
333
|
+
walk(f.items, `${name}[].`, depth + 1);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
walk(fields, "", 0);
|
|
337
|
+
return { rows, more };
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* An example as a titled code block (JSON unless it is a string), cut at
|
|
341
|
+
* ENDPOINT_EXAMPLE_CHARS. The title, not a paragraph, says what it is: the endpoint
|
|
342
|
+
* extractor takes an endpoint's first paragraph as its description.
|
|
343
|
+
*/
|
|
344
|
+
const example = (v, title, out) => {
|
|
345
|
+
if (v === undefined)
|
|
346
|
+
return;
|
|
347
|
+
let json = typeof v === "string" ? v : JSON.stringify(v, null, 2);
|
|
348
|
+
if (json === undefined)
|
|
349
|
+
return;
|
|
350
|
+
const cut = json.length > ENDPOINT_EXAMPLE_CHARS;
|
|
351
|
+
if (cut)
|
|
352
|
+
json = json.slice(0, ENDPOINT_EXAMPLE_CHARS);
|
|
353
|
+
out.push(renderCode({ type: "code", props: { code: json, language: typeof v === "string" ? "" : "json", title: cut ? `${title} (first ${ENDPOINT_EXAMPLE_CHARS} characters)` : title } }), "");
|
|
354
|
+
};
|
|
222
355
|
const out = [];
|
|
223
356
|
out.push(`## ${method.replace(/[^A-Z]/g, "")} ${line(path)}`);
|
|
224
357
|
out.push("");
|
|
@@ -245,18 +378,23 @@ function renderEndpoint(node) {
|
|
|
245
378
|
out.push("| Name | In | Type | Required | Description |");
|
|
246
379
|
out.push("| --- | --- | --- | --- | --- |");
|
|
247
380
|
for (const p of parameters) {
|
|
248
|
-
|
|
249
|
-
out.push(`| \`${code(p.name)}\` | ${cell(p.in)} | \`${code(p.type)}\` | ${p.required ? "yes" : "no"} | ${cell(p.description)} |`);
|
|
381
|
+
out.push(`| \`${codeCell(p.name)}\` | ${cell(p.in)} | \`${codeCell(p.type)}\` | ${p.required ? "yes" : "no"} | ${detail(p).replace(/\|/g, "\\|")} |`);
|
|
250
382
|
}
|
|
251
383
|
out.push("");
|
|
252
384
|
}
|
|
253
|
-
if (requestBody.length > 0) {
|
|
385
|
+
if (requestBody.length > 0 || props.requestBodyExample !== undefined) {
|
|
254
386
|
out.push("### Request body");
|
|
255
387
|
out.push("");
|
|
256
|
-
|
|
257
|
-
|
|
388
|
+
const { rows, more } = flatten(requestBody);
|
|
389
|
+
for (const { name, field } of rows) {
|
|
390
|
+
const d = detail(field);
|
|
391
|
+
out.push(`- \`${code(name)}\` (\`${code(field.type)}\`)${field.required ? " — required" : ""}${d ? ` — ${d}` : ""}`);
|
|
258
392
|
}
|
|
259
|
-
|
|
393
|
+
if (more)
|
|
394
|
+
out.push(`- … ${more} more field${more === 1 ? "" : "s"} not shown`);
|
|
395
|
+
if (rows.length || more)
|
|
396
|
+
out.push("");
|
|
397
|
+
example(props.requestBodyExample, "Request example", out);
|
|
260
398
|
}
|
|
261
399
|
if (responses.length > 0) {
|
|
262
400
|
out.push("### Responses");
|
|
@@ -267,6 +405,26 @@ function renderEndpoint(node) {
|
|
|
267
405
|
out.push(`- **${status}** — ${desc}`);
|
|
268
406
|
}
|
|
269
407
|
out.push("");
|
|
408
|
+
for (const r of responses) {
|
|
409
|
+
const schema = Array.isArray(r.schema) ? r.schema : [];
|
|
410
|
+
if (!schema.length && r.example === undefined)
|
|
411
|
+
continue;
|
|
412
|
+
// The content type in the heading, not a paragraph (see `example`)
|
|
413
|
+
out.push(`#### ${line(r.status)}${r.contentType ? ` (\`${code(r.contentType)}\`)` : ""}`);
|
|
414
|
+
out.push("");
|
|
415
|
+
const { rows, more } = flatten(schema);
|
|
416
|
+
if (rows.length) {
|
|
417
|
+
out.push("| Field | Type | Required | Description |");
|
|
418
|
+
out.push("| --- | --- | --- | --- |");
|
|
419
|
+
for (const { name, field } of rows) {
|
|
420
|
+
out.push(`| \`${codeCell(name)}\` | \`${codeCell(field.type)}\` | ${field.required ? "yes" : "no"} | ${detail(field).replace(/\|/g, "\\|")} |`);
|
|
421
|
+
}
|
|
422
|
+
out.push("");
|
|
423
|
+
}
|
|
424
|
+
if (more)
|
|
425
|
+
out.push(`… ${more} more field${more === 1 ? "" : "s"} not shown.`, "");
|
|
426
|
+
example(r.example, `Response example (${line(r.status)})`, out);
|
|
427
|
+
}
|
|
270
428
|
}
|
|
271
429
|
if (codeSamples.length > 0) {
|
|
272
430
|
out.push("### Code samples");
|
package/dist/yaml.d.ts
CHANGED
|
@@ -15,7 +15,4 @@ export type YamlValue = string | number | boolean | null | undefined | Date | Ya
|
|
|
15
15
|
* Keys emitted in insertion order.
|
|
16
16
|
*/
|
|
17
17
|
export declare function renderYaml(value: Record<string, YamlValue>, indent?: number): string;
|
|
18
|
-
/**
|
|
19
|
-
* Render a frontmatter block: --- YAML ---
|
|
20
|
-
*/
|
|
21
18
|
export declare function renderFrontmatter(value: Record<string, YamlValue>): string;
|
package/dist/yaml.js
CHANGED
|
@@ -131,7 +131,34 @@ function needsQuoting(value) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Render a frontmatter block: --- YAML ---
|
|
133
133
|
*/
|
|
134
|
+
/** Values a front matter may expand to when written out in full (see renderFrontmatter). */
|
|
135
|
+
const MAX_FRONTMATTER_VALUES = 100_000;
|
|
136
|
+
/** What `v` expands to when every shared reference is written out, up to `cap` + 1. */
|
|
137
|
+
function expandedSize(v, cap, memo = new Map(), depth = 0) {
|
|
138
|
+
if (v === null || typeof v !== "object" || v instanceof Date)
|
|
139
|
+
return 1;
|
|
140
|
+
const known = memo.get(v);
|
|
141
|
+
if (known !== undefined)
|
|
142
|
+
return known;
|
|
143
|
+
if (depth > 1000)
|
|
144
|
+
return cap + 1;
|
|
145
|
+
let n = 1;
|
|
146
|
+
for (const c of Array.isArray(v) ? v : Object.values(v)) {
|
|
147
|
+
n += expandedSize(c, cap, memo, depth + 1);
|
|
148
|
+
if (n > cap)
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
memo.set(v, n);
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
134
154
|
export function renderFrontmatter(value) {
|
|
155
|
+
// A source's YAML aliases are shared references once parsed. Written out in full, ten lines of
|
|
156
|
+
// nested aliases became megabytes (an alias bomb — review 2026-10-21, Docusaurus front matter;
|
|
157
|
+
// GitBook met it 2026-10-20). Such a value is written with its aliases kept; every other page
|
|
158
|
+
// is written exactly as before.
|
|
159
|
+
if (expandedSize(value, MAX_FRONTMATTER_VALUES) > MAX_FRONTMATTER_VALUES) {
|
|
160
|
+
return `---\n${jsyaml.dump(value, { lineWidth: -1, noRefs: false, skipInvalid: true }).trimEnd()}\n---`;
|
|
161
|
+
}
|
|
135
162
|
const yaml = renderYaml(value);
|
|
136
163
|
if (readsBackAs(yaml, value))
|
|
137
164
|
return `---\n${yaml}\n---`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-dogsbay-md",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.134",
|
|
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,10 +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/corpus": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
45
|
-
"@dogsbay/serialize-core": "0.2.0-beta.
|
|
46
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
43
|
+
"@dogsbay/corpus": "0.2.0-beta.134",
|
|
44
|
+
"@dogsbay/minja": "0.2.0-beta.134",
|
|
45
|
+
"@dogsbay/serialize-core": "0.2.0-beta.134",
|
|
46
|
+
"@dogsbay/types": "0.2.0-beta.134"
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"repository": {
|