@zudojs/docs 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -9
  3. package/dist/docsTypes/index.d.ts +1 -1
  4. package/dist/document/documentBuilder.core.d.ts +15 -5
  5. package/dist/document/documentBuilder.core.js +27 -15
  6. package/dist/document/index.d.ts +1 -1
  7. package/dist/errors/index.d.ts +16 -0
  8. package/dist/errors/index.js +18 -0
  9. package/dist/examples/examples.core.d.ts +4 -0
  10. package/dist/examples/examples.core.js +16 -7
  11. package/dist/frontmatter/frontmatter.core.d.ts +1 -1
  12. package/dist/frontmatter/frontmatter.parser.d.ts +15 -2
  13. package/dist/frontmatter/frontmatter.parser.js +202 -39
  14. package/dist/frontmatter/frontmatter.serializer.d.ts +13 -1
  15. package/dist/frontmatter/frontmatter.serializer.js +89 -4
  16. package/dist/frontmatter/frontmatter.types.d.ts +10 -1
  17. package/dist/frontmatter/index.d.ts +1 -1
  18. package/dist/generator/generator.types.d.ts +18 -0
  19. package/dist/generator/generatorJson.core.d.ts +8 -3
  20. package/dist/generator/generatorJson.core.js +16 -4
  21. package/dist/generator/generatorMarkdown.core.d.ts +4 -0
  22. package/dist/generator/generatorMarkdown.core.js +32 -13
  23. package/dist/generator/generatorMarkdownNodes.d.ts +20 -0
  24. package/dist/generator/generatorMarkdownNodes.js +87 -17
  25. package/dist/generator/index.d.ts +2 -2
  26. package/dist/generator/index.js +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +2 -0
  29. package/dist/navigation/index.d.ts +1 -1
  30. package/dist/navigation/index.js +1 -1
  31. package/dist/navigation/navigation.core.d.ts +25 -6
  32. package/dist/navigation/navigation.core.js +93 -25
  33. package/dist/registry/index.d.ts +2 -1
  34. package/dist/registry/index.js +1 -1
  35. package/dist/registry/registry.core.d.ts +29 -3
  36. package/dist/registry/registry.core.js +32 -6
  37. package/dist/utils/index.d.ts +2 -1
  38. package/dist/utils/index.js +2 -1
  39. package/dist/utils/utils.freeze.d.ts +16 -0
  40. package/dist/utils/utils.freeze.js +53 -0
  41. package/dist/utils/utils.helper.d.ts +27 -4
  42. package/dist/utils/utils.helper.js +108 -21
  43. package/dist/validator/index.d.ts +4 -1
  44. package/dist/validator/index.js +2 -1
  45. package/dist/validator/validator.types.d.ts +8 -0
  46. package/dist/validator/validator.types.js +10 -1
  47. package/dist/validator/validatorAll/validatorAll.core.d.ts +11 -2
  48. package/dist/validator/validatorAll/validatorAll.core.js +19 -7
  49. package/dist/validator/validatorAll/validatorNavigation.core.d.ts +8 -1
  50. package/dist/validator/validatorAll/validatorNavigation.core.js +64 -10
  51. package/dist/validator/validatorDocument.core.d.ts +3 -2
  52. package/dist/validator/validatorDocument.core.js +197 -15
  53. package/dist/validator/validatorDuplicates.core.d.ts +1 -1
  54. package/dist/validator/validatorDuplicates.core.js +4 -6
  55. package/dist/validator/validatorLinks.core.d.ts +22 -4
  56. package/dist/validator/validatorLinks.core.js +80 -44
  57. package/package.json +24 -15
  58. package/dist/docsTypes/documentationContent.d.ts.map +0 -1
  59. package/dist/docsTypes/documentationContent.js.map +0 -1
  60. package/dist/docsTypes/documentationDocument.d.ts.map +0 -1
  61. package/dist/docsTypes/documentationDocument.js.map +0 -1
  62. package/dist/docsTypes/documentationMetadata.d.ts.map +0 -1
  63. package/dist/docsTypes/documentationMetadata.js.map +0 -1
  64. package/dist/docsTypes/documentationNavigation.d.ts.map +0 -1
  65. package/dist/docsTypes/documentationNavigation.js.map +0 -1
  66. package/dist/docsTypes/documentationProvider.d.ts.map +0 -1
  67. package/dist/docsTypes/documentationProvider.js.map +0 -1
  68. package/dist/docsTypes/index.d.ts.map +0 -1
  69. package/dist/docsTypes/index.js.map +0 -1
  70. package/dist/document/documentBuilder.core.d.ts.map +0 -1
  71. package/dist/document/documentBuilder.core.js.map +0 -1
  72. package/dist/document/index.d.ts.map +0 -1
  73. package/dist/document/index.js.map +0 -1
  74. package/dist/examples/examples.core.d.ts.map +0 -1
  75. package/dist/examples/examples.core.js.map +0 -1
  76. package/dist/examples/index.d.ts.map +0 -1
  77. package/dist/examples/index.js.map +0 -1
  78. package/dist/frontmatter/frontmatter.core.d.ts.map +0 -1
  79. package/dist/frontmatter/frontmatter.core.js.map +0 -1
  80. package/dist/frontmatter/frontmatter.parser.d.ts.map +0 -1
  81. package/dist/frontmatter/frontmatter.parser.js.map +0 -1
  82. package/dist/frontmatter/frontmatter.serializer.d.ts.map +0 -1
  83. package/dist/frontmatter/frontmatter.serializer.js.map +0 -1
  84. package/dist/frontmatter/frontmatter.types.d.ts.map +0 -1
  85. package/dist/frontmatter/frontmatter.types.js.map +0 -1
  86. package/dist/frontmatter/index.d.ts.map +0 -1
  87. package/dist/frontmatter/index.js.map +0 -1
  88. package/dist/generator/generator.types.d.ts.map +0 -1
  89. package/dist/generator/generator.types.js.map +0 -1
  90. package/dist/generator/generatorJson.core.d.ts.map +0 -1
  91. package/dist/generator/generatorJson.core.js.map +0 -1
  92. package/dist/generator/generatorMarkdown.core.d.ts.map +0 -1
  93. package/dist/generator/generatorMarkdown.core.js.map +0 -1
  94. package/dist/generator/generatorMarkdownNodes.d.ts.map +0 -1
  95. package/dist/generator/generatorMarkdownNodes.js.map +0 -1
  96. package/dist/generator/index.d.ts.map +0 -1
  97. package/dist/generator/index.js.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/navigation/index.d.ts.map +0 -1
  101. package/dist/navigation/index.js.map +0 -1
  102. package/dist/navigation/navigation.core.d.ts.map +0 -1
  103. package/dist/navigation/navigation.core.js.map +0 -1
  104. package/dist/registry/index.d.ts.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/registry/registry.core.d.ts.map +0 -1
  107. package/dist/registry/registry.core.js.map +0 -1
  108. package/dist/utils/index.d.ts.map +0 -1
  109. package/dist/utils/index.js.map +0 -1
  110. package/dist/utils/utils.helper.d.ts.map +0 -1
  111. package/dist/utils/utils.helper.js.map +0 -1
  112. package/dist/validator/index.d.ts.map +0 -1
  113. package/dist/validator/index.js.map +0 -1
  114. package/dist/validator/validator.types.d.ts.map +0 -1
  115. package/dist/validator/validator.types.js.map +0 -1
  116. package/dist/validator/validatorAll/index.d.ts.map +0 -1
  117. package/dist/validator/validatorAll/index.js.map +0 -1
  118. package/dist/validator/validatorAll/validatorAll.core.d.ts.map +0 -1
  119. package/dist/validator/validatorAll/validatorAll.core.js.map +0 -1
  120. package/dist/validator/validatorAll/validatorNavigation.core.d.ts.map +0 -1
  121. package/dist/validator/validatorAll/validatorNavigation.core.js.map +0 -1
  122. package/dist/validator/validatorDocument.core.d.ts.map +0 -1
  123. package/dist/validator/validatorDocument.core.js.map +0 -1
  124. package/dist/validator/validatorDuplicates.core.d.ts.map +0 -1
  125. package/dist/validator/validatorDuplicates.core.js.map +0 -1
  126. package/dist/validator/validatorLinks.core.d.ts.map +0 -1
  127. package/dist/validator/validatorLinks.core.js.map +0 -1
@@ -1,9 +1,21 @@
1
1
  /**
2
2
  * Frontmatter serializer — converts metadata back to YAML-like format.
3
+ *
4
+ * Every value is quoted when it could otherwise be misread by the
5
+ * parser (newlines, `:`/`#`, leading list markers, surrounding
6
+ * whitespace, or text that looks like a number/boolean/null), so
7
+ * `parseFrontmatter(serializeFrontmatter(m, c))` round-trips `m`.
3
8
  */
4
9
  import type { FrontmatterMetadata } from "./frontmatter.types.js";
5
10
  /**
6
11
  * Serializes metadata back into a frontmatter string.
12
+ *
13
+ * Nested plain objects are written as a one-level mapping; other
14
+ * non-scalar values (Dates, class instances) are written as their
15
+ * JSON/ISO representation in quotes. Keys that are not valid
16
+ * frontmatter keys are skipped.
7
17
  */
8
- export declare function serializeFrontmatter(metadata: FrontmatterMetadata, content: string): string;
18
+ export declare function serializeFrontmatter(metadata: FrontmatterMetadata | Readonly<Record<string, unknown>>, content: string): string;
19
+ /** Formats a single scalar, quoting whenever the parser could misread it. */
20
+ export declare function formatScalar(value: unknown): string;
9
21
  //# sourceMappingURL=frontmatter.serializer.d.ts.map
@@ -1,26 +1,49 @@
1
1
  /**
2
2
  * Frontmatter serializer — converts metadata back to YAML-like format.
3
+ *
4
+ * Every value is quoted when it could otherwise be misread by the
5
+ * parser (newlines, `:`/`#`, leading list markers, surrounding
6
+ * whitespace, or text that looks like a number/boolean/null), so
7
+ * `parseFrontmatter(serializeFrontmatter(m, c))` round-trips `m`.
3
8
  */
4
9
  const FRONTMATTER_DELIMITER = "---";
10
+ /** Keys that are never written. */
11
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
12
+ /** Valid frontmatter key syntax (must match the parser). */
13
+ const KEY_PATTERN = /^[A-Za-z_][\w.-]*$/;
5
14
  /**
6
15
  * Serializes metadata back into a frontmatter string.
16
+ *
17
+ * Nested plain objects are written as a one-level mapping; other
18
+ * non-scalar values (Dates, class instances) are written as their
19
+ * JSON/ISO representation in quotes. Keys that are not valid
20
+ * frontmatter keys are skipped.
7
21
  */
8
22
  export function serializeFrontmatter(metadata, content) {
9
23
  const lines = [FRONTMATTER_DELIMITER];
10
24
  for (const [key, value] of Object.entries(metadata)) {
11
25
  if (value === undefined || value === null)
12
26
  continue;
27
+ if (!isSerializableKey(key))
28
+ continue;
13
29
  if (Array.isArray(value)) {
14
30
  lines.push(`${key}:`);
15
31
  for (const item of value) {
16
- lines.push(` - ${item}`);
32
+ lines.push(` - ${formatScalar(item)}`);
17
33
  }
18
34
  }
19
- else if (typeof value === "boolean") {
20
- lines.push(`${key}: ${value ? "true" : "false"}`);
35
+ else if (isPlainObject(value)) {
36
+ lines.push(`${key}:`);
37
+ for (const [subKey, subValue] of Object.entries(value)) {
38
+ if (subValue === undefined || subValue === null)
39
+ continue;
40
+ if (!isSerializableKey(subKey))
41
+ continue;
42
+ lines.push(` ${subKey}: ${formatScalar(subValue)}`);
43
+ }
21
44
  }
22
45
  else {
23
- lines.push(`${key}: ${value}`);
46
+ lines.push(`${key}: ${formatScalar(value)}`);
24
47
  }
25
48
  }
26
49
  lines.push(FRONTMATTER_DELIMITER);
@@ -28,4 +51,66 @@ export function serializeFrontmatter(metadata, content) {
28
51
  lines.push(content);
29
52
  return lines.join("\n");
30
53
  }
54
+ /** Formats a single scalar, quoting whenever the parser could misread it. */
55
+ export function formatScalar(value) {
56
+ if (typeof value === "boolean")
57
+ return value ? "true" : "false";
58
+ if (typeof value === "number") {
59
+ return Number.isFinite(value) ? String(value) : quote(String(value));
60
+ }
61
+ if (typeof value === "string") {
62
+ return needsQuotes(value) ? quote(value) : value;
63
+ }
64
+ if (value instanceof Date) {
65
+ return quote(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString());
66
+ }
67
+ if (value === null || value === undefined)
68
+ return quote("");
69
+ try {
70
+ return quote(JSON.stringify(value) ?? String(value));
71
+ }
72
+ catch {
73
+ return quote(String(value));
74
+ }
75
+ }
76
+ function needsQuotes(value) {
77
+ if (value === "")
78
+ return true;
79
+ if (value !== value.trim())
80
+ return true;
81
+ if (/[\n\r\t"'\\]/.test(value))
82
+ return true;
83
+ if (/(^|\s)#/.test(value))
84
+ return true;
85
+ if (/:(\s|$)/.test(value))
86
+ return true;
87
+ if (/^[-?[\]{}*&!|>%@`,]/.test(value))
88
+ return true;
89
+ if (value === "true" || value === "false")
90
+ return true;
91
+ if (value === "null" || value === "~")
92
+ return true;
93
+ if (/^-?(0|[1-9]\d*)(\.\d+)?$/.test(value))
94
+ return true;
95
+ return false;
96
+ }
97
+ function quote(value) {
98
+ return ('"' +
99
+ value
100
+ .replace(/\\/g, "\\\\")
101
+ .replace(/"/g, '\\"')
102
+ .replace(/\n/g, "\\n")
103
+ .replace(/\r/g, "\\r")
104
+ .replace(/\t/g, "\\t") +
105
+ '"');
106
+ }
107
+ function isSerializableKey(key) {
108
+ return !FORBIDDEN_KEYS.has(key) && KEY_PATTERN.test(key);
109
+ }
110
+ function isPlainObject(value) {
111
+ if (typeof value !== "object" || value === null)
112
+ return false;
113
+ const proto = Object.getPrototypeOf(value);
114
+ return proto === Object.prototype || proto === null;
115
+ }
31
116
  //# sourceMappingURL=frontmatter.serializer.js.map
@@ -8,8 +8,17 @@ export interface ParsedFrontmatter {
8
8
  readonly metadata: FrontmatterMetadata;
9
9
  readonly content: string;
10
10
  }
11
+ /** Scalar value produced by the frontmatter parser. */
12
+ export type FrontmatterScalar = string | number | boolean | null;
13
+ /** Any value the frontmatter parser can produce for a key. */
14
+ export type FrontmatterValue = FrontmatterScalar | readonly FrontmatterScalar[] | Readonly<Record<string, FrontmatterScalar>>;
11
15
  /**
12
16
  * Metadata extracted from frontmatter.
17
+ *
18
+ * The named keys below are always returned with the declared type
19
+ * (`version: 1.0` parses to the string `"1.0"`, `tags` items are
20
+ * strings). Unknown keys receive the parser's best-effort scalar,
21
+ * list, or one-level mapping.
13
22
  */
14
23
  export interface FrontmatterMetadata {
15
24
  readonly title?: string;
@@ -21,6 +30,6 @@ export interface FrontmatterMetadata {
21
30
  readonly deprecated?: boolean;
22
31
  readonly deprecatedMessage?: string;
23
32
  readonly visibility?: string;
24
- readonly [key: string]: unknown;
33
+ readonly [key: string]: FrontmatterValue | undefined;
25
34
  }
26
35
  //# sourceMappingURL=frontmatter.types.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * YAML frontmatter parsing and serialization for markdown documentation.
5
5
  */
6
6
  export { parseFrontmatter, serializeFrontmatter } from "./frontmatter.core.js";
7
- export type { ParsedFrontmatter, FrontmatterMetadata, } from "./frontmatter.core.js";
7
+ export type { ParsedFrontmatter, FrontmatterMetadata, FrontmatterScalar, FrontmatterValue, } from "./frontmatter.core.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,11 +1,29 @@
1
1
  /**
2
2
  * Type definitions for documentation generators.
3
3
  */
4
+ import type { DocumentationSanitizer } from "../docsTypes/index.js";
5
+ import type { DocumentVisibilityFilter } from "../registry/registry.core.js";
4
6
  /**
5
7
  * Options for markdown generation.
6
8
  */
7
9
  export interface MarkdownGeneratorOptions {
8
10
  readonly includeFrontmatter?: boolean;
9
11
  readonly includeMeta?: boolean;
12
+ /**
13
+ * Applied to `html` and `mdx` content before it is emitted. Markdown
14
+ * and structured content are not passed through the sanitizer.
15
+ */
16
+ readonly sanitizer?: DocumentationSanitizer;
17
+ }
18
+ /**
19
+ * Options for JSON index generation.
20
+ */
21
+ export interface IndexGeneratorOptions {
22
+ /**
23
+ * Which documents to include. Defaults to `"CLIENT"` so that
24
+ * `visibility: "SERVER"` documents never reach a client-side index
25
+ * unless explicitly requested with `"ALL"` or `"SERVER"`.
26
+ */
27
+ readonly visibility?: DocumentVisibilityFilter;
10
28
  }
11
29
  //# sourceMappingURL=generator.types.d.ts.map
@@ -2,12 +2,17 @@
2
2
  * JSON output generation for documentation documents.
3
3
  */
4
4
  import type { DocumentationDocument } from "../docsTypes/index.js";
5
+ import type { IndexGeneratorOptions } from "./generator.types.js";
5
6
  /**
6
- * Generates a JSON representation of a document.
7
+ * Generates a JSON representation of a document, including its
8
+ * `visibility`, `deprecated` and `deprecatedMessage` fields.
7
9
  */
8
10
  export declare function generateJSON(document: DocumentationDocument): Record<string, unknown>;
9
11
  /**
10
- * Generates a JSON index for an entire registry of documents.
12
+ * Generates a JSON index for a set of documents.
13
+ *
14
+ * By default only client-visible documents are included (see
15
+ * `IndexGeneratorOptions.visibility`).
11
16
  */
12
- export declare function generateIndex(documents: readonly DocumentationDocument[]): Record<string, unknown>[];
17
+ export declare function generateIndex(documents: readonly DocumentationDocument[], options?: IndexGeneratorOptions): Record<string, unknown>[];
13
18
  //# sourceMappingURL=generatorJson.core.d.ts.map
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * JSON output generation for documentation documents.
3
3
  */
4
+ import { matchesVisibility } from "../registry/registry.core.js";
4
5
  /**
5
- * Generates a JSON representation of a document.
6
+ * Generates a JSON representation of a document, including its
7
+ * `visibility`, `deprecated` and `deprecatedMessage` fields.
6
8
  */
7
9
  export function generateJSON(document) {
8
10
  return {
@@ -13,6 +15,7 @@ export function generateJSON(document) {
13
15
  tags: document.tags,
14
16
  version: document.version,
15
17
  status: document.status,
18
+ visibility: document.visibility,
16
19
  deprecated: document.deprecated,
17
20
  deprecatedMessage: document.deprecatedMessage,
18
21
  content: document.content,
@@ -20,10 +23,16 @@ export function generateJSON(document) {
20
23
  };
21
24
  }
22
25
  /**
23
- * Generates a JSON index for an entire registry of documents.
26
+ * Generates a JSON index for a set of documents.
27
+ *
28
+ * By default only client-visible documents are included (see
29
+ * `IndexGeneratorOptions.visibility`).
24
30
  */
25
- export function generateIndex(documents) {
26
- return documents.map((doc) => ({
31
+ export function generateIndex(documents, options = {}) {
32
+ const filter = options.visibility ?? "CLIENT";
33
+ return documents
34
+ .filter((doc) => matchesVisibility(doc, filter))
35
+ .map((doc) => ({
27
36
  id: doc.id,
28
37
  title: doc.title,
29
38
  description: doc.description,
@@ -31,6 +40,9 @@ export function generateIndex(documents) {
31
40
  tags: doc.tags,
32
41
  version: doc.version,
33
42
  status: doc.status,
43
+ visibility: doc.visibility,
44
+ deprecated: doc.deprecated,
45
+ deprecatedMessage: doc.deprecatedMessage,
34
46
  }));
35
47
  }
36
48
  //# sourceMappingURL=generatorJson.core.js.map
@@ -5,6 +5,10 @@ import type { DocumentationDocument } from "../docsTypes/index.js";
5
5
  import type { MarkdownGeneratorOptions } from "./generator.types.js";
6
6
  /**
7
7
  * Generates a markdown string from a document.
8
+ *
9
+ * Frontmatter values are quoted whenever they could be misread
10
+ * (newlines, `:`/`#`, leading `-`), so untrusted titles, descriptions
11
+ * and tags cannot inject metadata keys or terminate the block.
8
12
  */
9
13
  export declare function generateMarkdown(document: DocumentationDocument, options?: MarkdownGeneratorOptions): string;
10
14
  //# sourceMappingURL=generatorMarkdown.core.d.ts.map
@@ -1,33 +1,47 @@
1
1
  /**
2
2
  * Markdown output generation for documentation documents.
3
3
  */
4
+ import { formatScalar } from "../frontmatter/frontmatter.serializer.js";
4
5
  import { nodesToMarkdown } from "./generatorMarkdownNodes.js";
5
6
  /**
6
7
  * Generates a markdown string from a document.
8
+ *
9
+ * Frontmatter values are quoted whenever they could be misread
10
+ * (newlines, `:`/`#`, leading `-`), so untrusted titles, descriptions
11
+ * and tags cannot inject metadata keys or terminate the block.
7
12
  */
8
13
  export function generateMarkdown(document, options = {}) {
9
- const { includeFrontmatter = true, includeMeta = false } = options;
14
+ const { includeFrontmatter = true, includeMeta = false, sanitizer } = options;
10
15
  const lines = [];
11
16
  if (includeFrontmatter) {
12
17
  lines.push("---");
13
- lines.push(`title: ${document.title}`);
18
+ lines.push(`title: ${formatScalar(document.title)}`);
14
19
  if (document.description) {
15
- lines.push(`description: ${document.description}`);
20
+ lines.push(`description: ${formatScalar(document.description)}`);
16
21
  }
17
22
  if (document.category) {
18
- lines.push(`category: ${document.category}`);
23
+ lines.push(`category: ${formatScalar(document.category)}`);
19
24
  }
20
25
  if (document.tags && document.tags.length > 0) {
21
26
  lines.push("tags:");
22
27
  for (const tag of document.tags) {
23
- lines.push(` - ${tag}`);
28
+ lines.push(` - ${formatScalar(tag)}`);
24
29
  }
25
30
  }
26
31
  if (document.version) {
27
- lines.push(`version: ${document.version}`);
32
+ lines.push(`version: ${formatScalar(document.version)}`);
28
33
  }
29
34
  if (document.status) {
30
- lines.push(`status: ${document.status}`);
35
+ lines.push(`status: ${formatScalar(document.status)}`);
36
+ }
37
+ if (document.visibility) {
38
+ lines.push(`visibility: ${formatScalar(document.visibility)}`);
39
+ }
40
+ if (document.deprecated !== undefined) {
41
+ lines.push(`deprecated: ${formatScalar(document.deprecated)}`);
42
+ }
43
+ if (document.deprecatedMessage) {
44
+ lines.push(`deprecatedMessage: ${formatScalar(document.deprecatedMessage)}`);
31
45
  }
32
46
  lines.push("---");
33
47
  lines.push("");
@@ -36,17 +50,19 @@ export function generateMarkdown(document, options = {}) {
36
50
  lines.push("> **DEPRECATED:**");
37
51
  if (document.deprecatedMessage) {
38
52
  lines.push(">");
39
- lines.push(`> ${document.deprecatedMessage}`);
53
+ for (const line of document.deprecatedMessage.split(/\r?\n/)) {
54
+ lines.push(`> ${line}`);
55
+ }
40
56
  }
41
57
  lines.push("");
42
58
  }
43
- lines.push(contentToMarkdown(document.content));
59
+ lines.push(contentToMarkdown(document.content, sanitizer));
44
60
  if (includeMeta && document.metadata) {
45
61
  lines.push("");
46
62
  lines.push("---");
47
63
  lines.push("");
48
64
  if (document.metadata.owner) {
49
- lines.push(`**Owner:** ${document.metadata.owner}`);
65
+ lines.push(`**Owner:** ${escapeInline(document.metadata.owner)}`);
50
66
  }
51
67
  if (document.metadata.updatedAt) {
52
68
  lines.push(`**Updated:** ${document.metadata.updatedAt.toISOString()}`);
@@ -57,16 +73,19 @@ export function generateMarkdown(document, options = {}) {
57
73
  /**
58
74
  * Converts content to markdown string.
59
75
  */
60
- function contentToMarkdown(content) {
76
+ function contentToMarkdown(content, sanitizer) {
61
77
  switch (content.type) {
62
78
  case "markdown":
63
79
  return content.value;
64
80
  case "html":
65
- return content.value;
66
81
  case "mdx":
67
- return content.value;
82
+ return sanitizer ? sanitizer.sanitize(content.value) : content.value;
68
83
  case "structured":
69
84
  return nodesToMarkdown(content.nodes);
70
85
  }
71
86
  }
87
+ /** Collapses newlines so a value cannot break out of its line. */
88
+ function escapeInline(value) {
89
+ return value.replace(/\r?\n/g, " ");
90
+ }
72
91
  //# sourceMappingURL=generatorMarkdown.core.js.map
@@ -1,9 +1,29 @@
1
1
  /**
2
2
  * Converts structured AST nodes to markdown strings.
3
+ *
4
+ * Every value is escaped for the position it is written to, so
5
+ * untrusted node content cannot break out of a table, code fence,
6
+ * heading or link.
3
7
  */
4
8
  import type { DocumentationNode } from "../docsTypes/index.js";
5
9
  /**
6
10
  * Converts a list of documentation nodes to markdown.
11
+ *
12
+ * @throws {TypeError} for node types that are not part of `DocumentationNode`.
7
13
  */
8
14
  export declare function nodesToMarkdown(nodes: readonly DocumentationNode[]): string;
15
+ /** Clamps a heading level to 1–6 (non-integers become 1). */
16
+ export declare function clampHeadingLevel(level: number): number;
17
+ /**
18
+ * Returns a backtick fence longer than any backtick run in `value`,
19
+ * so the code block cannot be closed early by its own content.
20
+ */
21
+ export declare function fenceFor(value: string): string;
22
+ /**
23
+ * Keeps only the first whitespace-delimited token of the language and
24
+ * only characters that are safe on a fence info line.
25
+ */
26
+ export declare function sanitizeLanguage(language: string | undefined): string;
27
+ /** Escapes `|` and newlines so a value stays inside its table cell. */
28
+ export declare function tableCell(value: string): string;
9
29
  //# sourceMappingURL=generatorMarkdownNodes.d.ts.map
@@ -1,64 +1,134 @@
1
1
  /**
2
2
  * Converts structured AST nodes to markdown strings.
3
+ *
4
+ * Every value is escaped for the position it is written to, so
5
+ * untrusted node content cannot break out of a table, code fence,
6
+ * heading or link.
3
7
  */
8
+ const CALLOUT_LABELS = {
9
+ note: "NOTE",
10
+ warning: "WARNING",
11
+ tip: "TIP",
12
+ danger: "DANGER",
13
+ };
4
14
  /**
5
15
  * Converts a list of documentation nodes to markdown.
16
+ *
17
+ * @throws {TypeError} for node types that are not part of `DocumentationNode`.
6
18
  */
7
19
  export function nodesToMarkdown(nodes) {
8
20
  const lines = [];
9
21
  for (const node of nodes) {
10
22
  switch (node.type) {
11
- case "heading":
12
- lines.push(`${"#".repeat(node.level)} ${node.value}`);
23
+ case "heading": {
24
+ const level = clampHeadingLevel(node.level);
25
+ lines.push(`${"#".repeat(level)} ${singleLine(node.value)}`);
13
26
  lines.push("");
14
27
  break;
28
+ }
15
29
  case "paragraph":
16
30
  lines.push(node.value);
17
31
  lines.push("");
18
32
  break;
19
- case "code":
20
- lines.push("```" + (node.language ?? ""));
33
+ case "code": {
34
+ const fence = fenceFor(node.value);
35
+ lines.push(fence + sanitizeLanguage(node.language));
21
36
  lines.push(node.value);
22
- lines.push("```");
37
+ lines.push(fence);
23
38
  lines.push("");
24
39
  break;
40
+ }
25
41
  case "list":
26
42
  for (let i = 0; i < node.items.length; i++) {
27
43
  const prefix = node.ordered ? `${i + 1}. ` : "- ";
28
- lines.push(`${prefix}${node.items[i]}`);
44
+ lines.push(`${prefix}${listItem(node.items[i] ?? "")}`);
29
45
  }
30
46
  lines.push("");
31
47
  break;
32
48
  case "link":
33
- lines.push(`[${node.value}](${node.href})`);
49
+ lines.push(`[${escapeLinkText(node.value)}](${escapeLinkHref(node.href)})`);
34
50
  lines.push("");
35
51
  break;
36
52
  case "table": {
37
- lines.push("| " + node.headers.join(" | ") + " |");
53
+ lines.push("| " + node.headers.map(tableCell).join(" | ") + " |");
38
54
  lines.push("| " + node.headers.map(() => "---").join(" | ") + " |");
39
55
  for (const row of node.rows) {
40
- lines.push("| " + row.join(" | ") + " |");
56
+ lines.push("| " + row.map(tableCell).join(" | ") + " |");
41
57
  }
42
58
  lines.push("");
43
59
  break;
44
60
  }
45
61
  case "quote":
46
- lines.push(`> ${node.value}`);
62
+ for (const line of node.value.split(/\r?\n/)) {
63
+ lines.push(`> ${line}`);
64
+ }
47
65
  lines.push("");
48
66
  break;
49
67
  case "callout": {
50
- const labels = {
51
- note: "NOTE",
52
- warning: "WARNING",
53
- tip: "TIP",
54
- danger: "DANGER",
55
- };
56
- lines.push(`> **${labels[node.kind] ?? node.kind.toUpperCase()}:** ${node.value}`);
68
+ const label = CALLOUT_LABELS[node.kind] ?? singleLine(String(node.kind)).toUpperCase();
69
+ const [first = "", ...rest] = node.value.split(/\r?\n/);
70
+ lines.push(`> **${label}:** ${first}`);
71
+ for (const line of rest) {
72
+ lines.push(`> ${line}`);
73
+ }
57
74
  lines.push("");
58
75
  break;
59
76
  }
77
+ default: {
78
+ const unknown = node;
79
+ throw new TypeError(`Unknown documentation node type "${String(unknown.type)}".`);
80
+ }
60
81
  }
61
82
  }
62
83
  return lines.join("\n");
63
84
  }
85
+ /** Clamps a heading level to 1–6 (non-integers become 1). */
86
+ export function clampHeadingLevel(level) {
87
+ if (!Number.isInteger(level))
88
+ return 1;
89
+ return Math.min(6, Math.max(1, level));
90
+ }
91
+ /**
92
+ * Returns a backtick fence longer than any backtick run in `value`,
93
+ * so the code block cannot be closed early by its own content.
94
+ */
95
+ export function fenceFor(value) {
96
+ let longest = 0;
97
+ for (const run of value.match(/`+/g) ?? []) {
98
+ longest = Math.max(longest, run.length);
99
+ }
100
+ return "`".repeat(Math.max(3, longest + 1));
101
+ }
102
+ /**
103
+ * Keeps only the first whitespace-delimited token of the language and
104
+ * only characters that are safe on a fence info line.
105
+ */
106
+ export function sanitizeLanguage(language) {
107
+ if (!language)
108
+ return "";
109
+ const first = language.trim().split(/\s+/)[0] ?? "";
110
+ return first.replace(/[^\w+#.-]/g, "");
111
+ }
112
+ /** Escapes `|` and newlines so a value stays inside its table cell. */
113
+ export function tableCell(value) {
114
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
115
+ }
116
+ function singleLine(value) {
117
+ return value.replace(/\r?\n/g, " ");
118
+ }
119
+ function listItem(value) {
120
+ return value.replace(/\r?\n/g, "\n ");
121
+ }
122
+ function escapeLinkText(value) {
123
+ // Backslash first: escaping it after the brackets would turn the escapes we
124
+ // just added back into a literal backslash plus an unescaped bracket.
125
+ return singleLine(value)
126
+ .replace(/\\/g, "\\\\")
127
+ .replace(/\[/g, "\\[")
128
+ .replace(/\]/g, "\\]");
129
+ }
130
+ function escapeLinkHref(href) {
131
+ const clean = href.replace(/[\r\n]/g, "");
132
+ return /[\s()]/.test(clean) ? `<${clean.replace(/[<>]/g, "")}>` : clean;
133
+ }
64
134
  //# sourceMappingURL=generatorMarkdownNodes.js.map
@@ -4,7 +4,7 @@
4
4
  * Output generators — markdown and JSON for documentation.
5
5
  */
6
6
  export { generateMarkdown } from "./generatorMarkdown.core.js";
7
- export { nodesToMarkdown } from "./generatorMarkdownNodes.js";
7
+ export { nodesToMarkdown, clampHeadingLevel, fenceFor, sanitizeLanguage, tableCell, } from "./generatorMarkdownNodes.js";
8
8
  export { generateJSON, generateIndex } from "./generatorJson.core.js";
9
- export type { MarkdownGeneratorOptions } from "./generator.types.js";
9
+ export type { MarkdownGeneratorOptions, IndexGeneratorOptions, } from "./generator.types.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,6 @@
4
4
  * Output generators — markdown and JSON for documentation.
5
5
  */
6
6
  export { generateMarkdown } from "./generatorMarkdown.core.js";
7
- export { nodesToMarkdown } from "./generatorMarkdownNodes.js";
7
+ export { nodesToMarkdown, clampHeadingLevel, fenceFor, sanitizeLanguage, tableCell, } from "./generatorMarkdownNodes.js";
8
8
  export { generateJSON, generateIndex } from "./generatorJson.core.js";
9
9
  //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -16,4 +16,5 @@ export * from "./navigation/index.js";
16
16
  export * from "./examples/index.js";
17
17
  export * from "./generator/index.js";
18
18
  export * from "./utils/index.js";
19
+ export * from "./errors/index.js";
19
20
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -25,4 +25,6 @@ export * from "./examples/index.js";
25
25
  export * from "./generator/index.js";
26
26
  // Utils
27
27
  export * from "./utils/index.js";
28
+ // Errors (re-exported from @zudojs/errors)
29
+ export * from "./errors/index.js";
28
30
  //# sourceMappingURL=index.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Navigation tree utilities — breadcrumbs, flattening, sibling resolution.
5
5
  */
6
- export { getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, } from "./navigation.core.js";
6
+ export { getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, getAdjacent, } from "./navigation.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Navigation tree utilities — breadcrumbs, flattening, sibling resolution.
5
5
  */
6
- export { getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, } from "./navigation.core.js";
6
+ export { getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, getAdjacent, } from "./navigation.core.js";
7
7
  //# sourceMappingURL=index.js.map