@vmz/vmz 0.0.1 → 0.0.3
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 +52 -2
- package/bin/vmz.js +4 -0
- package/dist/application-cmd.d.ts +21 -0
- package/dist/application-cmd.js +347 -0
- package/dist/bundler-adapter.d.ts +63 -0
- package/dist/bundler-adapter.js +110 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +474 -0
- package/dist/dev-session.d.ts +35 -0
- package/dist/dev-session.js +290 -0
- package/dist/document-build.d.ts +99 -0
- package/dist/document-build.js +273 -0
- package/dist/document-check.d.ts +44 -0
- package/dist/document-check.js +246 -0
- package/dist/document-cmd.d.ts +8 -0
- package/dist/document-cmd.js +146 -0
- package/dist/document-designs.d.ts +9 -0
- package/dist/document-designs.js +126 -0
- package/dist/document-enrich.d.ts +23 -0
- package/dist/document-enrich.js +233 -0
- package/dist/document-evidence.d.ts +49 -0
- package/dist/document-evidence.js +509 -0
- package/dist/document-integrate.d.ts +34 -0
- package/dist/document-integrate.js +88 -0
- package/dist/document-interactive.d.ts +69 -0
- package/dist/document-interactive.js +254 -0
- package/dist/document-locale.d.ts +31 -0
- package/dist/document-locale.js +59 -0
- package/dist/document-markdown.d.ts +12 -0
- package/dist/document-markdown.js +45 -0
- package/dist/document-scan.d.ts +21 -0
- package/dist/document-scan.js +151 -0
- package/dist/document-schema.d.ts +86 -0
- package/dist/document-schema.js +87 -0
- package/dist/explain-cmd.d.ts +5 -0
- package/dist/explain-cmd.js +123 -0
- package/dist/index.d.ts +359 -0
- package/dist/index.js +580 -0
- package/dist/invocation.d.ts +91 -0
- package/dist/invocation.js +190 -0
- package/dist/locale-check.d.ts +106 -0
- package/dist/locale-check.js +736 -0
- package/dist/locale-cmd.d.ts +5 -0
- package/dist/locale-cmd.js +442 -0
- package/dist/locale-delivery.d.ts +298 -0
- package/dist/locale-delivery.js +443 -0
- package/dist/locale-router.d.ts +206 -0
- package/dist/locale-router.js +507 -0
- package/dist/locale-runtime.d.ts +406 -0
- package/dist/locale-runtime.js +541 -0
- package/dist/locale-schema.d.ts +8 -0
- package/dist/locale-schema.js +9 -0
- package/dist/locale-tooling.d.ts +118 -0
- package/dist/locale-tooling.js +357 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +42 -0
- package/dist/packages.d.ts +26 -0
- package/dist/packages.js +146 -0
- package/dist/plugin-host.d.ts +29 -0
- package/dist/plugin-host.js +369 -0
- package/dist/refactor-cmd.d.ts +8 -0
- package/dist/refactor-cmd.js +156 -0
- package/dist/resolve-native-cli.d.ts +14 -0
- package/dist/resolve-native-cli.js +84 -0
- package/dist/resolve.d.ts +24 -0
- package/dist/resolve.js +55 -0
- package/dist/test-cmd.d.ts +9 -0
- package/dist/test-cmd.js +363 -0
- package/dist/test-compile.d.ts +2 -0
- package/dist/test-compile.js +3 -0
- package/dist/test-discover.d.ts +2 -0
- package/dist/test-discover.js +3 -0
- package/dist/test-logic.d.ts +2 -0
- package/dist/test-logic.js +3 -0
- package/dist/test-protocol.d.ts +2 -0
- package/dist/test-protocol.js +3 -0
- package/dist/watch-diff.d.ts +17 -0
- package/dist/watch-diff.js +56 -0
- package/package.json +96 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip tags / collapse whitespace for search body text.
|
|
3
|
+
* @param {string} html
|
|
4
|
+
*/
|
|
5
|
+
export declare function htmlToSearchText(html: any): string;
|
|
6
|
+
/**
|
|
7
|
+
* @param {{
|
|
8
|
+
* manifest: any,
|
|
9
|
+
* enriched: { byId: Map<string, any> },
|
|
10
|
+
* evidence: any,
|
|
11
|
+
* version?: string | null,
|
|
12
|
+
* }} opts
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDocumentSearch(opts: any): {
|
|
15
|
+
schema: string;
|
|
16
|
+
status: string;
|
|
17
|
+
version: any;
|
|
18
|
+
records: any[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Island-only resume plan for document surfaces.
|
|
22
|
+
* @param {{
|
|
23
|
+
* evidence: any,
|
|
24
|
+
* searchHref?: string,
|
|
25
|
+
* fenceBodies?: Map<string, string>,
|
|
26
|
+
* }} opts
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildDocumentIslands(opts: any): {
|
|
29
|
+
schema: string;
|
|
30
|
+
hydrate: string;
|
|
31
|
+
fullPageHydrate: boolean;
|
|
32
|
+
islands: {
|
|
33
|
+
name: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
resume: string;
|
|
36
|
+
index: any;
|
|
37
|
+
}[];
|
|
38
|
+
status: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Relative href from an HTML page to a root artifact (posix).
|
|
42
|
+
* @param {string} htmlRel
|
|
43
|
+
* @param {string} artifactName
|
|
44
|
+
*/
|
|
45
|
+
export declare function artifactHrefFromHtml(htmlRel: any, artifactName: any): string;
|
|
46
|
+
/**
|
|
47
|
+
* Stable map key for fence body lookup.
|
|
48
|
+
* @param {{ locale: string, pageKey: string, lineStart: number }} f
|
|
49
|
+
*/
|
|
50
|
+
export declare function fenceBodyKey(f: any): string;
|
|
51
|
+
/**
|
|
52
|
+
* Collect fence bodies from analyzeMarkdown results for playground islands.
|
|
53
|
+
* @param {Map<string, { fences?: any[] }>} analyzedByPageId locale:pageKey → analyze result
|
|
54
|
+
* @param {any[]} pages manifest.pages
|
|
55
|
+
*/
|
|
56
|
+
export declare function collectFenceBodies(analyzedByPageId: any, pages: any): Map<any, any>;
|
|
57
|
+
/**
|
|
58
|
+
* Render SSR island shells (no script — resume later).
|
|
59
|
+
* @param {{
|
|
60
|
+
* islands: any,
|
|
61
|
+
* searchIndexHref: string,
|
|
62
|
+
* pageKey: string,
|
|
63
|
+
* locale: string,
|
|
64
|
+
* }} opts
|
|
65
|
+
*/
|
|
66
|
+
export declare function renderIslandShellsHtml(opts: any): {
|
|
67
|
+
searchHtml: string;
|
|
68
|
+
playgroundHtml: any;
|
|
69
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Document Interactive — search index + Island-only resume plan.
|
|
4
|
+
*
|
|
5
|
+
* Not a Doc IR: build artifacts + ResumeEntry-shaped island hosts.
|
|
6
|
+
* Static HTML stays no-JS readable; islands resume later, never full-page hydrate.
|
|
7
|
+
*/
|
|
8
|
+
import { DOCUMENT_ISLANDS_SCHEMA, DOCUMENT_SEARCH_SCHEMA } from './document-schema.js';
|
|
9
|
+
import { parseFenceInfo } from './document-evidence.js';
|
|
10
|
+
/**
|
|
11
|
+
* Strip tags / collapse whitespace for search body text.
|
|
12
|
+
* @param {string} html
|
|
13
|
+
*/
|
|
14
|
+
export function htmlToSearchText(html) {
|
|
15
|
+
return String(html || '')
|
|
16
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
17
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
18
|
+
.replace(/<[^>]+>/g, ' ')
|
|
19
|
+
.replace(/ /g, ' ')
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @param {{
|
|
29
|
+
* manifest: any,
|
|
30
|
+
* enriched: { byId: Map<string, any> },
|
|
31
|
+
* evidence: any,
|
|
32
|
+
* version?: string | null,
|
|
33
|
+
* }} opts
|
|
34
|
+
*/
|
|
35
|
+
export function buildDocumentSearch(opts) {
|
|
36
|
+
const { manifest, enriched, evidence } = opts;
|
|
37
|
+
const version = opts.version ?? null;
|
|
38
|
+
/** @type {any[]} */
|
|
39
|
+
const records = [];
|
|
40
|
+
for (const page of manifest.pages || []) {
|
|
41
|
+
const id = `${page.identity.locale}:${page.identity.pageKey}`;
|
|
42
|
+
const info = enriched.byId.get(id);
|
|
43
|
+
if (!info)
|
|
44
|
+
continue;
|
|
45
|
+
const locale = page.identity.locale;
|
|
46
|
+
const pageKey = page.identity.pageKey;
|
|
47
|
+
const route = info.route || page.route;
|
|
48
|
+
const title = info.title || pageKey;
|
|
49
|
+
const text = htmlToSearchText(info.html);
|
|
50
|
+
const headings = (info.headings || []).map((h) => ({
|
|
51
|
+
id: h.id,
|
|
52
|
+
level: h.level,
|
|
53
|
+
text: h.text,
|
|
54
|
+
}));
|
|
55
|
+
const apiSymbolIds = (evidence?.apiRefs || [])
|
|
56
|
+
.filter((r) => r.locale === locale && r.pageKey === pageKey && r.status === 'ok')
|
|
57
|
+
.flatMap((r) => (r.matches || []).map((m) => m.chunkId || m.name))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
records.push({
|
|
60
|
+
kind: 'page',
|
|
61
|
+
id: `page:${locale}:${pageKey}`,
|
|
62
|
+
locale,
|
|
63
|
+
pageKey,
|
|
64
|
+
route,
|
|
65
|
+
title,
|
|
66
|
+
text,
|
|
67
|
+
headings,
|
|
68
|
+
apiSymbolIds,
|
|
69
|
+
version,
|
|
70
|
+
});
|
|
71
|
+
for (const h of headings) {
|
|
72
|
+
records.push({
|
|
73
|
+
kind: 'heading',
|
|
74
|
+
id: `heading:${locale}:${pageKey}#${h.id}`,
|
|
75
|
+
locale,
|
|
76
|
+
pageKey,
|
|
77
|
+
route: `${route}#${h.id}`,
|
|
78
|
+
title: h.text,
|
|
79
|
+
text: h.text,
|
|
80
|
+
headingId: h.id,
|
|
81
|
+
headingLevel: h.level,
|
|
82
|
+
version,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const ref of evidence?.apiRefs || []) {
|
|
87
|
+
if (ref.status !== 'ok')
|
|
88
|
+
continue;
|
|
89
|
+
for (const m of ref.matches || []) {
|
|
90
|
+
const page = (manifest.pages || []).find((p) => p.identity.locale === ref.locale && p.identity.pageKey === ref.pageKey);
|
|
91
|
+
const info = page ? enriched.byId.get(`${ref.locale}:${ref.pageKey}`) : null;
|
|
92
|
+
records.push({
|
|
93
|
+
kind: 'api',
|
|
94
|
+
id: `api:${m.chunkId || m.name}`,
|
|
95
|
+
locale: ref.locale,
|
|
96
|
+
pageKey: ref.pageKey,
|
|
97
|
+
route: info?.route || null,
|
|
98
|
+
title: m.name || m.chunkId,
|
|
99
|
+
text: `${m.name || ''} ${m.chunkId || ''} ${(m.capabilities || []).join(' ')}`.trim(),
|
|
100
|
+
apiSymbolId: m.chunkId || m.name,
|
|
101
|
+
stableId: m.stableId || { kind: 'chunk', id: m.chunkId },
|
|
102
|
+
version,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
schema: DOCUMENT_SEARCH_SCHEMA,
|
|
108
|
+
status: records.length ? 'ready' : 'empty',
|
|
109
|
+
version,
|
|
110
|
+
records,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Island-only resume plan for document surfaces.
|
|
115
|
+
* @param {{
|
|
116
|
+
* evidence: any,
|
|
117
|
+
* searchHref?: string,
|
|
118
|
+
* fenceBodies?: Map<string, string>,
|
|
119
|
+
* }} opts
|
|
120
|
+
*/
|
|
121
|
+
export function buildDocumentIslands(opts) {
|
|
122
|
+
const searchHref = opts.searchHref || 'document.search.json';
|
|
123
|
+
/** @type {any[]} */
|
|
124
|
+
const islands = [
|
|
125
|
+
{
|
|
126
|
+
name: 'DocumentSearch',
|
|
127
|
+
kind: 'search',
|
|
128
|
+
resume: 'island',
|
|
129
|
+
index: searchHref,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const fences = opts.evidence?.fences || [];
|
|
133
|
+
for (let i = 0; i < fences.length; i++) {
|
|
134
|
+
const f = fences[i];
|
|
135
|
+
const meta = parseFenceInfo(f.info);
|
|
136
|
+
const interactive = Boolean(meta.playground || meta.run);
|
|
137
|
+
if (!interactive)
|
|
138
|
+
continue;
|
|
139
|
+
if (f.status && f.status !== 'ok' && f.status !== 'highlight')
|
|
140
|
+
continue;
|
|
141
|
+
if (meta.lang !== 'vmz' && !meta.playground)
|
|
142
|
+
continue;
|
|
143
|
+
const name = `DocumentPlayground:${f.locale}:${f.pageKey}:${f.lineStart || i}`;
|
|
144
|
+
const bodyKey = `${f.locale}:${f.pageKey}:${f.lineStart}`;
|
|
145
|
+
islands.push({
|
|
146
|
+
name,
|
|
147
|
+
kind: 'playground',
|
|
148
|
+
resume: 'island',
|
|
149
|
+
fence: {
|
|
150
|
+
lang: meta.lang,
|
|
151
|
+
run: meta.run,
|
|
152
|
+
playground: Boolean(meta.playground),
|
|
153
|
+
source: meta.source,
|
|
154
|
+
pageKey: f.pageKey,
|
|
155
|
+
locale: f.locale,
|
|
156
|
+
path: f.path,
|
|
157
|
+
lineStart: f.lineStart,
|
|
158
|
+
lineEnd: f.lineEnd,
|
|
159
|
+
status: f.status,
|
|
160
|
+
},
|
|
161
|
+
preview: opts.fenceBodies?.get(bodyKey) ?? null,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
schema: DOCUMENT_ISLANDS_SCHEMA,
|
|
166
|
+
hydrate: 'island-only',
|
|
167
|
+
fullPageHydrate: false,
|
|
168
|
+
islands,
|
|
169
|
+
status: 'ready',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Relative href from an HTML page to a root artifact (posix).
|
|
174
|
+
* @param {string} htmlRel
|
|
175
|
+
* @param {string} artifactName
|
|
176
|
+
*/
|
|
177
|
+
export function artifactHrefFromHtml(htmlRel, artifactName) {
|
|
178
|
+
const depth = String(htmlRel).replace(/\\/g, '/').split('/').length - 1;
|
|
179
|
+
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
180
|
+
return prefix + artifactName;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Stable map key for fence body lookup.
|
|
184
|
+
* @param {{ locale: string, pageKey: string, lineStart: number }} f
|
|
185
|
+
*/
|
|
186
|
+
export function fenceBodyKey(f) {
|
|
187
|
+
return `${f.locale}:${f.pageKey}:${f.lineStart}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Collect fence bodies from analyzeMarkdown results for playground islands.
|
|
191
|
+
* @param {Map<string, { fences?: any[] }>} analyzedByPageId locale:pageKey → analyze result
|
|
192
|
+
* @param {any[]} pages manifest.pages
|
|
193
|
+
*/
|
|
194
|
+
export function collectFenceBodies(analyzedByPageId, pages) {
|
|
195
|
+
/** @type {Map<string, string>} */
|
|
196
|
+
const out = new Map();
|
|
197
|
+
for (const page of pages || []) {
|
|
198
|
+
const id = `${page.identity.locale}:${page.identity.pageKey}`;
|
|
199
|
+
const analyzed = analyzedByPageId.get(id);
|
|
200
|
+
if (!analyzed?.fences)
|
|
201
|
+
continue;
|
|
202
|
+
for (const fence of analyzed.fences) {
|
|
203
|
+
const meta = parseFenceInfo(fence.info);
|
|
204
|
+
if (!(meta.playground || meta.run) || meta.lang !== 'vmz')
|
|
205
|
+
continue;
|
|
206
|
+
out.set(fenceBodyKey({
|
|
207
|
+
locale: page.identity.locale,
|
|
208
|
+
pageKey: page.identity.pageKey,
|
|
209
|
+
lineStart: fence.lineStart,
|
|
210
|
+
}), String(fence.content || '').trim());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Render SSR island shells (no script — resume later).
|
|
217
|
+
* @param {{
|
|
218
|
+
* islands: any,
|
|
219
|
+
* searchIndexHref: string,
|
|
220
|
+
* pageKey: string,
|
|
221
|
+
* locale: string,
|
|
222
|
+
* }} opts
|
|
223
|
+
*/
|
|
224
|
+
export function renderIslandShellsHtml(opts) {
|
|
225
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
226
|
+
const search = `
|
|
227
|
+
<div
|
|
228
|
+
data-vmz-island="DocumentSearch"
|
|
229
|
+
data-vmz-resume="island"
|
|
230
|
+
data-vmz-search-index="${esc(opts.searchIndexHref)}"
|
|
231
|
+
role="search"
|
|
232
|
+
aria-label="Document search"
|
|
233
|
+
>
|
|
234
|
+
<p class="doc-search-fallback">Search requires Island resume; use navigation without JavaScript.</p>
|
|
235
|
+
</div>`;
|
|
236
|
+
const playgrounds = (opts.islands?.islands || [])
|
|
237
|
+
.filter((isl) => isl.kind === 'playground' && isl.fence?.locale === opts.locale && isl.fence?.pageKey === opts.pageKey)
|
|
238
|
+
.map((isl) => {
|
|
239
|
+
const preview = isl.preview ? `<pre class="doc-playground-source"><code>${esc(isl.preview)}</code></pre>` : '';
|
|
240
|
+
return `
|
|
241
|
+
<div
|
|
242
|
+
data-vmz-island="${esc(isl.name)}"
|
|
243
|
+
data-vmz-resume="island"
|
|
244
|
+
data-vmz-playground="1"
|
|
245
|
+
data-vmz-fence-lang="${esc(isl.fence?.lang || 'vmz')}"
|
|
246
|
+
aria-label="Interactive example"
|
|
247
|
+
>
|
|
248
|
+
<p class="doc-playground-fallback">Interactive example resumes as an Island; source remains readable above.</p>
|
|
249
|
+
${preview}
|
|
250
|
+
</div>`;
|
|
251
|
+
})
|
|
252
|
+
.join('');
|
|
253
|
+
return { searchHtml: search, playgroundHtml: playgrounds };
|
|
254
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale key validation & canonicalization .
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Soft-normalize for alias / conflict detection (does not validate).
|
|
6
|
+
* @param {string} raw
|
|
7
|
+
*/
|
|
8
|
+
export declare function softNormalizeLocale(raw: any): string;
|
|
9
|
+
/**
|
|
10
|
+
* Map soft-normalized key through alias table.
|
|
11
|
+
* @param {string} soft
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalLocale(soft: any): any;
|
|
14
|
+
/**
|
|
15
|
+
* Validate a top-level locale directory name.
|
|
16
|
+
* @param {string} literal
|
|
17
|
+
* @returns {{ ok: true, soft: string, canonical: string } | { ok: false, code: string, message: string }}
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateLocaleLiteral(literal: any): {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
soft?: undefined;
|
|
24
|
+
canonical?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
soft: string;
|
|
28
|
+
canonical: any;
|
|
29
|
+
code?: undefined;
|
|
30
|
+
message?: undefined;
|
|
31
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Locale key validation & canonicalization .
|
|
4
|
+
*/
|
|
5
|
+
import { LOCALE_ALIASES } from './document-schema.js';
|
|
6
|
+
/** Literal must be lowercase ASCII BCP 47-ish with `-` separators only. */
|
|
7
|
+
const LOCALE_LITERAL_RE = /^[a-z]{2,3}(-[a-z0-9]+)*$/;
|
|
8
|
+
/**
|
|
9
|
+
* Soft-normalize for alias / conflict detection (does not validate).
|
|
10
|
+
* @param {string} raw
|
|
11
|
+
*/
|
|
12
|
+
export function softNormalizeLocale(raw) {
|
|
13
|
+
return String(raw || '')
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/_/g, '-');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Map soft-normalized key through alias table.
|
|
20
|
+
* @param {string} soft
|
|
21
|
+
*/
|
|
22
|
+
export function canonicalLocale(soft) {
|
|
23
|
+
const s = softNormalizeLocale(soft);
|
|
24
|
+
return LOCALE_ALIASES[s] || s;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate a top-level locale directory name.
|
|
28
|
+
* @param {string} literal
|
|
29
|
+
* @returns {{ ok: true, soft: string, canonical: string } | { ok: false, code: string, message: string }}
|
|
30
|
+
*/
|
|
31
|
+
export function validateLocaleLiteral(literal) {
|
|
32
|
+
const name = String(literal || '');
|
|
33
|
+
if (!name) {
|
|
34
|
+
return { ok: false, code: 'document::locale::invalid', message: 'empty locale key' };
|
|
35
|
+
}
|
|
36
|
+
if (name.includes('_')) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
code: 'document::locale::separator',
|
|
40
|
+
message: `locale key must use '-' not '_': ${JSON.stringify(name)}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (name !== name.toLowerCase()) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
code: 'document::locale::case',
|
|
47
|
+
message: `locale key must be lowercase ASCII (got ${JSON.stringify(name)}; use ${JSON.stringify(name.toLowerCase())})`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!LOCALE_LITERAL_RE.test(name)) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: 'document::locale::invalid',
|
|
54
|
+
message: `locale key is not a valid lowercase BCP 47 form: ${JSON.stringify(name)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const soft = softNormalizeLocale(name);
|
|
58
|
+
return { ok: true, soft, canonical: canonicalLocale(soft) };
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document — resolve engines.markdown via the official plugin runtime.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @param {{ engines?: { markdown?: string } }} [opts]
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveMarkdownEngine(opts?: {}): Promise<{
|
|
8
|
+
engine: any;
|
|
9
|
+
analyzeMarkdown: any;
|
|
10
|
+
renderMarkdown: any;
|
|
11
|
+
slugify: any;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Document — resolve engines.markdown via the official plugin runtime.
|
|
4
|
+
*/
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { importMaybeTs } from './plugin-host.js';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
/**
|
|
12
|
+
* @param {{ engines?: { markdown?: string } }} [opts]
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveMarkdownEngine(opts = {}) {
|
|
15
|
+
const id = opts.engines?.markdown || 'markdown-it';
|
|
16
|
+
if (id !== 'markdown-it') {
|
|
17
|
+
throw new Error(`unsupported engines.markdown ${JSON.stringify(id)} (markdown-it only)`);
|
|
18
|
+
}
|
|
19
|
+
let runtimeFile = null;
|
|
20
|
+
try {
|
|
21
|
+
const pkg = require.resolve('@vmz/plugin-markdown-it/package.json');
|
|
22
|
+
runtimeFile = path.join(path.dirname(pkg), 'runtime.ts');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Developer mode: monorepo source checkout.
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const fallback = path.resolve(here, '../../../plugins/vmz-plugin-markdown-it/runtime.ts');
|
|
28
|
+
if (existsSync(fallback))
|
|
29
|
+
runtimeFile = fallback;
|
|
30
|
+
}
|
|
31
|
+
if (!runtimeFile || !existsSync(runtimeFile)) {
|
|
32
|
+
throw new Error('markdown engine needs `@vmz/plugin-markdown-it` (optional peer of `@vmz/vmz`).\n' +
|
|
33
|
+
' Install: pnpm add -D @vmz/plugin-markdown-it');
|
|
34
|
+
}
|
|
35
|
+
const mod = await importMaybeTs(runtimeFile);
|
|
36
|
+
if (typeof mod.analyzeMarkdown !== 'function') {
|
|
37
|
+
throw new Error(`cannot load markdown engine markdown-it from ${runtimeFile}`);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
engine: id,
|
|
41
|
+
analyzeMarkdown: mod.analyzeMarkdown,
|
|
42
|
+
renderMarkdown: mod.renderMarkdown,
|
|
43
|
+
slugify: mod.slugify,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan /documents tree → pages + locale dirs .
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* PageKey from locale-relative markdown path (strip extension; index → parent or "index").
|
|
6
|
+
* @param {string} relMd e.g. guide/install.md | index.md | guide/index.md
|
|
7
|
+
*/
|
|
8
|
+
export declare function pageKeyFromRel(relMd: any): any;
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} documentsRoot absolute path to .../documents
|
|
11
|
+
* @returns {{
|
|
12
|
+
* locales: string[],
|
|
13
|
+
* pages: Array<{ identity: { pageKey: string, locale: string }, sourcePath: string }>,
|
|
14
|
+
* diagnostics: Array<{ code: string, severity: string, message: string, path?: string }>,
|
|
15
|
+
* }}
|
|
16
|
+
*/
|
|
17
|
+
export declare function scanDocumentsTree(documentsRoot: any): {
|
|
18
|
+
locales: any[];
|
|
19
|
+
pages: any[];
|
|
20
|
+
diagnostics: any[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Scan /documents tree → pages + locale dirs .
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { DIAG, DOCUMENT_RESERVED_TOP } from './document-schema.js';
|
|
8
|
+
import { canonicalLocale, softNormalizeLocale, validateLocaleLiteral } from './document-locale.js';
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} dir
|
|
11
|
+
* @param {(rel: string) => void} visit
|
|
12
|
+
* @param {string} [prefix]
|
|
13
|
+
*/
|
|
14
|
+
function walkMd(dir, visit, prefix = '') {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const ent of entries) {
|
|
23
|
+
if (ent.name.startsWith('.'))
|
|
24
|
+
continue;
|
|
25
|
+
const rel = prefix ? `${prefix}/${ent.name}` : ent.name;
|
|
26
|
+
const full = path.join(dir, ent.name);
|
|
27
|
+
if (ent.isDirectory()) {
|
|
28
|
+
walkMd(full, visit, rel);
|
|
29
|
+
}
|
|
30
|
+
else if (ent.isFile() && /\.md$/i.test(ent.name)) {
|
|
31
|
+
visit(rel.replace(/\\/g, '/'));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* PageKey from locale-relative markdown path (strip extension; index → parent or "index").
|
|
37
|
+
* @param {string} relMd e.g. guide/install.md | index.md | guide/index.md
|
|
38
|
+
*/
|
|
39
|
+
export function pageKeyFromRel(relMd) {
|
|
40
|
+
const norm = relMd.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
41
|
+
let withoutExt = norm.replace(/\.md$/i, '');
|
|
42
|
+
if (withoutExt.endsWith('/index')) {
|
|
43
|
+
withoutExt = withoutExt.slice(0, -'/index'.length);
|
|
44
|
+
}
|
|
45
|
+
else if (withoutExt === 'index') {
|
|
46
|
+
withoutExt = 'index';
|
|
47
|
+
}
|
|
48
|
+
return withoutExt.replace(/^\/+|\/+$/g, '') || 'index';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} documentsRoot absolute path to .../documents
|
|
52
|
+
* @returns {{
|
|
53
|
+
* locales: string[],
|
|
54
|
+
* pages: Array<{ identity: { pageKey: string, locale: string }, sourcePath: string }>,
|
|
55
|
+
* diagnostics: Array<{ code: string, severity: string, message: string, path?: string }>,
|
|
56
|
+
* }}
|
|
57
|
+
*/
|
|
58
|
+
export function scanDocumentsTree(documentsRoot) {
|
|
59
|
+
/** @type {Array<{ code: string, severity: string, message: string, path?: string }>} */
|
|
60
|
+
const diagnostics = [];
|
|
61
|
+
/** @type {string[]} */
|
|
62
|
+
const locales = [];
|
|
63
|
+
/** @type {Array<{ identity: { pageKey: string, locale: string }, sourcePath: string }>} */
|
|
64
|
+
const pages = [];
|
|
65
|
+
if (!fs.existsSync(documentsRoot) || !fs.statSync(documentsRoot).isDirectory()) {
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
code: DIAG.LAYOUT_MISSING_DOCUMENTS,
|
|
68
|
+
severity: 'error',
|
|
69
|
+
message: `documents root missing: ${documentsRoot}`,
|
|
70
|
+
path: documentsRoot,
|
|
71
|
+
});
|
|
72
|
+
return { locales, pages, diagnostics };
|
|
73
|
+
}
|
|
74
|
+
/** @type {Map<string, string[]>} canonical → literals */
|
|
75
|
+
const byCanonical = new Map();
|
|
76
|
+
const top = fs.readdirSync(documentsRoot, { withFileTypes: true });
|
|
77
|
+
for (const ent of top) {
|
|
78
|
+
if (ent.name.startsWith('.'))
|
|
79
|
+
continue;
|
|
80
|
+
const full = path.join(documentsRoot, ent.name);
|
|
81
|
+
if (DOCUMENT_RESERVED_TOP.has(ent.name))
|
|
82
|
+
continue;
|
|
83
|
+
if (ent.isFile()) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
code: DIAG.LAYOUT_ILLEGAL_TOP,
|
|
86
|
+
severity: 'error',
|
|
87
|
+
message: `illegal top-level file under documents/ (only package.json, documents.config.*, public/ reserved): ${ent.name}`,
|
|
88
|
+
path: full,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!ent.isDirectory())
|
|
93
|
+
continue;
|
|
94
|
+
const v = validateLocaleLiteral(ent.name);
|
|
95
|
+
if (!v.ok) {
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
code: v.code,
|
|
98
|
+
severity: 'error',
|
|
99
|
+
message: v.message,
|
|
100
|
+
path: full,
|
|
101
|
+
});
|
|
102
|
+
// Still try to detect alias conflicts with soft form
|
|
103
|
+
const soft = softNormalizeLocale(ent.name);
|
|
104
|
+
const can = canonicalLocale(soft);
|
|
105
|
+
const list = byCanonical.get(can) || [];
|
|
106
|
+
list.push(ent.name);
|
|
107
|
+
byCanonical.set(can, list);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const list = byCanonical.get(v.canonical) || [];
|
|
111
|
+
list.push(ent.name);
|
|
112
|
+
byCanonical.set(v.canonical, list);
|
|
113
|
+
locales.push(ent.name);
|
|
114
|
+
const seenKeys = new Set();
|
|
115
|
+
walkMd(full, (relMd) => {
|
|
116
|
+
const pageKey = pageKeyFromRel(relMd);
|
|
117
|
+
const sourcePath = path.join(ent.name, relMd).replace(/\\/g, '/');
|
|
118
|
+
if (seenKeys.has(pageKey)) {
|
|
119
|
+
diagnostics.push({
|
|
120
|
+
code: DIAG.PAGE_DUPLICATE,
|
|
121
|
+
severity: 'error',
|
|
122
|
+
message: `duplicate PageKey ${JSON.stringify(pageKey)} under locale ${ent.name}`,
|
|
123
|
+
path: sourcePath,
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
seenKeys.add(pageKey);
|
|
128
|
+
pages.push({
|
|
129
|
+
identity: { pageKey, locale: ent.name },
|
|
130
|
+
sourcePath,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const [canonical, literals] of byCanonical) {
|
|
135
|
+
const uniq = [...new Set(literals)];
|
|
136
|
+
if (uniq.length > 1) {
|
|
137
|
+
diagnostics.push({
|
|
138
|
+
code: DIAG.LOCALE_CONFLICT,
|
|
139
|
+
severity: 'error',
|
|
140
|
+
message: `locale identity conflict for canonical ${JSON.stringify(canonical)}: directories ${uniq.map((s) => JSON.stringify(s)).join(', ')} must not coexist`,
|
|
141
|
+
path: documentsRoot,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
locales.sort();
|
|
146
|
+
pages.sort((a, b) => {
|
|
147
|
+
const c = a.identity.locale.localeCompare(b.identity.locale);
|
|
148
|
+
return c !== 0 ? c : a.identity.pageKey.localeCompare(b.identity.pageKey);
|
|
149
|
+
});
|
|
150
|
+
return { locales, pages, diagnostics };
|
|
151
|
+
}
|