@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,34 +1,64 @@
1
1
  /**
2
2
  * Utility helpers for the documentation package.
3
3
  */
4
+ /** Characters allowed in one segment of a document ID. */
5
+ const ID_SEGMENT = /^[A-Za-z0-9_-]+$/;
6
+ /**
7
+ * Returns true when `id` is a stable dot-separated identifier such as
8
+ * `guides.http.routing`: non-empty segments of letters, digits, `_`
9
+ * and `-`, with no whitespace or empty segments.
10
+ */
11
+ export function isValidDocumentId(id) {
12
+ if (typeof id !== "string" || id.length === 0)
13
+ return false;
14
+ return id.split(".").every((segment) => ID_SEGMENT.test(segment));
15
+ }
4
16
  /**
5
17
  * Normalizes a document ID to a consistent format.
6
- * Strips leading/trailing dots, collapses consecutive dots.
18
+ * Trims whitespace, collapses consecutive dots and strips leading/trailing dots.
7
19
  */
8
20
  export function normalizeDocumentId(id) {
9
- return id
21
+ return (id
22
+ .trim()
10
23
  .replace(/\.+/g, ".")
11
- .replace(/^\.|\.$/g, "")
12
- .trim();
24
+ // Runs are already collapsed, so at most one dot can sit at each end.
25
+ // Matching a single dot keeps this linear; `\.+$` would backtrack.
26
+ .replace(/^\.|\.$/g, ""));
13
27
  }
14
28
  /**
15
29
  * Generates a document ID from a file path.
16
- * Converts path separators to dots and removes extension.
30
+ * Converts path separators to dots and removes the extension of the
31
+ * final segment only (`guides/v1.2/routing.md` → `guides.v1.2.routing`).
32
+ * Returns an empty string when the path has no usable segment
33
+ * (e.g. `.gitignore`).
17
34
  */
18
35
  export function documentIdFromPath(path) {
19
- return normalizeDocumentId(path.replace(/\.[^.]+$/, "").replace(/[/\\]/g, "."));
36
+ const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
37
+ const segments = normalizedPath.split("/").filter((s) => s.length > 0);
38
+ if (segments.length === 0)
39
+ return "";
40
+ const last = segments[segments.length - 1] ?? "";
41
+ const stem = last.replace(/\.[^.]+$/, "");
42
+ if (stem.length === 0)
43
+ return "";
44
+ segments[segments.length - 1] = stem;
45
+ return normalizeDocumentId(segments.join("."));
20
46
  }
21
47
  /**
22
48
  * Resolves a relative document link against a base ID.
23
49
  * "guides.http" + "./routing" → "guides.http.routing"
50
+ *
51
+ * Fragments (`#section`), `.md`/`.mdx` extensions and Windows
52
+ * separators are stripped before resolving.
24
53
  */
25
54
  export function resolveDocumentLink(baseId, link) {
26
- if (link.startsWith("/")) {
27
- return normalizeDocumentId(link.slice(1));
55
+ const cleaned = stripLinkDecorations(link);
56
+ if (cleaned.startsWith("/")) {
57
+ return normalizeDocumentId(cleaned.slice(1).replace(/\//g, "."));
28
58
  }
29
- const baseParts = baseId.split(".");
59
+ const baseParts = baseId.split(".").filter((p) => p.length > 0);
30
60
  baseParts.pop();
31
- const linkParts = link.split("/");
61
+ const linkParts = cleaned.split("/");
32
62
  for (const part of linkParts) {
33
63
  if (part === "..") {
34
64
  baseParts.pop();
@@ -40,43 +70,100 @@ export function resolveDocumentLink(baseId, link) {
40
70
  return normalizeDocumentId(baseParts.join("."));
41
71
  }
42
72
  /**
43
- * Extracts the title from markdown content (first heading).
73
+ * Removes a `#fragment`, a trailing `.md`/`.mdx` extension and
74
+ * converts `\` to `/` in a link target.
75
+ */
76
+ export function stripLinkDecorations(link) {
77
+ const withoutFragment = link.split("#")[0] ?? "";
78
+ return withoutFragment.replace(/\\/g, "/").replace(/\.mdx?$/i, "");
79
+ }
80
+ /**
81
+ * Removes fenced code blocks (``` or ~~~) from markdown so that
82
+ * headings and links inside them are not interpreted.
83
+ */
84
+ export function stripFencedCodeBlocks(markdown) {
85
+ return markdown.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[^\n]*$/gm, "");
86
+ }
87
+ /**
88
+ * Extracts the title from markdown content (first level-1 heading
89
+ * outside fenced code blocks).
44
90
  */
45
91
  export function extractTitleFromMarkdown(markdown) {
46
- const match = markdown.match(/^# ([^\n]+)$/m);
47
- return match?.[1]?.trim();
92
+ const match = stripFencedCodeBlocks(markdown).match(/^# ([^\n]+)$/m);
93
+ return match?.[1] ? cleanHeadingText(match[1]) : undefined;
48
94
  }
49
95
  /**
50
- * Extracts headings from markdown content.
96
+ * Extracts headings from markdown content (outside fenced code blocks).
51
97
  */
52
98
  export function extractHeadings(markdown) {
53
99
  const results = [];
54
100
  const pattern = /^(#{1,6}) ([^\n]+)$/gm;
101
+ const source = stripFencedCodeBlocks(markdown);
55
102
  let match;
56
- while ((match = pattern.exec(markdown)) !== null) {
103
+ while ((match = pattern.exec(source)) !== null) {
57
104
  const hashes = match[1];
58
105
  const headingText = match[2];
59
106
  if (hashes && headingText) {
60
107
  results.push({
61
108
  level: hashes.length,
62
- text: headingText.trim(),
109
+ text: cleanHeadingText(headingText),
63
110
  });
64
111
  }
65
112
  }
66
113
  return Object.freeze(results);
67
114
  }
115
+ /** Trims a heading and removes ATX closing hashes (`# Title #`). */
116
+ function cleanHeadingText(text) {
117
+ return text.replace(/\s+#+\s*$/, "").trim();
118
+ }
119
+ const HTML_TAG = /<\/?[a-zA-Z][^>\n]*>/g;
120
+ /**
121
+ * Removes HTML tags until the result stops changing. A single pass is not
122
+ * enough: stripping the inner tag of `<<b>b>x` re-forms `<b>x`, so a lone pass
123
+ * can leave a tag behind in text that is later rendered as HTML.
124
+ */
125
+ function stripHtmlTags(text) {
126
+ let current = text;
127
+ let previous;
128
+ do {
129
+ previous = current;
130
+ current = current.replace(HTML_TAG, "");
131
+ } while (current !== previous);
132
+ return current;
133
+ }
68
134
  /**
69
135
  * Strips markdown formatting to plain text.
70
136
  */
71
137
  export function stripMarkdown(markdown) {
72
- return markdown
138
+ return (stripHtmlTags(
139
+ // fenced code blocks: keep the code, drop the fences
140
+ markdown.replace(/^(`{3,}|~{3,})[^\n]*\n([\s\S]*?)^\1[^\n]*$/gm, "$2"))
141
+ // images before links
142
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
143
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
144
+ // headings
73
145
  .replace(/^#{1,6}\s+/gm, "")
74
- .replace(/\*\*([^*]+)\*\*/g, "$1")
75
- .replace(/\*([^*]+)\*/g, "$1")
146
+ .replace(/\s+#+\s*$/gm, "")
147
+ // emphasis (bold before italic, non-greedy)
148
+ .replace(/\*\*(.+?)\*\*/g, "$1")
149
+ .replace(/__(.+?)__/g, "$1")
150
+ .replace(/\*(.+?)\*/g, "$1")
151
+ .replace(/(^|[^\w])_(.+?)_(?=[^\w]|$)/g, "$1$2")
152
+ .replace(/~~(.+?)~~/g, "$1")
153
+ // inline code
76
154
  .replace(/`([^`]+)`/g, "$1")
77
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
155
+ // blockquotes, list markers
156
+ .replace(/^\s*>\s?/gm, "")
78
157
  .replace(/^\s*[-*+]\s+/gm, "")
79
158
  .replace(/^\s*\d+\.\s+/gm, "")
80
- .trim();
159
+ // tables: drop separator rows, unpipe cells
160
+ .replace(/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/gm, "")
161
+ .replace(/^\s*\|/gm, "")
162
+ .replace(/\|\s*$/gm, "")
163
+ .replace(/\s*\|\s*/g, " ")
164
+ // horizontal rules
165
+ .replace(/^\s*([-*_]\s*){3,}$/gm, "")
166
+ .replace(/\n{3,}/g, "\n\n")
167
+ .trim());
81
168
  }
82
169
  //# sourceMappingURL=utils.helper.js.map
@@ -5,7 +5,10 @@
5
5
  */
6
6
  export { validateDocument } from "./validatorDocument.core.js";
7
7
  export { validateNoDuplicateIds } from "./validatorDuplicates.core.js";
8
- export { validateLinks } from "./validatorLinks.core.js";
8
+ export { validateLinks, DEFAULT_MAX_LINK_SCAN_LENGTH, } from "./validatorLinks.core.js";
9
9
  export { validateAll, validateNavigation } from "./validatorAll/index.js";
10
+ export { toValidationResult } from "./validator.types.js";
10
11
  export type { ValidationIssue, ValidationResult } from "./validator.types.js";
12
+ export type { ValidateLinksOptions } from "./validatorLinks.core.js";
13
+ export type { ValidateAllOptions } from "./validatorAll/validatorAll.core.js";
11
14
  //# sourceMappingURL=index.d.ts.map
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export { validateDocument } from "./validatorDocument.core.js";
7
7
  export { validateNoDuplicateIds } from "./validatorDuplicates.core.js";
8
- export { validateLinks } from "./validatorLinks.core.js";
8
+ export { validateLinks, DEFAULT_MAX_LINK_SCAN_LENGTH, } from "./validatorLinks.core.js";
9
9
  export { validateAll, validateNavigation } from "./validatorAll/index.js";
10
+ export { toValidationResult } from "./validator.types.js";
10
11
  //# sourceMappingURL=index.js.map
@@ -12,9 +12,17 @@ export interface ValidationIssue {
12
12
  }
13
13
  /**
14
14
  * Result of validating documentation.
15
+ *
16
+ * `valid` is `false` only when at least one issue has severity
17
+ * `"error"`; warnings never make a result invalid.
15
18
  */
16
19
  export interface ValidationResult {
17
20
  readonly valid: boolean;
18
21
  readonly issues: readonly ValidationIssue[];
19
22
  }
23
+ /**
24
+ * Builds a `ValidationResult` from a list of issues using the single
25
+ * rule shared by every validator in this package.
26
+ */
27
+ export declare function toValidationResult(issues: readonly ValidationIssue[]): ValidationResult;
20
28
  //# sourceMappingURL=validator.types.d.ts.map
@@ -1,5 +1,14 @@
1
1
  /**
2
2
  * Type definitions for document validation.
3
3
  */
4
- export {};
4
+ /**
5
+ * Builds a `ValidationResult` from a list of issues using the single
6
+ * rule shared by every validator in this package.
7
+ */
8
+ export function toValidationResult(issues) {
9
+ return {
10
+ valid: !issues.some((issue) => issue.severity === "error"),
11
+ issues,
12
+ };
13
+ }
5
14
  //# sourceMappingURL=validator.types.js.map
@@ -2,9 +2,18 @@
2
2
  * Validates an entire documentation set.
3
3
  */
4
4
  import type { DocumentationDocument, DocumentationNavigationItem } from "../../docsTypes/index.js";
5
- import type { ValidationResult } from "../validator.types.js";
5
+ import { type ValidationResult } from "../validator.types.js";
6
+ import { type ValidateLinksOptions } from "../validatorLinks.core.js";
7
+ /** Options for `validateAll`. */
8
+ export interface ValidateAllOptions extends ValidateLinksOptions {
9
+ /**
10
+ * When a navigation tree is supplied, report documents that do not
11
+ * appear in it as `NAVIGATION_ORPHAN_DOCUMENT` warnings. Default true.
12
+ */
13
+ readonly reportOrphans?: boolean;
14
+ }
6
15
  /**
7
16
  * Validates all documents and optionally a navigation tree.
8
17
  */
9
- export declare function validateAll(documents: readonly DocumentationDocument[], navigation?: readonly DocumentationNavigationItem[]): ValidationResult;
18
+ export declare function validateAll(documents: readonly DocumentationDocument[], navigation?: readonly DocumentationNavigationItem[], options?: ValidateAllOptions): ValidationResult;
10
19
  //# sourceMappingURL=validatorAll.core.d.ts.map
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Validates an entire documentation set.
3
3
  */
4
+ import { flattenNavigation } from "../../navigation/navigation.core.js";
5
+ import { toValidationResult, } from "../validator.types.js";
4
6
  import { validateDocument } from "../validatorDocument.core.js";
5
7
  import { validateNoDuplicateIds } from "../validatorDuplicates.core.js";
6
- import { validateLinks } from "../validatorLinks.core.js";
8
+ import { validateLinks, } from "../validatorLinks.core.js";
7
9
  import { validateNavigation } from "./validatorNavigation.core.js";
8
10
  /**
9
11
  * Validates all documents and optionally a navigation tree.
10
12
  */
11
- export function validateAll(documents, navigation) {
13
+ export function validateAll(documents, navigation, options = {}) {
12
14
  const allIssues = [];
13
15
  const idResult = validateNoDuplicateIds(documents);
14
16
  allIssues.push(...idResult.issues);
@@ -16,16 +18,26 @@ export function validateAll(documents, navigation) {
16
18
  for (const doc of documents) {
17
19
  const docResult = validateDocument(doc);
18
20
  allIssues.push(...docResult.issues);
19
- const linkResult = validateLinks(doc, registeredIds);
21
+ const linkResult = validateLinks(doc, registeredIds, options);
20
22
  allIssues.push(...linkResult.issues);
21
23
  }
22
24
  if (navigation) {
23
25
  const navResult = validateNavigation(navigation, registeredIds);
24
26
  allIssues.push(...navResult.issues);
27
+ if (options.reportOrphans ?? true) {
28
+ const inNavigation = new Set(flattenNavigation(navigation));
29
+ for (const id of registeredIds) {
30
+ if (!inNavigation.has(id)) {
31
+ allIssues.push({
32
+ severity: "warning",
33
+ code: "NAVIGATION_ORPHAN_DOCUMENT",
34
+ message: `Document "${id}" is not reachable from the navigation.`,
35
+ documentId: id,
36
+ });
37
+ }
38
+ }
39
+ }
25
40
  }
26
- return {
27
- valid: allIssues.filter((i) => i.severity === "error").length === 0,
28
- issues: allIssues,
29
- };
41
+ return toValidationResult(allIssues);
30
42
  }
31
43
  //# sourceMappingURL=validatorAll.core.js.map
@@ -2,9 +2,16 @@
2
2
  * Validates navigation tree references against registered documents.
3
3
  */
4
4
  import type { DocumentationNavigationItem } from "../../docsTypes/index.js";
5
- import type { ValidationResult } from "../validator.types.js";
5
+ import { type ValidationResult } from "../validator.types.js";
6
6
  /**
7
7
  * Validates navigation tree references.
8
+ *
9
+ * Reports `NAVIGATION_UNKNOWN_DOCUMENT` (error) for references to
10
+ * unregistered documents, `NAVIGATION_CYCLE` (error) when a node is
11
+ * its own ancestor or the tree is deeper than `MAX_NAVIGATION_DEPTH`,
12
+ * `NAVIGATION_DUPLICATE_DOCUMENT` (warning) when a document is
13
+ * referenced more than once, and `NAVIGATION_EMPTY_ITEM` (warning)
14
+ * for items with neither `documentId` nor `children`.
8
15
  */
9
16
  export declare function validateNavigation(items: readonly DocumentationNavigationItem[], registeredIds: ReadonlySet<string>): ValidationResult;
10
17
  //# sourceMappingURL=validatorNavigation.core.d.ts.map
@@ -1,29 +1,83 @@
1
1
  /**
2
2
  * Validates navigation tree references against registered documents.
3
3
  */
4
+ import { MAX_NAVIGATION_DEPTH } from "../../navigation/navigation.core.js";
5
+ import { toValidationResult, } from "../validator.types.js";
4
6
  /**
5
7
  * Validates navigation tree references.
8
+ *
9
+ * Reports `NAVIGATION_UNKNOWN_DOCUMENT` (error) for references to
10
+ * unregistered documents, `NAVIGATION_CYCLE` (error) when a node is
11
+ * its own ancestor or the tree is deeper than `MAX_NAVIGATION_DEPTH`,
12
+ * `NAVIGATION_DUPLICATE_DOCUMENT` (warning) when a document is
13
+ * referenced more than once, and `NAVIGATION_EMPTY_ITEM` (warning)
14
+ * for items with neither `documentId` nor `children`.
6
15
  */
7
16
  export function validateNavigation(items, registeredIds) {
8
17
  const issues = [];
9
- function walk(nodes) {
18
+ const seenIds = new Set();
19
+ const visited = new WeakSet();
20
+ function walk(nodes, ancestors) {
21
+ if (ancestors.length > MAX_NAVIGATION_DEPTH) {
22
+ issues.push({
23
+ severity: "error",
24
+ code: "NAVIGATION_CYCLE",
25
+ message: `Navigation tree exceeds the maximum depth of ${MAX_NAVIGATION_DEPTH}.`,
26
+ });
27
+ return;
28
+ }
10
29
  for (const node of nodes) {
11
- if (node.documentId && !registeredIds.has(node.documentId)) {
30
+ if (!node || typeof node !== "object") {
31
+ issues.push({
32
+ severity: "error",
33
+ code: "NAVIGATION_EMPTY_ITEM",
34
+ message: "Navigation item is not an object.",
35
+ });
36
+ continue;
37
+ }
38
+ if (ancestors.includes(node)) {
12
39
  issues.push({
13
40
  severity: "error",
14
- code: "NAVIGATION_UNKNOWN_DOCUMENT",
15
- message: `Navigation item "${node.title}" references unknown document "${node.documentId}".`,
41
+ code: "NAVIGATION_CYCLE",
42
+ message: `Navigation item "${node.title}" is its own ancestor.`,
43
+ });
44
+ continue;
45
+ }
46
+ if (visited.has(node))
47
+ continue;
48
+ visited.add(node);
49
+ if (node.documentId) {
50
+ if (!registeredIds.has(node.documentId)) {
51
+ issues.push({
52
+ severity: "error",
53
+ code: "NAVIGATION_UNKNOWN_DOCUMENT",
54
+ message: `Navigation item "${node.title}" references unknown document "${node.documentId}".`,
55
+ documentId: node.documentId,
56
+ });
57
+ }
58
+ if (seenIds.has(node.documentId)) {
59
+ issues.push({
60
+ severity: "warning",
61
+ code: "NAVIGATION_DUPLICATE_DOCUMENT",
62
+ message: `Document "${node.documentId}" is referenced more than once in the navigation.`,
63
+ documentId: node.documentId,
64
+ });
65
+ }
66
+ seenIds.add(node.documentId);
67
+ }
68
+ else if (!node.children || node.children.length === 0) {
69
+ issues.push({
70
+ severity: "warning",
71
+ code: "NAVIGATION_EMPTY_ITEM",
72
+ message: `Navigation item "${node.title}" has neither a documentId nor children.`,
16
73
  });
17
74
  }
18
75
  if (node.children) {
19
- walk(node.children);
76
+ walk(node.children, [...ancestors, node]);
20
77
  }
21
78
  }
22
79
  }
23
- walk(items);
24
- return {
25
- valid: issues.length === 0,
26
- issues,
27
- };
80
+ walk(items, []);
81
+ return toValidationResult(issues);
28
82
  }
29
83
  //# sourceMappingURL=validatorNavigation.core.js.map
@@ -2,9 +2,10 @@
2
2
  * Validates a single document for structural correctness.
3
3
  */
4
4
  import type { DocumentationDocument } from "../docsTypes/index.js";
5
- import type { ValidationResult } from "./validator.types.js";
5
+ import { type ValidationResult } from "./validator.types.js";
6
6
  /**
7
- * Validates a single document.
7
+ * Validates a single document: ID syntax, required fields, content
8
+ * shape (including structured nodes), and the metadata enums.
8
9
  */
9
10
  export declare function validateDocument(document: DocumentationDocument): ValidationResult;
10
11
  //# sourceMappingURL=validatorDocument.core.d.ts.map