@contentbit/core 0.2.0 → 0.3.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/dist/frontmatter.js +68 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/links.d.ts +96 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +435 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/frontmatter.js
CHANGED
|
@@ -68,12 +68,64 @@ function parseValue(value, indented) {
|
|
|
68
68
|
if (indented.length === 0)
|
|
69
69
|
return null;
|
|
70
70
|
const items = dedent(indented);
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const list = parseDashList(items);
|
|
72
|
+
if (list)
|
|
73
|
+
return list;
|
|
74
|
+
const mapping = parseNestedMapping(items);
|
|
75
|
+
if (mapping)
|
|
76
|
+
return mapping;
|
|
73
77
|
return items.join('\n');
|
|
74
78
|
}
|
|
75
79
|
return parseScalar(value);
|
|
76
80
|
}
|
|
81
|
+
// A one-level mapping: every dedented line is `key: scalar` (or `key:` with an
|
|
82
|
+
// inline array value). Returns null if any line isn't a flat mapping entry —
|
|
83
|
+
// e.g. a deeper-indented line or a `key:` with no inline value, which would
|
|
84
|
+
// need its own nested block — so the caller keeps the raw-text fallback.
|
|
85
|
+
function parseNestedMapping(items) {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const line of items) {
|
|
88
|
+
if (/^[ \t]/.test(line))
|
|
89
|
+
return null;
|
|
90
|
+
const m = line.match(KEY_RE);
|
|
91
|
+
if (!m)
|
|
92
|
+
return null;
|
|
93
|
+
const [, key, rawValue] = m;
|
|
94
|
+
const v = rawValue.trim();
|
|
95
|
+
if (v === '')
|
|
96
|
+
return null;
|
|
97
|
+
out[key] = parseScalar(v);
|
|
98
|
+
}
|
|
99
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
100
|
+
}
|
|
101
|
+
function parseDashList(items) {
|
|
102
|
+
const groups = [];
|
|
103
|
+
let current = null;
|
|
104
|
+
for (const line of items) {
|
|
105
|
+
if (line.startsWith('- ')) {
|
|
106
|
+
if (current)
|
|
107
|
+
groups.push(current);
|
|
108
|
+
current = [line.slice(2)];
|
|
109
|
+
}
|
|
110
|
+
else if (current && /^[ \t]/.test(line)) {
|
|
111
|
+
current.push(line);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (current)
|
|
118
|
+
groups.push(current);
|
|
119
|
+
return groups.map((group) => parseDashItem(group));
|
|
120
|
+
}
|
|
121
|
+
function parseDashItem(lines) {
|
|
122
|
+
const first = lines[0].trim();
|
|
123
|
+
if (lines.length === 1)
|
|
124
|
+
return parseNestedMapping([first]) ?? parseScalar(first);
|
|
125
|
+
const rest = dedent(lines.slice(1));
|
|
126
|
+
const mapping = parseNestedMapping([first, ...rest]);
|
|
127
|
+
return mapping ?? lines.join('\n');
|
|
128
|
+
}
|
|
77
129
|
function dedent(lines) {
|
|
78
130
|
const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
|
|
79
131
|
return lines.map((l) => l.slice(indent));
|
|
@@ -106,6 +158,20 @@ function parseScalar(value) {
|
|
|
106
158
|
return [];
|
|
107
159
|
return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
|
|
108
160
|
}
|
|
161
|
+
if (value.startsWith('{') && value.endsWith('}')) {
|
|
162
|
+
const inner = value.slice(1, -1).trim();
|
|
163
|
+
if (inner === '')
|
|
164
|
+
return {};
|
|
165
|
+
const out = {};
|
|
166
|
+
for (const item of splitInlineItems(inner)) {
|
|
167
|
+
const m = item.trim().match(KEY_RE);
|
|
168
|
+
if (!m)
|
|
169
|
+
return value;
|
|
170
|
+
const [, key, rawValue] = m;
|
|
171
|
+
out[key] = parseScalar(rawValue.trim());
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
109
175
|
return value;
|
|
110
176
|
}
|
|
111
177
|
/** Split inline array items on top-level commas, respecting quotes. */
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export { markdownBody, pipeRows, listItems, childBlocks, type MarkdownBodyData,
|
|
|
12
12
|
export { validateDocument, isValidatedBlock, type ValidateOptions, type ValidationResult, type ValidatedBlockNode, } from './validate.js';
|
|
13
13
|
export { generateAuthoringGuide, type AuthoringGuideOptions } from './authoring.js';
|
|
14
14
|
export { renderToMarkdown, type MarkdownBlockRenderer, type MarkdownRenderContext, type RenderToMarkdownOptions, } from './render-markdown.js';
|
|
15
|
+
export { parseLinkFrontmatter, buildLinkIndex, validateLinks, serializeLinkIndex, aliasReplacementsForPage, type LinkResolveMode, type LinkResolverOptions, type LinkTarget, type LinkFrontmatter, type ParseLinkResult, type LinkReference, type IndexedPage, type LinkAliasEntry, type LinkIndex, type LinkInput, type LinkDiagnostic, type SerializedLinkIndex, } from './links.js';
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzF,OAAO,EACL,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,GACZ,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzF,OAAO,EACL,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,GACZ,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { markdownBody, pipeRows, listItems, childBlocks, } from './content-model
|
|
|
10
10
|
export { validateDocument, isValidatedBlock, } from './validate.js';
|
|
11
11
|
export { generateAuthoringGuide } from './authoring.js';
|
|
12
12
|
export { renderToMarkdown, } from './render-markdown.js';
|
|
13
|
+
export { parseLinkFrontmatter, buildLinkIndex, validateLinks, serializeLinkIndex, aliasReplacementsForPage, } from './links.js';
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Diagnostic } from './diagnostics.js';
|
|
3
|
+
declare const LinkTarget: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
4
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
5
|
+
key: z.ZodOptional<z.ZodString>;
|
|
6
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$strip>]>;
|
|
8
|
+
declare const LinkFrontmatter: z.ZodObject<{
|
|
9
|
+
slug: z.ZodString;
|
|
10
|
+
key: z.ZodOptional<z.ZodString>;
|
|
11
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
12
|
+
title: z.ZodOptional<z.ZodString>;
|
|
13
|
+
linksTo: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
14
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
15
|
+
key: z.ZodOptional<z.ZodString>;
|
|
16
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>]>>>;
|
|
18
|
+
aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
19
|
+
keywords: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
primary: z.ZodOptional<z.ZodString>;
|
|
21
|
+
secondary: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type LinkTarget = z.infer<typeof LinkTarget>;
|
|
25
|
+
export type LinkFrontmatter = z.infer<typeof LinkFrontmatter>;
|
|
26
|
+
export type LinkResolveMode = 'global-slug' | 'same-locale-slug' | 'same-locale-key' | 'prefer-same-locale-key-fallback-slug';
|
|
27
|
+
export interface LinkResolverOptions {
|
|
28
|
+
resolve?: LinkResolveMode;
|
|
29
|
+
localeField?: string;
|
|
30
|
+
slugField?: string;
|
|
31
|
+
keyField?: string;
|
|
32
|
+
defaultLocale?: string;
|
|
33
|
+
}
|
|
34
|
+
export type ParseLinkResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
value: LinkFrontmatter | null;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
errors: string[];
|
|
40
|
+
};
|
|
41
|
+
export interface LinkReference {
|
|
42
|
+
target?: string;
|
|
43
|
+
locale?: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
key?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface IndexedPage {
|
|
48
|
+
slug: string;
|
|
49
|
+
key?: string;
|
|
50
|
+
locale?: string;
|
|
51
|
+
path: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
keywords?: {
|
|
54
|
+
primary?: string;
|
|
55
|
+
secondary?: string[];
|
|
56
|
+
};
|
|
57
|
+
linksTo: string[];
|
|
58
|
+
linkedFrom: string[];
|
|
59
|
+
aliases: string[];
|
|
60
|
+
linkRefs: LinkReference[];
|
|
61
|
+
linkedFromRefs: LinkReference[];
|
|
62
|
+
}
|
|
63
|
+
export interface LinkAliasEntry {
|
|
64
|
+
alias: string;
|
|
65
|
+
locale?: string;
|
|
66
|
+
replacement: string;
|
|
67
|
+
page: IndexedPage;
|
|
68
|
+
}
|
|
69
|
+
export interface LinkIndex {
|
|
70
|
+
pages: Map<string, IndexedPage>;
|
|
71
|
+
aliases: Map<string, string>;
|
|
72
|
+
aliasEntries: LinkAliasEntry[];
|
|
73
|
+
options: Required<Pick<LinkResolverOptions, 'resolve'>> & Omit<LinkResolverOptions, 'resolve'>;
|
|
74
|
+
}
|
|
75
|
+
export interface LinkInput {
|
|
76
|
+
path: string;
|
|
77
|
+
data: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
export interface SerializedLinkIndex {
|
|
80
|
+
pages: Array<Omit<IndexedPage, 'linkRefs' | 'linkedFromRefs' | 'linksTo' | 'linkedFrom'> & {
|
|
81
|
+
linksTo: string[] | LinkReference[];
|
|
82
|
+
linkedFrom: string[] | LinkReference[];
|
|
83
|
+
}>;
|
|
84
|
+
aliases: Record<string, string | LinkReference>;
|
|
85
|
+
}
|
|
86
|
+
export interface LinkDiagnostic {
|
|
87
|
+
file: string;
|
|
88
|
+
diagnostic: Diagnostic;
|
|
89
|
+
}
|
|
90
|
+
export declare function parseLinkFrontmatter(data: Record<string, unknown>, options?: LinkResolverOptions): ParseLinkResult;
|
|
91
|
+
export declare function buildLinkIndex(inputs: LinkInput[], options?: LinkResolverOptions): LinkIndex;
|
|
92
|
+
export declare function serializeLinkIndex(index: LinkIndex): SerializedLinkIndex;
|
|
93
|
+
export declare function aliasReplacementsForPage(index: LinkIndex, data: Record<string, unknown>): Map<string, string>;
|
|
94
|
+
export declare function validateLinks(inputs: LinkInput[], options?: LinkResolverOptions): LinkDiagnostic[];
|
|
95
|
+
export {};
|
|
96
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const Keywords = z.object({
|
|
3
|
+
primary: z.string().optional(),
|
|
4
|
+
secondary: z.array(z.string()).optional(),
|
|
5
|
+
});
|
|
6
|
+
const LinkTarget = z.union([
|
|
7
|
+
z.string(),
|
|
8
|
+
z
|
|
9
|
+
.object({
|
|
10
|
+
slug: z.string().min(1).optional(),
|
|
11
|
+
key: z.string().min(1).optional(),
|
|
12
|
+
locale: z.string().min(1).optional(),
|
|
13
|
+
})
|
|
14
|
+
.refine((target) => target.slug || target.key, {
|
|
15
|
+
message: 'object linksTo targets must include slug or key',
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
const LinkFrontmatter = z.object({
|
|
19
|
+
slug: z.string().min(1),
|
|
20
|
+
key: z.string().min(1).optional(),
|
|
21
|
+
locale: z.string().min(1).optional(),
|
|
22
|
+
title: z.string().optional(),
|
|
23
|
+
linksTo: z.array(LinkTarget).optional(),
|
|
24
|
+
aliases: z.array(z.string()).optional(),
|
|
25
|
+
keywords: Keywords.optional(),
|
|
26
|
+
});
|
|
27
|
+
const DEFAULT_OPTIONS = {
|
|
28
|
+
resolve: 'global-slug',
|
|
29
|
+
};
|
|
30
|
+
const FM_POSITION = {
|
|
31
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
32
|
+
end: { line: 1, column: 1, offset: 0 },
|
|
33
|
+
};
|
|
34
|
+
// Returns { value: null } when there is no `slug` (a non-participating page),
|
|
35
|
+
// parsed data when the link shape is valid, or shape errors otherwise. Never
|
|
36
|
+
// throws — callers turn errors into diagnostics.
|
|
37
|
+
export function parseLinkFrontmatter(data, options = {}) {
|
|
38
|
+
const normalized = normalizeFrontmatter(data, options);
|
|
39
|
+
if (!('slug' in normalized))
|
|
40
|
+
return { ok: true, value: null };
|
|
41
|
+
const parsed = LinkFrontmatter.safeParse(normalized);
|
|
42
|
+
if (parsed.success)
|
|
43
|
+
return { ok: true, value: parsed.data };
|
|
44
|
+
return { ok: false, errors: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`) };
|
|
45
|
+
}
|
|
46
|
+
// Pure: builds the resolved link graph from per-file frontmatter data. Pass 1
|
|
47
|
+
// collects pages and registers aliases; pass 2 resolves each linksTo through
|
|
48
|
+
// the configured identity maps and inverts edges to derive linkedFrom.
|
|
49
|
+
export function buildLinkIndex(inputs, options = {}) {
|
|
50
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
51
|
+
const pages = new Map();
|
|
52
|
+
const aliases = new Map();
|
|
53
|
+
const aliasEntries = [];
|
|
54
|
+
for (const { path, data } of inputs) {
|
|
55
|
+
const parsed = parseLinkFrontmatter(data, resolvedOptions);
|
|
56
|
+
if (!parsed.ok || parsed.value === null)
|
|
57
|
+
continue;
|
|
58
|
+
const fm = parsed.value;
|
|
59
|
+
const page = {
|
|
60
|
+
slug: fm.slug,
|
|
61
|
+
key: fm.key,
|
|
62
|
+
locale: effectiveLocale(fm, resolvedOptions),
|
|
63
|
+
path,
|
|
64
|
+
title: fm.title,
|
|
65
|
+
keywords: fm.keywords,
|
|
66
|
+
linksTo: [],
|
|
67
|
+
linkedFrom: [],
|
|
68
|
+
aliases: fm.aliases ?? [],
|
|
69
|
+
linkRefs: [],
|
|
70
|
+
linkedFromRefs: [],
|
|
71
|
+
};
|
|
72
|
+
pages.set(pageMapKey(page, resolvedOptions), page);
|
|
73
|
+
}
|
|
74
|
+
for (const page of pages.values()) {
|
|
75
|
+
for (const alias of page.aliases) {
|
|
76
|
+
const replacement = replacementFor(page, resolvedOptions);
|
|
77
|
+
const key = aliasMapKey(alias, page.locale, resolvedOptions);
|
|
78
|
+
aliases.set(key, replacement);
|
|
79
|
+
aliasEntries.push({ alias, locale: page.locale, replacement, page });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const lookup = buildLookup(pages, resolvedOptions);
|
|
83
|
+
for (const page of pages.values()) {
|
|
84
|
+
const source = parseLinkFrontmatter(inputForPage(page, inputs, resolvedOptions), resolvedOptions);
|
|
85
|
+
if (!source.ok || source.value === null)
|
|
86
|
+
continue;
|
|
87
|
+
for (const rawTarget of source.value.linksTo ?? []) {
|
|
88
|
+
const resolved = resolveTarget(rawTarget, page, lookup, resolvedOptions);
|
|
89
|
+
if (!resolved.page) {
|
|
90
|
+
page.linksTo.push(resolved.target);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
page.linksTo.push(replacementFor(resolved.page, resolvedOptions));
|
|
94
|
+
page.linkRefs.push(referenceFor(resolved.page, resolved.target));
|
|
95
|
+
if (resolved.page === page)
|
|
96
|
+
continue;
|
|
97
|
+
const from = replacementFor(page, resolvedOptions);
|
|
98
|
+
if (!resolved.page.linkedFrom.includes(from))
|
|
99
|
+
resolved.page.linkedFrom.push(from);
|
|
100
|
+
if (!resolved.page.linkedFromRefs.some((r) => sameReference(r, page))) {
|
|
101
|
+
resolved.page.linkedFromRefs.push(referenceFor(page));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { pages, aliases, aliasEntries, options: resolvedOptions };
|
|
106
|
+
}
|
|
107
|
+
// Stable, sorted JSON form for .contentbit/link-index.json. Global-slug projects
|
|
108
|
+
// keep the original compact string arrays; locale/key projects receive richer
|
|
109
|
+
// references so agents can see which local target was resolved.
|
|
110
|
+
export function serializeLinkIndex(index) {
|
|
111
|
+
const scoped = [...index.pages.values()].some((page) => page.locale || page.key);
|
|
112
|
+
const pages = [...index.pages.values()]
|
|
113
|
+
.map((p) => {
|
|
114
|
+
const base = {
|
|
115
|
+
slug: p.slug,
|
|
116
|
+
...(p.key ? { key: p.key } : {}),
|
|
117
|
+
...(p.locale ? { locale: p.locale } : {}),
|
|
118
|
+
path: p.path,
|
|
119
|
+
...(p.title ? { title: p.title } : {}),
|
|
120
|
+
...(p.keywords ? { keywords: p.keywords } : {}),
|
|
121
|
+
linksTo: scoped ? p.linkRefs : [...p.linksTo],
|
|
122
|
+
linkedFrom: scoped ? sortedRefs(p.linkedFromRefs) : [...p.linkedFrom].sort(),
|
|
123
|
+
aliases: [...p.aliases],
|
|
124
|
+
};
|
|
125
|
+
return base;
|
|
126
|
+
})
|
|
127
|
+
.sort((a, b) => sortIdentity(a.locale, a.slug).localeCompare(sortIdentity(b.locale, b.slug)));
|
|
128
|
+
const aliases = {};
|
|
129
|
+
if (scoped) {
|
|
130
|
+
for (const entry of [...index.aliasEntries].sort((a, b) => sortIdentity(a.locale, a.alias).localeCompare(sortIdentity(b.locale, b.alias)))) {
|
|
131
|
+
aliases[aliasMapKey(entry.alias, entry.locale, index.options)] = referenceFor(entry.page);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
for (const key of [...index.aliases.keys()].sort())
|
|
136
|
+
aliases[key] = index.aliases.get(key);
|
|
137
|
+
}
|
|
138
|
+
return { pages, aliases };
|
|
139
|
+
}
|
|
140
|
+
export function aliasReplacementsForPage(index, data) {
|
|
141
|
+
const parsed = parseLinkFrontmatter(data, index.options);
|
|
142
|
+
const out = new Map();
|
|
143
|
+
if (!parsed.ok || parsed.value === null)
|
|
144
|
+
return out;
|
|
145
|
+
const locale = effectiveLocale(parsed.value, index.options);
|
|
146
|
+
for (const entry of index.aliasEntries) {
|
|
147
|
+
if (index.options.resolve === 'global-slug' || entry.locale === locale) {
|
|
148
|
+
out.set(entry.alias, entry.replacement);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
// Cross-file link validation. Emits shape errors, duplicate-slug and
|
|
154
|
+
// alias-conflict errors, dangling-link errors (with did-you-mean), and
|
|
155
|
+
// self-link / orphan warnings. Returns file-tagged diagnostics so the CLI can
|
|
156
|
+
// format each with its own filename.
|
|
157
|
+
export function validateLinks(inputs, options = {}) {
|
|
158
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
159
|
+
const out = [];
|
|
160
|
+
const validInputs = [];
|
|
161
|
+
const seenSlug = new Map(); // identity -> first file
|
|
162
|
+
const seenKey = new Map(); // identity -> first file
|
|
163
|
+
const seenAlias = new Map(); // identity -> file
|
|
164
|
+
// Shape + duplicate/alias-conflict pass (operates on raw inputs).
|
|
165
|
+
for (const { path, data } of inputs) {
|
|
166
|
+
const parsed = parseLinkFrontmatter(data, resolvedOptions);
|
|
167
|
+
if (!parsed.ok) {
|
|
168
|
+
for (const e of parsed.errors)
|
|
169
|
+
out.push(diag(path, 'CB_LINK_SHAPE', 'error', `invalid link frontmatter: ${e}`));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (parsed.value === null)
|
|
173
|
+
continue;
|
|
174
|
+
const fm = parsed.value;
|
|
175
|
+
const locale = effectiveLocale(fm, resolvedOptions);
|
|
176
|
+
validInputs.push({ path, fm });
|
|
177
|
+
const slugKey = scopedKey(fm.slug, locale, resolvedOptions);
|
|
178
|
+
const prior = seenSlug.get(slugKey);
|
|
179
|
+
if (prior)
|
|
180
|
+
out.push(diag(path, 'CB_SLUG_DUPLICATE', 'error', `slug "${fm.slug}" also used by ${prior}`));
|
|
181
|
+
else
|
|
182
|
+
seenSlug.set(slugKey, path);
|
|
183
|
+
if (usesKeyResolution(resolvedOptions) && !fm.key) {
|
|
184
|
+
out.push(diag(path, 'CB_KEY_MISSING', 'error', `page "${fm.slug}" is missing key`));
|
|
185
|
+
}
|
|
186
|
+
if (fm.key) {
|
|
187
|
+
const keyKey = scopedKey(fm.key, locale, resolvedOptions);
|
|
188
|
+
const priorKey = seenKey.get(keyKey);
|
|
189
|
+
if (priorKey)
|
|
190
|
+
out.push(diag(path, 'CB_KEY_DUPLICATE', 'error', `key "${fm.key}" also used by ${priorKey}`));
|
|
191
|
+
else
|
|
192
|
+
seenKey.set(keyKey, path);
|
|
193
|
+
}
|
|
194
|
+
for (const alias of fm.aliases ?? []) {
|
|
195
|
+
const aliasKey = scopedKey(alias, locale, resolvedOptions);
|
|
196
|
+
if (seenAlias.has(aliasKey))
|
|
197
|
+
out.push(diag(path, 'CB_ALIAS_CONFLICT', 'error', `alias "${alias}" already declared by ${seenAlias.get(aliasKey)}`));
|
|
198
|
+
else
|
|
199
|
+
seenAlias.set(aliasKey, path);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const index = buildLinkIndex(inputs, resolvedOptions);
|
|
203
|
+
const lookup = buildLookup(index.pages, resolvedOptions);
|
|
204
|
+
for (const { path, fm } of validInputs) {
|
|
205
|
+
const page = index.pages.get(pageMapKey(frontmatterIdentity(fm, resolvedOptions), resolvedOptions));
|
|
206
|
+
if (!page)
|
|
207
|
+
continue;
|
|
208
|
+
// alias colliding with a real slug/key in the same scope
|
|
209
|
+
for (const alias of page.aliases) {
|
|
210
|
+
if (collidesWithPageIdentity(alias, page.locale, lookup, resolvedOptions))
|
|
211
|
+
out.push(diag(page.path, 'CB_ALIAS_CONFLICT', 'error', `alias "${alias}" collides with an existing page identity`));
|
|
212
|
+
}
|
|
213
|
+
for (const target of fm.linksTo ?? []) {
|
|
214
|
+
const resolved = resolveTarget(target, page, lookup, resolvedOptions);
|
|
215
|
+
if (resolved.page === page) {
|
|
216
|
+
out.push(diag(page.path, 'CB_LINK_SELF', 'warning', `page "${page.slug}" links to itself`));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (resolved.page && resolved.crossLocale) {
|
|
220
|
+
out.push(diag(page.path, 'CB_LINK_CROSS_LOCALE', 'warning', `linksTo "${resolved.target}" resolves to locale "${resolved.page.locale}"`));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!resolved.page) {
|
|
224
|
+
if (targetExistsOutsideLocale(resolved.target, page.locale, lookup, resolvedOptions)) {
|
|
225
|
+
out.push(diag(page.path, 'CB_LINK_LOCALE_MISSING', 'error', `linksTo "${resolved.target}" exists in another locale but not "${page.locale ?? 'default'}"`));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const hint = closest(resolved.target, candidatesFor(page.locale, lookup, resolvedOptions));
|
|
229
|
+
out.push(diag(page.path, 'CB_LINK_UNRESOLVED', 'error', `linksTo "${resolved.target}" does not resolve to any page`, hint ? `Did you mean "${hint}"?` : undefined));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (page.linkedFrom.length === 0)
|
|
233
|
+
out.push(diag(page.path, 'CB_LINK_ORPHAN', 'warning', `page "${page.slug}" has no inbound links`));
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function normalizeFrontmatter(data, options) {
|
|
238
|
+
const out = { ...data };
|
|
239
|
+
copyConfiguredField(out, data, options.slugField, 'slug');
|
|
240
|
+
copyConfiguredField(out, data, options.keyField, 'key');
|
|
241
|
+
copyConfiguredField(out, data, options.localeField, 'locale');
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function copyConfiguredField(out, data, from, to) {
|
|
245
|
+
if (!from || from === to || !(from in data) || to in out)
|
|
246
|
+
return;
|
|
247
|
+
out[to] = data[from];
|
|
248
|
+
}
|
|
249
|
+
function effectiveLocale(fm, options) {
|
|
250
|
+
return fm.locale ?? options.defaultLocale;
|
|
251
|
+
}
|
|
252
|
+
function frontmatterIdentity(fm, options) {
|
|
253
|
+
return { slug: fm.slug, key: fm.key, locale: effectiveLocale(fm, options) };
|
|
254
|
+
}
|
|
255
|
+
function pageMapKey(page, options) {
|
|
256
|
+
if (options.resolve === 'global-slug')
|
|
257
|
+
return page.slug;
|
|
258
|
+
return scopedKey(page.slug, page.locale, options);
|
|
259
|
+
}
|
|
260
|
+
function scopedKey(value, locale, options) {
|
|
261
|
+
if (options.resolve === 'global-slug')
|
|
262
|
+
return value;
|
|
263
|
+
return `${locale ?? ''}\0${value}`;
|
|
264
|
+
}
|
|
265
|
+
function aliasMapKey(alias, locale, options) {
|
|
266
|
+
return options.resolve === 'global-slug' ? alias : `${locale ?? ''}:${alias}`;
|
|
267
|
+
}
|
|
268
|
+
function replacementFor(page, options) {
|
|
269
|
+
if (options.resolve === 'same-locale-key')
|
|
270
|
+
return page.key ?? page.slug;
|
|
271
|
+
if (options.resolve === 'prefer-same-locale-key-fallback-slug')
|
|
272
|
+
return page.key ?? page.slug;
|
|
273
|
+
return page.slug;
|
|
274
|
+
}
|
|
275
|
+
function referenceFor(page, target) {
|
|
276
|
+
return {
|
|
277
|
+
...(target ? { target } : {}),
|
|
278
|
+
...(page.locale ? { locale: page.locale } : {}),
|
|
279
|
+
slug: page.slug,
|
|
280
|
+
...(page.key ? { key: page.key } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function sameReference(ref, page) {
|
|
284
|
+
return ref.slug === page.slug && ref.locale === page.locale && ref.key === page.key;
|
|
285
|
+
}
|
|
286
|
+
function sortedRefs(refs) {
|
|
287
|
+
return [...refs].sort((a, b) => sortIdentity(a.locale, a.key ?? a.slug).localeCompare(sortIdentity(b.locale, b.key ?? b.slug)));
|
|
288
|
+
}
|
|
289
|
+
function sortIdentity(locale, value) {
|
|
290
|
+
return `${locale ?? ''}\0${value}`;
|
|
291
|
+
}
|
|
292
|
+
function buildLookup(pages, options) {
|
|
293
|
+
const lookup = {
|
|
294
|
+
bySlug: new Map(),
|
|
295
|
+
byScopedSlug: new Map(),
|
|
296
|
+
byKey: new Map(),
|
|
297
|
+
byScopedKey: new Map(),
|
|
298
|
+
aliasBySlug: new Map(),
|
|
299
|
+
aliasByScopedSlug: new Map(),
|
|
300
|
+
aliasByKey: new Map(),
|
|
301
|
+
aliasByScopedKey: new Map(),
|
|
302
|
+
};
|
|
303
|
+
for (const page of pages.values()) {
|
|
304
|
+
pushMulti(lookup.bySlug, page.slug, page);
|
|
305
|
+
lookup.byScopedSlug.set(scopedKey(page.slug, page.locale, options), page);
|
|
306
|
+
if (page.key) {
|
|
307
|
+
pushMulti(lookup.byKey, page.key, page);
|
|
308
|
+
lookup.byScopedKey.set(scopedKey(page.key, page.locale, options), page);
|
|
309
|
+
}
|
|
310
|
+
for (const alias of page.aliases) {
|
|
311
|
+
lookup.aliasBySlug.set(alias, page);
|
|
312
|
+
lookup.aliasByScopedSlug.set(scopedKey(alias, page.locale, options), page);
|
|
313
|
+
if (page.key) {
|
|
314
|
+
lookup.aliasByKey.set(alias, page);
|
|
315
|
+
lookup.aliasByScopedKey.set(scopedKey(alias, page.locale, options), page);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return lookup;
|
|
320
|
+
}
|
|
321
|
+
function pushMulti(map, key, page) {
|
|
322
|
+
const existing = map.get(key);
|
|
323
|
+
if (existing)
|
|
324
|
+
existing.push(page);
|
|
325
|
+
else
|
|
326
|
+
map.set(key, [page]);
|
|
327
|
+
}
|
|
328
|
+
function resolveTarget(rawTarget, source, lookup, options) {
|
|
329
|
+
if (typeof rawTarget !== 'string') {
|
|
330
|
+
const locale = rawTarget.locale ?? source.locale;
|
|
331
|
+
const page = rawTarget.key
|
|
332
|
+
? lookup.byScopedKey.get(scopedKey(rawTarget.key, locale, options))
|
|
333
|
+
: rawTarget.slug
|
|
334
|
+
? lookup.byScopedSlug.get(scopedKey(rawTarget.slug, locale, options))
|
|
335
|
+
: undefined;
|
|
336
|
+
const target = rawTarget.key ?? rawTarget.slug ?? '';
|
|
337
|
+
return {
|
|
338
|
+
page,
|
|
339
|
+
target,
|
|
340
|
+
explicitLocale: rawTarget.locale,
|
|
341
|
+
crossLocale: Boolean(page && rawTarget.locale && rawTarget.locale !== source.locale),
|
|
342
|
+
matchedBy: rawTarget.key ? 'key' : 'slug',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const locale = source.locale;
|
|
346
|
+
if (options.resolve === 'global-slug') {
|
|
347
|
+
const page = lookup.aliasBySlug.get(rawTarget) ?? single(lookup.bySlug.get(rawTarget));
|
|
348
|
+
return { page, target: rawTarget, crossLocale: false, matchedBy: page ? 'slug' : undefined };
|
|
349
|
+
}
|
|
350
|
+
if (options.resolve === 'same-locale-key') {
|
|
351
|
+
const scoped = scopedKey(rawTarget, locale, options);
|
|
352
|
+
const page = lookup.byScopedKey.get(scoped) ?? lookup.aliasByScopedKey.get(scoped);
|
|
353
|
+
return { page, target: rawTarget, crossLocale: false, matchedBy: page ? 'key' : undefined };
|
|
354
|
+
}
|
|
355
|
+
if (options.resolve === 'prefer-same-locale-key-fallback-slug') {
|
|
356
|
+
const scoped = scopedKey(rawTarget, locale, options);
|
|
357
|
+
const page = lookup.byScopedKey.get(scoped) ??
|
|
358
|
+
lookup.aliasByScopedKey.get(scoped) ??
|
|
359
|
+
lookup.byScopedSlug.get(scoped) ??
|
|
360
|
+
lookup.aliasByScopedSlug.get(scoped);
|
|
361
|
+
return { page, target: rawTarget, crossLocale: false, matchedBy: page ? 'key' : undefined };
|
|
362
|
+
}
|
|
363
|
+
const scoped = scopedKey(rawTarget, locale, options);
|
|
364
|
+
const page = lookup.byScopedSlug.get(scoped) ?? lookup.aliasByScopedSlug.get(scoped);
|
|
365
|
+
return { page, target: rawTarget, crossLocale: false, matchedBy: page ? 'slug' : undefined };
|
|
366
|
+
}
|
|
367
|
+
function single(values) {
|
|
368
|
+
return values?.length === 1 ? values[0] : undefined;
|
|
369
|
+
}
|
|
370
|
+
function usesKeyResolution(options) {
|
|
371
|
+
return options.resolve === 'same-locale-key';
|
|
372
|
+
}
|
|
373
|
+
function collidesWithPageIdentity(alias, locale, lookup, options) {
|
|
374
|
+
if (options.resolve === 'global-slug')
|
|
375
|
+
return lookup.bySlug.has(alias);
|
|
376
|
+
const scoped = scopedKey(alias, locale, options);
|
|
377
|
+
return (lookup.byScopedSlug.has(scoped) ||
|
|
378
|
+
(usesKeyResolution(options) && lookup.byScopedKey.has(scoped)));
|
|
379
|
+
}
|
|
380
|
+
function targetExistsOutsideLocale(target, locale, lookup, options) {
|
|
381
|
+
if (options.resolve === 'global-slug')
|
|
382
|
+
return false;
|
|
383
|
+
const byIdentity = usesKeyResolution(options) ? lookup.byKey : lookup.bySlug;
|
|
384
|
+
return (byIdentity.get(target) ?? []).some((page) => page.locale !== locale);
|
|
385
|
+
}
|
|
386
|
+
function candidatesFor(locale, lookup, options) {
|
|
387
|
+
if (options.resolve === 'global-slug')
|
|
388
|
+
return [...lookup.bySlug.keys()];
|
|
389
|
+
const out = [];
|
|
390
|
+
for (const page of lookup.byScopedSlug.values()) {
|
|
391
|
+
if (page.locale === locale)
|
|
392
|
+
out.push(usesKeyResolution(options) && page.key ? page.key : page.slug);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
function inputForPage(page, inputs, options) {
|
|
397
|
+
for (const input of inputs) {
|
|
398
|
+
const parsed = parseLinkFrontmatter(input.data, options);
|
|
399
|
+
if (!parsed.ok || parsed.value === null)
|
|
400
|
+
continue;
|
|
401
|
+
const identity = frontmatterIdentity(parsed.value, options);
|
|
402
|
+
if (pageMapKey(identity, options) === pageMapKey(page, options))
|
|
403
|
+
return input.data;
|
|
404
|
+
}
|
|
405
|
+
return {};
|
|
406
|
+
}
|
|
407
|
+
function diag(file, code, severity, message, hint) {
|
|
408
|
+
return { file, diagnostic: { code, severity, message, hint, position: FM_POSITION } };
|
|
409
|
+
}
|
|
410
|
+
// Levenshtein distance for did-you-mean hints. Small inputs (slugs), so the
|
|
411
|
+
// simple O(n*m) matrix is fine.
|
|
412
|
+
function editDistance(a, b) {
|
|
413
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
414
|
+
for (let j = 0; j <= b.length; j++)
|
|
415
|
+
dp[0][j] = j;
|
|
416
|
+
for (let i = 1; i <= a.length; i++) {
|
|
417
|
+
for (let j = 1; j <= b.length; j++) {
|
|
418
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
419
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return dp[a.length][b.length];
|
|
423
|
+
}
|
|
424
|
+
function closest(target, candidates) {
|
|
425
|
+
let best;
|
|
426
|
+
let bestD = Infinity;
|
|
427
|
+
for (const c of candidates) {
|
|
428
|
+
const d = editDistance(target, c);
|
|
429
|
+
if (d < bestD) {
|
|
430
|
+
bestD = d;
|
|
431
|
+
best = c;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return best && bestD <= Math.max(2, Math.floor(target.length / 3)) ? best : undefined;
|
|
435
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.2.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.2.0';
|