@zudojs/docs 1.0.6 → 1.1.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.
- package/README.md +4 -1
- package/dist/docsTypes/documentationContent.d.ts +0 -1
- package/dist/docsTypes/documentationContent.js +0 -1
- package/dist/docsTypes/documentationDocument.d.ts +0 -1
- package/dist/docsTypes/documentationDocument.js +0 -1
- package/dist/docsTypes/documentationMetadata.d.ts +5 -2
- package/dist/docsTypes/documentationMetadata.js +0 -1
- package/dist/docsTypes/documentationNavigation.d.ts +9 -3
- package/dist/docsTypes/documentationNavigation.js +0 -1
- package/dist/docsTypes/documentationProvider.d.ts +5 -2
- package/dist/docsTypes/documentationProvider.js +0 -1
- package/dist/docsTypes/index.d.ts +0 -1
- package/dist/docsTypes/index.js +0 -1
- package/dist/document/documentBuilder.convenience.d.ts +15 -0
- package/dist/document/documentBuilder.convenience.js +28 -0
- package/dist/document/documentBuilder.core.d.ts +18 -13
- package/dist/document/documentBuilder.core.js +16 -50
- package/dist/document/documentBuilder.normalize.d.ts +5 -1
- package/dist/document/documentBuilder.normalize.js +12 -1
- package/dist/document/documentBuilder.validate.d.ts +16 -0
- package/dist/document/documentBuilder.validate.js +36 -0
- package/dist/document/index.d.ts +2 -2
- package/dist/document/index.js +2 -2
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +0 -1
- package/dist/examples/examples.core.d.ts +0 -1
- package/dist/examples/examples.core.js +0 -1
- package/dist/examples/index.d.ts +0 -1
- package/dist/examples/index.js +0 -1
- package/dist/frontmatter/frontmatter.core.d.ts +0 -1
- package/dist/frontmatter/frontmatter.core.js +0 -1
- package/dist/frontmatter/frontmatter.parser.d.ts +0 -1
- package/dist/frontmatter/frontmatter.parser.js +0 -1
- package/dist/frontmatter/frontmatter.serializer.d.ts +0 -1
- package/dist/frontmatter/frontmatter.serializer.js +0 -1
- package/dist/frontmatter/frontmatter.types.d.ts +0 -1
- package/dist/frontmatter/frontmatter.types.js +0 -1
- package/dist/frontmatter/frontmatter.values.d.ts +0 -1
- package/dist/frontmatter/frontmatter.values.js +0 -1
- package/dist/frontmatter/index.d.ts +0 -1
- package/dist/frontmatter/index.js +0 -1
- package/dist/generator/generator.types.d.ts +0 -1
- package/dist/generator/generator.types.js +0 -1
- package/dist/generator/generatorJson.core.d.ts +0 -1
- package/dist/generator/generatorJson.core.js +0 -1
- package/dist/generator/generatorMarkdown.core.d.ts +0 -1
- package/dist/generator/generatorMarkdown.core.js +0 -1
- package/dist/generator/generatorMarkdownNodes.d.ts +0 -1
- package/dist/generator/generatorMarkdownNodes.js +0 -1
- package/dist/generator/index.d.ts +0 -1
- package/dist/generator/index.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/navigation/index.d.ts +3 -2
- package/dist/navigation/index.js +2 -2
- package/dist/navigation/navigation.adjacent.d.ts +32 -0
- package/dist/navigation/navigation.adjacent.js +60 -0
- package/dist/navigation/navigation.core.d.ts +0 -13
- package/dist/navigation/navigation.core.js +0 -51
- package/dist/registry/index.d.ts +0 -1
- package/dist/registry/index.js +0 -1
- package/dist/registry/registry.core.d.ts +0 -1
- package/dist/registry/registry.core.js +0 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
- package/dist/utils/utils.freeze.d.ts +0 -1
- package/dist/utils/utils.freeze.js +0 -1
- package/dist/utils/utils.helper.d.ts +0 -1
- package/dist/utils/utils.helper.js +0 -1
- package/dist/utils/utils.href.d.ts +0 -1
- package/dist/utils/utils.href.js +0 -1
- package/dist/utils/utils.markdownText.d.ts +0 -1
- package/dist/utils/utils.markdownText.js +0 -1
- package/dist/validator/index.d.ts +0 -1
- package/dist/validator/index.js +0 -1
- package/dist/validator/validator.types.d.ts +0 -1
- package/dist/validator/validator.types.js +0 -1
- package/dist/validator/validatorAll/index.d.ts +0 -1
- package/dist/validator/validatorAll/index.js +0 -1
- package/dist/validator/validatorAll/validatorAll.core.d.ts +11 -1
- package/dist/validator/validatorAll/validatorAll.core.js +6 -2
- package/dist/validator/validatorAll/validatorNavigation.core.d.ts +0 -1
- package/dist/validator/validatorAll/validatorNavigation.core.js +0 -1
- package/dist/validator/validatorDocument.core.d.ts +0 -1
- package/dist/validator/validatorDocument.core.js +0 -1
- package/dist/validator/validatorDuplicates.core.d.ts +0 -1
- package/dist/validator/validatorDuplicates.core.js +0 -1
- package/dist/validator/validatorLinks.core.d.ts +16 -6
- package/dist/validator/validatorLinks.core.js +18 -37
- package/dist/validator/validatorLinks.extract.d.ts +30 -0
- package/dist/validator/validatorLinks.extract.js +118 -0
- package/package.json +2 -2
package/dist/navigation/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Navigation tree utilities — breadcrumbs, flattening, sibling resolution.
|
|
5
5
|
*/
|
|
6
|
-
export { getBreadcrumbs, flattenNavigation, findNavigationItem,
|
|
7
|
-
|
|
6
|
+
export { getBreadcrumbs, flattenNavigation, findNavigationItem, } from "./navigation.core.js";
|
|
7
|
+
export { getSiblings, getAdjacent } from "./navigation.adjacent.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling and previous/next resolution over a navigation tree.
|
|
3
|
+
*
|
|
4
|
+
* @module navigation/navigation.adjacent
|
|
5
|
+
*/
|
|
6
|
+
import type { DocumentationNavigationItem } from "../docsTypes/index.js";
|
|
7
|
+
type Nodes = readonly DocumentationNavigationItem[];
|
|
8
|
+
/**
|
|
9
|
+
* Gets sibling document IDs for a given document (the other documents
|
|
10
|
+
* at the same navigation level, excluding the document itself).
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSiblings(documentId: string, items: Nodes): readonly string[];
|
|
13
|
+
/** Options for `getAdjacent`. */
|
|
14
|
+
export interface GetAdjacentOptions {
|
|
15
|
+
/**
|
|
16
|
+
* `"level"` (default) looks only at the documents that share the
|
|
17
|
+
* target's navigation level, so the first item of a section has no
|
|
18
|
+
* `previous`. `"tree"` walks the whole tree in reading order (the
|
|
19
|
+
* order of `flattenNavigation`), crossing section boundaries.
|
|
20
|
+
*/
|
|
21
|
+
readonly scope?: "level" | "tree";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Gets the previous and next document IDs around `documentId`: within its
|
|
25
|
+
* navigation level by default, or across the whole tree in reading order
|
|
26
|
+
* with `{ scope: "tree" }`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getAdjacent(documentId: string, items: Nodes, options?: GetAdjacentOptions): {
|
|
29
|
+
readonly previous?: string;
|
|
30
|
+
readonly next?: string;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling and previous/next resolution over a navigation tree.
|
|
3
|
+
*
|
|
4
|
+
* @module navigation/navigation.adjacent
|
|
5
|
+
*/
|
|
6
|
+
import { MAX_NAVIGATION_DEPTH, flattenNavigation } from "./navigation.core.js";
|
|
7
|
+
/**
|
|
8
|
+
* Returns the list of document IDs at the same level as `documentId`,
|
|
9
|
+
* including the document itself, in navigation order.
|
|
10
|
+
*/
|
|
11
|
+
function getLevel(documentId, items) {
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
function walk(nodes, depth) {
|
|
14
|
+
if (depth > MAX_NAVIGATION_DEPTH)
|
|
15
|
+
return undefined;
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
if (seen.has(node))
|
|
18
|
+
continue;
|
|
19
|
+
seen.add(node);
|
|
20
|
+
if (node.documentId === documentId) {
|
|
21
|
+
return nodes
|
|
22
|
+
.map((n) => n.documentId)
|
|
23
|
+
.filter((id) => typeof id === "string");
|
|
24
|
+
}
|
|
25
|
+
if (node.children) {
|
|
26
|
+
const result = walk(node.children, depth + 1);
|
|
27
|
+
if (result)
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return walk(items, 0) ?? [];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Gets sibling document IDs for a given document (the other documents
|
|
37
|
+
* at the same navigation level, excluding the document itself).
|
|
38
|
+
*/
|
|
39
|
+
export function getSiblings(documentId, items) {
|
|
40
|
+
return Object.freeze(getLevel(documentId, items).filter((id) => id !== documentId));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Gets the previous and next document IDs around `documentId`: within its
|
|
44
|
+
* navigation level by default, or across the whole tree in reading order
|
|
45
|
+
* with `{ scope: "tree" }`.
|
|
46
|
+
*/
|
|
47
|
+
export function getAdjacent(documentId, items, options = {}) {
|
|
48
|
+
const level = options.scope === "tree"
|
|
49
|
+
? flattenNavigation(items)
|
|
50
|
+
: getLevel(documentId, items);
|
|
51
|
+
const index = level.indexOf(documentId);
|
|
52
|
+
if (index === -1)
|
|
53
|
+
return Object.freeze({});
|
|
54
|
+
const previous = level[index - 1];
|
|
55
|
+
const next = level[index + 1];
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
...(previous !== undefined ? { previous } : {}),
|
|
58
|
+
...(next !== undefined ? { next } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -28,17 +28,4 @@ export declare function flattenNavigation(items: Nodes): readonly string[];
|
|
|
28
28
|
* Finds a navigation item by document ID.
|
|
29
29
|
*/
|
|
30
30
|
export declare function findNavigationItem(documentId: string, items: Nodes): DocumentationNavigationItem | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Gets sibling document IDs for a given document (the other documents
|
|
33
|
-
* at the same navigation level, excluding the document itself).
|
|
34
|
-
*/
|
|
35
|
-
export declare function getSiblings(documentId: string, items: Nodes): readonly string[];
|
|
36
|
-
/**
|
|
37
|
-
* Gets the previous and next document IDs at the same navigation level.
|
|
38
|
-
*/
|
|
39
|
-
export declare function getAdjacent(documentId: string, items: Nodes): {
|
|
40
|
-
readonly previous?: string;
|
|
41
|
-
readonly next?: string;
|
|
42
|
-
};
|
|
43
31
|
export {};
|
|
44
|
-
//# sourceMappingURL=navigation.core.d.ts.map
|
|
@@ -100,54 +100,3 @@ export function findNavigationItem(documentId, items) {
|
|
|
100
100
|
}
|
|
101
101
|
return walk(items, 0);
|
|
102
102
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Returns the list of document IDs at the same level as `documentId`,
|
|
105
|
-
* including the document itself, in navigation order.
|
|
106
|
-
*/
|
|
107
|
-
function getLevel(documentId, items) {
|
|
108
|
-
const seen = new WeakSet();
|
|
109
|
-
function walk(nodes, depth) {
|
|
110
|
-
if (depth > MAX_NAVIGATION_DEPTH)
|
|
111
|
-
return undefined;
|
|
112
|
-
for (const node of nodes) {
|
|
113
|
-
if (seen.has(node))
|
|
114
|
-
continue;
|
|
115
|
-
seen.add(node);
|
|
116
|
-
if (node.documentId === documentId) {
|
|
117
|
-
return nodes
|
|
118
|
-
.map((n) => n.documentId)
|
|
119
|
-
.filter((id) => typeof id === "string");
|
|
120
|
-
}
|
|
121
|
-
if (node.children) {
|
|
122
|
-
const result = walk(node.children, depth + 1);
|
|
123
|
-
if (result)
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
return walk(items, 0) ?? [];
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Gets sibling document IDs for a given document (the other documents
|
|
133
|
-
* at the same navigation level, excluding the document itself).
|
|
134
|
-
*/
|
|
135
|
-
export function getSiblings(documentId, items) {
|
|
136
|
-
return Object.freeze(getLevel(documentId, items).filter((id) => id !== documentId));
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Gets the previous and next document IDs at the same navigation level.
|
|
140
|
-
*/
|
|
141
|
-
export function getAdjacent(documentId, items) {
|
|
142
|
-
const level = getLevel(documentId, items);
|
|
143
|
-
const index = level.indexOf(documentId);
|
|
144
|
-
if (index === -1)
|
|
145
|
-
return Object.freeze({});
|
|
146
|
-
const previous = level[index - 1];
|
|
147
|
-
const next = level[index + 1];
|
|
148
|
-
return Object.freeze({
|
|
149
|
-
...(previous !== undefined ? { previous } : {}),
|
|
150
|
-
...(next !== undefined ? { next } : {}),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
//# sourceMappingURL=navigation.core.js.map
|
package/dist/registry/index.d.ts
CHANGED
package/dist/registry/index.js
CHANGED
package/dist/utils/index.d.ts
CHANGED
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { isValidDocumentId, normalizeDocumentId, documentIdFromPath, resolveDocumentLink, stripLinkDecorations, stripFencedCodeBlocks, extractTitleFromMarkdown, extractHeadings, } from "./utils.helper.js";
|
|
7
7
|
export { stripMarkdown } from "./utils.markdownText.js";
|
|
8
8
|
export { deepFreeze, deepFreezeClone } from "./utils.freeze.js";
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/utils/index.js
CHANGED
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { isValidDocumentId, normalizeDocumentId, documentIdFromPath, resolveDocumentLink, stripLinkDecorations, stripFencedCodeBlocks, extractTitleFromMarkdown, extractHeadings, } from "./utils.helper.js";
|
|
7
7
|
export { stripMarkdown } from "./utils.markdownText.js";
|
|
8
8
|
export { deepFreeze, deepFreezeClone } from "./utils.freeze.js";
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
package/dist/utils/utils.href.js
CHANGED
|
@@ -11,4 +11,3 @@ export { toValidationResult } from "./validator.types.js";
|
|
|
11
11
|
export type { ValidationIssue, ValidationResult } from "./validator.types.js";
|
|
12
12
|
export type { ValidateLinksOptions } from "./validatorLinks.core.js";
|
|
13
13
|
export type { ValidateAllOptions } from "./validatorAll/validatorAll.core.js";
|
|
14
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/validator/index.js
CHANGED
|
@@ -8,4 +8,3 @@ export { validateNoDuplicateIds } from "./validatorDuplicates.core.js";
|
|
|
8
8
|
export { validateLinks, DEFAULT_MAX_LINK_SCAN_LENGTH, } from "./validatorLinks.core.js";
|
|
9
9
|
export { validateAll, validateNavigation } from "./validatorAll/index.js";
|
|
10
10
|
export { toValidationResult } from "./validator.types.js";
|
|
11
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -11,9 +11,19 @@ export interface ValidateAllOptions extends ValidateLinksOptions {
|
|
|
11
11
|
* appear in it as `NAVIGATION_ORPHAN_DOCUMENT` warnings. Default true.
|
|
12
12
|
*/
|
|
13
13
|
readonly reportOrphans?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Severity of a `NAVIGATION_ORPHAN_DOCUMENT` issue. Defaults to
|
|
16
|
+
* `"warning"`; pass `"error"` to fail validation when a document is
|
|
17
|
+
* unreachable from the navigation.
|
|
18
|
+
*/
|
|
19
|
+
readonly orphanSeverity?: "error" | "warning";
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Validates all documents and optionally a navigation tree.
|
|
23
|
+
*
|
|
24
|
+
* `valid` is false only when an issue has severity `"error"`. Broken
|
|
25
|
+
* links and orphan documents are warnings by default, so a set with dead
|
|
26
|
+
* links still validates; set `brokenLinkSeverity` / `orphanSeverity` to
|
|
27
|
+
* `"error"` to fail on them.
|
|
17
28
|
*/
|
|
18
29
|
export declare function validateAll(documents: readonly DocumentationDocument[], navigation?: readonly DocumentationNavigationItem[], options?: ValidateAllOptions): ValidationResult;
|
|
19
|
-
//# sourceMappingURL=validatorAll.core.d.ts.map
|
|
@@ -9,6 +9,11 @@ import { validateLinks, } from "../validatorLinks.core.js";
|
|
|
9
9
|
import { validateNavigation } from "./validatorNavigation.core.js";
|
|
10
10
|
/**
|
|
11
11
|
* Validates all documents and optionally a navigation tree.
|
|
12
|
+
*
|
|
13
|
+
* `valid` is false only when an issue has severity `"error"`. Broken
|
|
14
|
+
* links and orphan documents are warnings by default, so a set with dead
|
|
15
|
+
* links still validates; set `brokenLinkSeverity` / `orphanSeverity` to
|
|
16
|
+
* `"error"` to fail on them.
|
|
12
17
|
*/
|
|
13
18
|
export function validateAll(documents, navigation, options = {}) {
|
|
14
19
|
const allIssues = [];
|
|
@@ -29,7 +34,7 @@ export function validateAll(documents, navigation, options = {}) {
|
|
|
29
34
|
for (const id of registeredIds) {
|
|
30
35
|
if (!inNavigation.has(id)) {
|
|
31
36
|
allIssues.push({
|
|
32
|
-
severity: "warning",
|
|
37
|
+
severity: options.orphanSeverity ?? "warning",
|
|
33
38
|
code: "NAVIGATION_ORPHAN_DOCUMENT",
|
|
34
39
|
message: `Document "${id}" is not reachable from the navigation.`,
|
|
35
40
|
documentId: id,
|
|
@@ -40,4 +45,3 @@ export function validateAll(documents, navigation, options = {}) {
|
|
|
40
45
|
}
|
|
41
46
|
return toValidationResult(allIssues);
|
|
42
47
|
}
|
|
43
|
-
//# sourceMappingURL=validatorAll.core.js.map
|
|
@@ -14,4 +14,3 @@ import { type ValidationResult } from "../validator.types.js";
|
|
|
14
14
|
* for items with neither `documentId` nor `children`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function validateNavigation(items: readonly DocumentationNavigationItem[], registeredIds: ReadonlySet<string>): ValidationResult;
|
|
17
|
-
//# sourceMappingURL=validatorNavigation.core.d.ts.map
|
|
@@ -8,4 +8,3 @@ import { type ValidationResult } from "./validator.types.js";
|
|
|
8
8
|
* shape (including structured nodes), and the metadata enums.
|
|
9
9
|
*/
|
|
10
10
|
export declare function validateDocument(document: DocumentationDocument): ValidationResult;
|
|
11
|
-
//# sourceMappingURL=validatorDocument.core.d.ts.map
|
|
@@ -7,4 +7,3 @@ import { type ValidationResult } from "./validator.types.js";
|
|
|
7
7
|
* Validates no duplicate IDs exist in a collection of documents.
|
|
8
8
|
*/
|
|
9
9
|
export declare function validateNoDuplicateIds(documents: readonly DocumentationDocument[]): ValidationResult;
|
|
10
|
-
//# sourceMappingURL=validatorDuplicates.core.d.ts.map
|
|
@@ -13,18 +13,28 @@ export interface ValidateLinksOptions {
|
|
|
13
13
|
* Defaults to {@link DEFAULT_MAX_LINK_SCAN_LENGTH}.
|
|
14
14
|
*/
|
|
15
15
|
readonly maxContentLength?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Severity of a `BROKEN_LINK` issue (a relative link to an unregistered
|
|
18
|
+
* document). Defaults to `"warning"`, which never makes a result invalid;
|
|
19
|
+
* pass `"error"` to fail validation on dead links.
|
|
20
|
+
*/
|
|
21
|
+
readonly brokenLinkSeverity?: "error" | "warning";
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Validates internal links in a document's markdown content and in
|
|
19
25
|
* structured `link` nodes.
|
|
20
26
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* Every markdown link form is scanned: inline `[text](target)` (with
|
|
28
|
+
* balanced parentheses, `<…>` targets and titles), reference definitions
|
|
29
|
+
* `[label]: target`, raw HTML `href="…"` attributes and `<scheme:…>`
|
|
30
|
+
* autolinks. A target whose scheme is not on the allow-list (http, https,
|
|
31
|
+
* mailto, tel, ftp, ftps) — `javascript:`, `data:`, `vbscript:` … — is
|
|
32
|
+
* reported as an `UNSAFE_LINK` error.
|
|
33
|
+
* Skipped (never reported): images, anchors (`#…`), other targets with an
|
|
34
|
+
* allowed URL scheme or `//` prefix, links inside fenced or inline code.
|
|
25
35
|
* Relative targets (`./x`, `../x`, `/x`) are resolved against the
|
|
26
36
|
* document ID with `resolveDocumentLink`; bare targets are looked up
|
|
27
|
-
* both as-is and resolved.
|
|
37
|
+
* both as-is and resolved. An unregistered target is a `BROKEN_LINK`
|
|
38
|
+
* whose severity is `options.brokenLinkSeverity` (default `"warning"`).
|
|
28
39
|
*/
|
|
29
40
|
export declare function validateLinks(document: DocumentationDocument, registeredIds: ReadonlySet<string>, options?: ValidateLinksOptions): ValidationResult;
|
|
30
|
-
//# sourceMappingURL=validatorLinks.core.d.ts.map
|
|
@@ -3,42 +3,37 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { resolveDocumentLink, stripFencedCodeBlocks, stripLinkDecorations, } from "../utils/utils.helper.js";
|
|
5
5
|
import { isSafeLinkHref } from "../utils/utils.href.js";
|
|
6
|
+
import { extractLinkTargets, stripInlineCode, } from "./validatorLinks.extract.js";
|
|
6
7
|
import { toValidationResult, } from "./validator.types.js";
|
|
7
8
|
/** Default maximum markdown length that is scanned for links. */
|
|
8
9
|
export const DEFAULT_MAX_LINK_SCAN_LENGTH = 100_000;
|
|
9
|
-
/**
|
|
10
|
-
* Matches `[text](target)` and `` links. Group 1 is the
|
|
11
|
-
* optional image bang, group 2 the target (optionally `<…>` wrapped
|
|
12
|
-
* and followed by a `"title"`).
|
|
13
|
-
*
|
|
14
|
-
* Linear by construction: link text cannot contain `[`/`]` and a target
|
|
15
|
-
* cannot contain `(`/`)`, so each start scans only to the next opener; the
|
|
16
|
-
* target is non-empty, so leading whitespace is never split between two
|
|
17
|
-
* quantifiers; and whitespace is `[ \t]` (the old `\s` ran across lines).
|
|
18
|
-
* The old pattern took ~30 s on a 99 KB run of `[`.
|
|
19
|
-
*/
|
|
20
|
-
const LINK_PATTERN = /(!?)\[[^[\]\n]*\]\((?:[ \t]*(<[^<>\n]*>|[^()\s<>]+)(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'))?)?[ \t]*\)/g;
|
|
21
10
|
/** `scheme:` (mailto:, ftp:, http:) or protocol-relative `//`. */
|
|
22
11
|
const EXTERNAL_PATTERN = /^([a-zA-Z][a-zA-Z0-9+.-]*:|\/\/)/;
|
|
23
12
|
/**
|
|
24
13
|
* Validates internal links in a document's markdown content and in
|
|
25
14
|
* structured `link` nodes.
|
|
26
15
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
16
|
+
* Every markdown link form is scanned: inline `[text](target)` (with
|
|
17
|
+
* balanced parentheses, `<…>` targets and titles), reference definitions
|
|
18
|
+
* `[label]: target`, raw HTML `href="…"` attributes and `<scheme:…>`
|
|
19
|
+
* autolinks. A target whose scheme is not on the allow-list (http, https,
|
|
20
|
+
* mailto, tel, ftp, ftps) — `javascript:`, `data:`, `vbscript:` … — is
|
|
21
|
+
* reported as an `UNSAFE_LINK` error.
|
|
22
|
+
* Skipped (never reported): images, anchors (`#…`), other targets with an
|
|
23
|
+
* allowed URL scheme or `//` prefix, links inside fenced or inline code.
|
|
31
24
|
* Relative targets (`./x`, `../x`, `/x`) are resolved against the
|
|
32
25
|
* document ID with `resolveDocumentLink`; bare targets are looked up
|
|
33
|
-
* both as-is and resolved.
|
|
26
|
+
* both as-is and resolved. An unregistered target is a `BROKEN_LINK`
|
|
27
|
+
* whose severity is `options.brokenLinkSeverity` (default `"warning"`).
|
|
34
28
|
*/
|
|
35
29
|
export function validateLinks(document, registeredIds, options = {}) {
|
|
36
30
|
const issues = [];
|
|
37
31
|
const content = document.content;
|
|
32
|
+
const brokenSeverity = options.brokenLinkSeverity ?? "warning";
|
|
38
33
|
if (content.type === "structured") {
|
|
39
34
|
for (const node of content.nodes) {
|
|
40
35
|
if (node.type === "link") {
|
|
41
|
-
checkTarget(node.href, document, registeredIds, issues);
|
|
36
|
+
checkTarget(node.href, document, registeredIds, issues, brokenSeverity);
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
return toValidationResult(issues);
|
|
@@ -57,19 +52,14 @@ export function validateLinks(document, registeredIds, options = {}) {
|
|
|
57
52
|
return toValidationResult(issues);
|
|
58
53
|
}
|
|
59
54
|
const source = stripInlineCode(stripFencedCodeBlocks(content.value));
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
let target = match[2] ?? "";
|
|
63
|
-
if (isImage)
|
|
55
|
+
for (const link of extractLinkTargets(source)) {
|
|
56
|
+
if (link.isImage)
|
|
64
57
|
continue;
|
|
65
|
-
|
|
66
|
-
target = target.slice(1, -1);
|
|
67
|
-
}
|
|
68
|
-
checkTarget(target, document, registeredIds, issues);
|
|
58
|
+
checkTarget(link.target, document, registeredIds, issues, brokenSeverity);
|
|
69
59
|
}
|
|
70
60
|
return toValidationResult(issues);
|
|
71
61
|
}
|
|
72
|
-
function checkTarget(rawTarget, document, registeredIds, issues) {
|
|
62
|
+
function checkTarget(rawTarget, document, registeredIds, issues, brokenSeverity) {
|
|
73
63
|
const target = rawTarget.trim();
|
|
74
64
|
if (target === "" || target.startsWith("#"))
|
|
75
65
|
return;
|
|
@@ -97,18 +87,9 @@ function checkTarget(rawTarget, document, registeredIds, issues) {
|
|
|
97
87
|
return;
|
|
98
88
|
}
|
|
99
89
|
issues.push({
|
|
100
|
-
severity:
|
|
90
|
+
severity: brokenSeverity,
|
|
101
91
|
code: "BROKEN_LINK",
|
|
102
92
|
message: `Document "${document.id}" links to "${rawTarget}" which is not registered.`,
|
|
103
93
|
documentId: document.id,
|
|
104
94
|
});
|
|
105
95
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Blanks out inline code spans so links inside them are ignored. The
|
|
108
|
-
* lookarounds anchor each attempt at a whole backtick run, so a long run is
|
|
109
|
-
* not re-scanned from every position inside it.
|
|
110
|
-
*/
|
|
111
|
-
function stripInlineCode(markdown) {
|
|
112
|
-
return markdown.replace(/(?<!`)(`+)(?![`\n])[\s\S]*?(?<!`)\1(?!`)/g, (m) => " ".repeat(m.length));
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=validatorLinks.core.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts link targets from markdown in every form a renderer turns into
|
|
3
|
+
* a live link: inline `[text](target)`, reference definitions
|
|
4
|
+
* `[label]: target`, raw HTML `href` attributes and `<scheme:…>` autolinks.
|
|
5
|
+
*
|
|
6
|
+
* Every scanner is linear in the input length, so hostile documents
|
|
7
|
+
* (a 99 KB run of `[`, `(` or `<`) are scanned in milliseconds.
|
|
8
|
+
*
|
|
9
|
+
* @module validator/validatorLinks.extract
|
|
10
|
+
*/
|
|
11
|
+
/** A link target found in markdown. */
|
|
12
|
+
export interface ExtractedLink {
|
|
13
|
+
/** The raw target, without `<…>` wrapping. */
|
|
14
|
+
readonly target: string;
|
|
15
|
+
/** True for `` images, which are never validated. */
|
|
16
|
+
readonly isImage: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns every distinct link target in `source`, in order of first
|
|
20
|
+
* appearance. `source` must already have fenced code blocks and inline
|
|
21
|
+
* code spans removed. A target found by two scanners (`[x](<t>)` is both
|
|
22
|
+
* an inline link and an autolink) is returned once.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractLinkTargets(source: string): readonly ExtractedLink[];
|
|
25
|
+
/**
|
|
26
|
+
* Blanks out inline code spans so links inside them are ignored. The
|
|
27
|
+
* lookarounds anchor each attempt at a whole backtick run, so a long run is
|
|
28
|
+
* not re-scanned from every position inside it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stripInlineCode(markdown: string): string;
|