@vmz/vmz 0.3.11 → 0.3.13
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/delivery-runtime-files.json +1 -0
- package/dist/cli/cli.js +46 -23
- package/dist/cli/format-cmd.d.ts +9 -17
- package/dist/cli/format-cmd.js +37 -49
- package/dist/cli/format-config.d.ts +6 -9
- package/dist/cli/format-config.js +5 -29
- package/dist/delivery/delivery-profile.js +1 -1
- package/dist/dev/dev-session.js +16 -5
- package/dist/document/document-base-css.d.ts +15 -0
- package/dist/document/document-base-css.js +578 -0
- package/dist/document/document-build.d.ts +1 -0
- package/dist/document/document-build.js +94 -78
- package/dist/document/document-client.d.ts +13 -0
- package/dist/document/document-client.js +217 -0
- package/dist/document/document-cmd.js +3 -1
- package/dist/document/document-designs.d.ts +9 -3
- package/dist/document/document-designs.js +20 -34
- package/dist/document/document-enrich.d.ts +9 -0
- package/dist/document/document-enrich.js +55 -0
- package/dist/document/document-highlight.d.ts +26 -0
- package/dist/document/document-highlight.js +54 -0
- package/dist/document/document-integrate.d.ts +5 -0
- package/dist/document/document-integrate.js +1 -0
- package/dist/document/document-interactive.js +10 -2
- package/dist/document/document-layout-render.js +6 -2
- package/dist/document/document-schema.d.ts +5 -0
- package/dist/workspace/build-assemble.js +18 -6
- package/dist/workspace/cdn-policy.js +12 -0
- package/dist/workspace/content-addressed-assets.d.ts +6 -17
- package/dist/workspace/content-addressed-assets.js +10 -95
- package/dist/workspace/delivery-runtime-imports.js +3 -0
- package/dist/workspace/native-addon.d.ts +4 -0
- package/dist/workspace/pack-native.d.ts +139 -0
- package/dist/workspace/pack-native.js +273 -0
- package/dist/workspace/pack.d.ts +113 -27
- package/dist/workspace/pack.js +271 -54
- package/dist/workspace/plugin-host.d.ts +4 -4
- package/dist/workspace/plugin-host.js +9 -12
- package/dist/workspace/production-observability.d.ts +2 -0
- package/dist/workspace/production-observability.js +6 -2
- package/dist/workspace/project-build.js +26 -4
- package/dist/workspace/public-api.d.ts +7 -3
- package/dist/workspace/public-api.js +5 -3
- package/dist/workspace/static-emit.d.ts +30 -0
- package/dist/workspace/static-emit.js +211 -18
- package/dist/workspace/user-config.d.ts +112 -0
- package/dist/workspace/user-config.js +12 -0
- package/host-runtime-files.json +1 -0
- package/locales/en-US/cli.json +5 -4
- package/package.json +20 -20
- package/dist/workspace/pack-client-packages.d.ts +0 -32
- package/dist/workspace/pack-client-packages.js +0 -437
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { checkDocuments, manifestHasErrors } from './document-check.js';
|
|
7
|
+
import { DOCUMENT_BASE_CSS } from './document-base-css.js';
|
|
8
|
+
import { DOCUMENT_CLIENT_HREF, DOCUMENT_CLIENT_JS } from './document-client.js';
|
|
7
9
|
import { resolveDocumentDesignsCss } from './document-designs.js';
|
|
8
|
-
import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
|
|
10
|
+
import { enrichDocumentContent, pageHtmlRel, pageRoute, routeHrefForDocLink } from './document-enrich.js';
|
|
9
11
|
import { enrichDocumentEvidence } from './document-evidence.js';
|
|
12
|
+
import { resolveDocumentCodeHighlighter } from './document-highlight.js';
|
|
10
13
|
import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
|
|
11
14
|
import { assertIntegratedDistReady, renderCompiledDocumentLayout } from './document-layout-render.js';
|
|
12
15
|
import { resolveMarkdownEngine } from './document-markdown.js';
|
|
@@ -39,11 +42,19 @@ export async function buildDocuments(opts) {
|
|
|
39
42
|
return { ok: false, manifest, outDir, pages: [] };
|
|
40
43
|
}
|
|
41
44
|
const analyzedByPageId = new Map();
|
|
45
|
+
const highlighter = await resolveDocumentCodeHighlighter({ code: opts.engines?.code }, manifest.diagnostics);
|
|
46
|
+
const pageKeySet = new Map();
|
|
47
|
+
for (const p of manifest.pages) {
|
|
48
|
+
const set = pageKeySet.get(p.identity.locale) || new Set();
|
|
49
|
+
set.add(p.identity.pageKey);
|
|
50
|
+
pageKeySet.set(p.identity.locale, set);
|
|
51
|
+
}
|
|
42
52
|
for (const page of manifest.pages) {
|
|
43
53
|
const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
|
|
44
54
|
const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
|
|
45
55
|
const id = `${page.identity.locale}:${page.identity.pageKey}`;
|
|
46
|
-
|
|
56
|
+
const hooks = documentRenderHooks(page, enriched.routeBase, pageKeySet, routing, highlighter);
|
|
57
|
+
analyzedByPageId.set(id, engine.analyzeMarkdown(source, hooks));
|
|
47
58
|
}
|
|
48
59
|
const fenceBodies = collectFenceBodies(analyzedByPageId, manifest.pages);
|
|
49
60
|
const search = buildDocumentSearch({
|
|
@@ -67,12 +78,20 @@ export async function buildDocuments(opts) {
|
|
|
67
78
|
}
|
|
68
79
|
fs.mkdirSync(outDir, { recursive: true });
|
|
69
80
|
const designs = resolveDocumentDesignsCss(projectRoot, appDistDir);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
// Framework document base always emits; site token bundle emits when the
|
|
82
|
+
// project has compiled tokens (integrated) or a `designs/styles/index.css`
|
|
83
|
+
// entry (standalone).
|
|
84
|
+
const baseCssPath = path.join(outDir, designs.baseHref);
|
|
85
|
+
fs.mkdirSync(path.dirname(baseCssPath), { recursive: true });
|
|
86
|
+
fs.writeFileSync(baseCssPath, DOCUMENT_BASE_CSS, 'utf8');
|
|
87
|
+
// Framework client enhancement (search + copy) ships with every build.
|
|
88
|
+
const clientPath = path.join(outDir, DOCUMENT_CLIENT_HREF);
|
|
89
|
+
fs.mkdirSync(path.dirname(clientPath), { recursive: true });
|
|
90
|
+
fs.writeFileSync(clientPath, DOCUMENT_CLIENT_JS, 'utf8');
|
|
91
|
+
if (designs.tokensCss && designs.tokensHref) {
|
|
92
|
+
const cssPath = path.join(outDir, designs.tokensHref);
|
|
73
93
|
fs.mkdirSync(path.dirname(cssPath), { recursive: true });
|
|
74
|
-
fs.writeFileSync(cssPath, designs.
|
|
75
|
-
designsHref = designs.href;
|
|
94
|
+
fs.writeFileSync(cssPath, designs.tokensCss, 'utf8');
|
|
76
95
|
}
|
|
77
96
|
const viewsDir = path.join(outDir, 'views');
|
|
78
97
|
fs.mkdirSync(viewsDir, { recursive: true });
|
|
@@ -82,6 +101,10 @@ export async function buildDocuments(opts) {
|
|
|
82
101
|
const info = enriched.byId.get(id);
|
|
83
102
|
if (!info)
|
|
84
103
|
continue;
|
|
104
|
+
// Render-pass HTML: fences highlighted and author links rewritten to
|
|
105
|
+
// route form (validation already ran on the author form in enrich).
|
|
106
|
+
const rendered = analyzedByPageId.get(id);
|
|
107
|
+
const bodyHtml = rendered?.html ?? info.html;
|
|
85
108
|
const nav = enriched.navByLocale[page.identity.locale] || [];
|
|
86
109
|
const htmlRel = pageHtmlRel(enriched.routeBase, page.identity.locale, page.identity.pageKey);
|
|
87
110
|
const htmlAbs = path.join(outDir, htmlRel);
|
|
@@ -95,10 +118,12 @@ export async function buildDocuments(opts) {
|
|
|
95
118
|
});
|
|
96
119
|
const slotHtml = buildDocumentSlotHtml({
|
|
97
120
|
nav,
|
|
98
|
-
bodyHtml
|
|
121
|
+
bodyHtml,
|
|
99
122
|
headings: info.headings,
|
|
100
|
-
|
|
123
|
+
title: info.title,
|
|
101
124
|
route: info.route,
|
|
125
|
+
routeBase: enriched.routeBase,
|
|
126
|
+
locale: page.identity.locale,
|
|
102
127
|
routing,
|
|
103
128
|
searchShellHtml: shells.searchHtml,
|
|
104
129
|
playgroundShellHtml: shells.playgroundHtml,
|
|
@@ -116,8 +141,10 @@ export async function buildDocuments(opts) {
|
|
|
116
141
|
headings: info.headings,
|
|
117
142
|
nav,
|
|
118
143
|
bodyKind: 'html',
|
|
119
|
-
html:
|
|
120
|
-
designsCss:
|
|
144
|
+
html: bodyHtml,
|
|
145
|
+
designsCss: designs.tokensHref,
|
|
146
|
+
documentCss: designs.baseHref,
|
|
147
|
+
clientJs: DOCUMENT_CLIENT_HREF,
|
|
121
148
|
noJsReadable: true,
|
|
122
149
|
hydrate: 'island-only',
|
|
123
150
|
hostShell: useCompiledShell ? 'compiled-layout' : false,
|
|
@@ -135,14 +162,9 @@ export async function buildDocuments(opts) {
|
|
|
135
162
|
title: info.title,
|
|
136
163
|
locale: page.identity.locale,
|
|
137
164
|
route: info.route,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
designsHref,
|
|
142
|
-
htmlRel,
|
|
143
|
-
searchShellHtml: shells.searchHtml,
|
|
144
|
-
playgroundShellHtml: shells.playgroundHtml,
|
|
145
|
-
routing,
|
|
165
|
+
slotHtml,
|
|
166
|
+
tokensHref: designs.tokensHref,
|
|
167
|
+
baseHref: designs.baseHref,
|
|
146
168
|
compiledLayoutHtml,
|
|
147
169
|
useCompiledShell,
|
|
148
170
|
});
|
|
@@ -159,7 +181,9 @@ export async function buildDocuments(opts) {
|
|
|
159
181
|
engine: engine.engine,
|
|
160
182
|
outDir: path.relative(projectRoot, outDir).replace(/\\/g, '/') || '.',
|
|
161
183
|
designs: designs.source,
|
|
162
|
-
designsCss:
|
|
184
|
+
designsCss: designs.tokensHref,
|
|
185
|
+
documentCss: designs.baseHref,
|
|
186
|
+
clientJs: DOCUMENT_CLIENT_HREF,
|
|
163
187
|
hostShell: useCompiledShell ? 'compiled-layout' : 'standalone',
|
|
164
188
|
pages: written,
|
|
165
189
|
evidence: 'document.evidence.json',
|
|
@@ -180,22 +204,36 @@ function resolveAppDistDir(opts, outDir) {
|
|
|
180
204
|
}
|
|
181
205
|
return fs.existsSync(path.join(outDir, 'vmz-dom.js')) ? outDir : null;
|
|
182
206
|
}
|
|
183
|
-
/** Document
|
|
184
|
-
function
|
|
207
|
+
/** Document-layer chrome copy, localized by page locale (normal case, no decorative micro labels). */
|
|
208
|
+
function docUiCopy(locale) {
|
|
209
|
+
return locale.startsWith('zh')
|
|
210
|
+
? { home: '首页', docs: '文档', toc: '本页内容' }
|
|
211
|
+
: { home: 'Home', docs: 'Documentation', toc: 'On this page' };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Document main column + sidebar (injected into the DocumentLayout slot;
|
|
215
|
+
* standalone mounts wrap the same markup — one DOM shape, one stylesheet).
|
|
216
|
+
* In-site links are route-form (`/d/<locale>/<pageKey>`), identical to
|
|
217
|
+
* application routes — no `.html` file paths leak into the page.
|
|
218
|
+
*/
|
|
219
|
+
function buildDocumentSlotHtml({ nav, bodyHtml, headings, title, route, routeBase, locale, routing, searchShellHtml = '', playgroundShellHtml = '', }) {
|
|
185
220
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
221
|
+
const copy = docUiCopy(locale);
|
|
186
222
|
const navItems = nav
|
|
187
223
|
.map((n) => {
|
|
188
|
-
const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
|
|
189
224
|
const current = n.href === route ? ' aria-current="page"' : '';
|
|
190
|
-
return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
|
|
225
|
+
return ` <li><a href="${esc(n.href)}"${current}>${esc(n.title)}</a></li>`;
|
|
191
226
|
})
|
|
192
227
|
.join('\n');
|
|
193
228
|
const toc = headings.length > 1
|
|
194
|
-
? `<nav aria-label="
|
|
229
|
+
? `<nav aria-label="${esc(copy.toc)}" class="toc">\n <p class="toc__label">${esc(copy.toc)}</p>\n <ol>\n${headings
|
|
195
230
|
.map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
|
|
196
231
|
.join('\n')}\n </ol>\n </nav>\n`
|
|
197
232
|
: '';
|
|
198
|
-
const
|
|
233
|
+
const docsRoot = pageRoute(routeBase, locale, 'index', routing);
|
|
234
|
+
const breadcrumb = `<nav class="doc-breadcrumb" aria-label="Breadcrumb"><a href="/">${esc(copy.home)}</a><span aria-hidden="true"> / </span><a href="${esc(docsRoot)}">${esc(copy.docs)}</a><span aria-hidden="true"> / </span><span aria-current="page">${esc(title)}</span></nav>`;
|
|
235
|
+
const docsNav = ` <p class="doc-sidebar__label">${esc(copy.docs)}</p>
|
|
236
|
+
<nav aria-label="Documents" class="doc-subnav">
|
|
199
237
|
<ul>
|
|
200
238
|
${navItems}
|
|
201
239
|
</ul>
|
|
@@ -206,55 +244,40 @@ ${searchShellHtml}
|
|
|
206
244
|
</aside>
|
|
207
245
|
<div class="doc-content">
|
|
208
246
|
${toc}<main id="main">
|
|
247
|
+
${breadcrumb}
|
|
209
248
|
${bodyHtml}
|
|
210
249
|
${playgroundShellHtml}
|
|
211
250
|
</main>
|
|
212
251
|
</div>`;
|
|
213
252
|
}
|
|
214
253
|
/**
|
|
215
|
-
* No-JS readable static HTML: nav + main landmarks, Island shells
|
|
216
|
-
*
|
|
254
|
+
* No-JS readable static HTML: nav + main landmarks, Island shells, and exactly
|
|
255
|
+
* one deferred framework enhancement module (`assets/vmz-document.js` — search
|
|
256
|
+
* filter + copy buttons). Integrated mounts wrap content in compiled
|
|
257
|
+
* DocumentLayout (SiteHeader/SiteFooter SSR); standalone mounts emit the same
|
|
258
|
+
* `.site.site--docs > .doc-body` skeleton themselves.
|
|
217
259
|
*/
|
|
218
|
-
function renderStaticHtml({ title, locale,
|
|
219
|
-
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
220
|
-
const depth = htmlRel.split('/').length - 1;
|
|
221
|
-
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
260
|
+
function renderStaticHtml({ title, locale, slotHtml, tokensHref, baseHref, compiledLayoutHtml, useCompiledShell = false, }) {
|
|
222
261
|
const cssHrefs = [];
|
|
223
262
|
if (useCompiledShell) {
|
|
224
263
|
cssHrefs.push('/vmz.css');
|
|
225
264
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
|
|
242
|
-
.map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
|
|
243
|
-
.join('\n')}\n </ol>\n </nav>\n`
|
|
244
|
-
: '';
|
|
245
|
-
bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
|
|
246
|
-
<nav aria-label="Documents" class="doc-subnav">
|
|
247
|
-
<ul>
|
|
248
|
-
${navItems}
|
|
249
|
-
</ul>
|
|
250
|
-
</nav>
|
|
251
|
-
${searchShellHtml}
|
|
252
|
-
${toc}<main id="main">
|
|
253
|
-
${bodyHtml}
|
|
254
|
-
${playgroundShellHtml}
|
|
255
|
-
</main>
|
|
265
|
+
// Absolute hrefs: document pages are served under the mount root by the
|
|
266
|
+
// same host as the app (integrated) or as the site root (standalone).
|
|
267
|
+
if (tokensHref)
|
|
268
|
+
cssHrefs.push(`/${tokensHref}`);
|
|
269
|
+
cssHrefs.push(`/${baseHref}`);
|
|
270
|
+
const enhancement = `<script type="module" src="/${DOCUMENT_CLIENT_HREF}"></script>`;
|
|
271
|
+
const bodyInner = useCompiledShell
|
|
272
|
+
? `${compiledLayoutHtml || ''}\n ${enhancement}`
|
|
273
|
+
: ` <a class="skip-link" href="#main">Skip to content</a>
|
|
274
|
+
<div class="site site--docs">
|
|
275
|
+
<div class="doc-body">
|
|
276
|
+
${slotHtml}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
${enhancement}
|
|
256
280
|
`;
|
|
257
|
-
}
|
|
258
281
|
const native = requireNativeAddon();
|
|
259
282
|
if (typeof native.generateHtmlShell !== 'function') {
|
|
260
283
|
throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
|
|
@@ -267,19 +290,12 @@ ${playgroundShellHtml}
|
|
|
267
290
|
bodyAttrs: ['data-vmz-hydrate', 'island-only'],
|
|
268
291
|
});
|
|
269
292
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
toRel = [...toParts.slice(0, 2), file].join('/');
|
|
279
|
-
}
|
|
280
|
-
const fromDir = path.posix.dirname(fromHtmlRel.replace(/\\/g, '/'));
|
|
281
|
-
let rel = path.posix.relative(fromDir, toRel);
|
|
282
|
-
if (!rel.startsWith('.') && !rel.startsWith('/'))
|
|
283
|
-
rel = './' + rel;
|
|
284
|
-
return rel || './index.html';
|
|
293
|
+
/** Render-pass hooks: fence highlight + author link → route-form rewrite + heading anchors. */
|
|
294
|
+
function documentRenderHooks(page, routeBase, pageKeySet, routing, highlighter) {
|
|
295
|
+
return {
|
|
296
|
+
resolveHref: (href) => routeHrefForDocLink(href, page.identity.pageKey, page.identity.locale, routeBase, pageKeySet, routing),
|
|
297
|
+
// Self-links on h2+ (the h1 is the page title — anchoring it is noise).
|
|
298
|
+
headingAnchor: (h) => (h.level >= 2 ? `#${h.id}` : null),
|
|
299
|
+
...(highlighter ? { highlight: (code, lang) => highlighter.highlightFenceInline(code, lang) } : {}),
|
|
300
|
+
};
|
|
285
301
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-owned document client enhancement (`assets/vmz-document.js`).
|
|
3
|
+
*
|
|
4
|
+
* Progressive enhancement only: pages stay no-JS readable. The module wires
|
|
5
|
+
* the static DocumentSearch shell to `document.search.json` (locale-scoped
|
|
6
|
+
* substring filter, full keyboard loop) and injects copy buttons on article
|
|
7
|
+
* code blocks. No framework runtime, no full-page hydration.
|
|
8
|
+
*
|
|
9
|
+
* Emitted verbatim by `buildDocuments`; keep it dependency-free plain JS —
|
|
10
|
+
* this file is served as-authored, never bundled.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DOCUMENT_CLIENT_HREF = "assets/vmz-document.js";
|
|
13
|
+
export declare const DOCUMENT_CLIENT_JS = "/* vmz document layer \u2014 progressive enhancement (no framework runtime). */\n(function () {\n 'use strict';\n\n var doc = document.documentElement;\n var zh = String(doc.lang || '').toLowerCase().indexOf('zh') === 0;\n var copy = zh\n ? { search: '\u641C\u7D22\u6587\u6863', noResults: '\u6CA1\u6709\u5339\u914D\u7ED3\u679C', hintJs: '\u8F93\u5165\u5173\u952E\u8BCD\u8FC7\u6EE4\u6587\u6863', copyLabel: '\u590D\u5236', copied: '\u5DF2\u590D\u5236', results: '\u641C\u7D22\u7ED3\u679C' }\n : { search: 'Search docs', noResults: 'No matching results', hintJs: 'Type to filter the docs', copyLabel: 'Copy', copied: 'Copied', results: 'Search results' };\n\n function ready(fn) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', fn, { once: true });\n } else {\n fn();\n }\n }\n\n /* Search ------------------------------------------------------------ */\n\n function enhanceSearch(host) {\n var input = host.querySelector('.doc-search__input');\n var list = host.querySelector('.doc-search__results');\n var hint = host.querySelector('.doc-search__hint');\n if (!input || !list) return;\n var indexHref = host.getAttribute('data-vmz-search-index');\n if (!indexHref) return;\n\n var locale = String(doc.lang || '');\n var records = null;\n var loading = null;\n var active = -1;\n var current = [];\n\n function loadIndex() {\n if (records) return Promise.resolve(records);\n if (loading) return loading;\n loading = fetch(indexHref, { credentials: 'same-origin' })\n .then(function (res) {\n if (!res.ok) throw new Error('search index ' + res.status);\n return res.json();\n })\n .then(function (json) {\n records = (json && json.records ? json.records : []).filter(function (r) {\n return r && r.locale === locale && (r.kind === 'page' || r.kind === 'heading') && r.route;\n });\n return records;\n })\n .catch(function () {\n records = [];\n return records;\n });\n return loading;\n }\n\n function close() {\n list.hidden = true;\n input.setAttribute('aria-expanded', 'false');\n current = [];\n active = -1;\n }\n\n function open() {\n list.hidden = false;\n input.setAttribute('aria-expanded', 'true');\n }\n\n function render(query) {\n var q = String(query || '').trim().toLowerCase();\n current = [];\n active = -1;\n list.innerHTML = '';\n if (!q) {\n close();\n return;\n }\n loadIndex().then(function (all) {\n // Re-read the live query: a slow fetch must not paint stale hits.\n var live = String(input.value || '').trim().toLowerCase();\n if (live !== q) return;\n var seen = {};\n for (var i = 0; i < all.length && current.length < 8; i++) {\n var r = all[i];\n var hay = String(r.title || '') + ' ' + String(r.text || '');\n if (hay.toLowerCase().indexOf(live) < 0) continue;\n if (seen[r.route]) continue;\n seen[r.route] = true;\n current.push(r);\n }\n list.innerHTML = '';\n if (!current.length) {\n var empty = document.createElement('li');\n empty.className = 'doc-search__empty';\n empty.setAttribute('role', 'option');\n empty.setAttribute('aria-disabled', 'true');\n empty.textContent = copy.noResults;\n list.appendChild(empty);\n } else {\n current.forEach(function (r, i) {\n var li = document.createElement('li');\n li.setAttribute('role', 'option');\n li.id = 'vmz-doc-search-hit-' + i;\n li.setAttribute('aria-selected', 'false');\n var a = document.createElement('a');\n a.href = r.route;\n a.textContent = r.title || r.route;\n if (r.kind === 'heading') {\n var ctx = document.createElement('span');\n ctx.className = 'doc-search__ctx';\n ctx.textContent = '\u00A0\u00B7\u00A0' + (r.pageKey || '');\n a.appendChild(ctx);\n }\n li.appendChild(a);\n list.appendChild(li);\n });\n }\n open();\n });\n }\n\n function move(delta) {\n if (!current.length) return;\n active = (active + delta + current.length) % current.length;\n var rows = list.querySelectorAll('li[role=\"option\"]');\n rows.forEach(function (row, i) {\n row.setAttribute('aria-selected', i === active ? 'true' : 'false');\n });\n input.setAttribute('aria-activedescendant', 'vmz-doc-search-hit-' + active);\n }\n\n input.setAttribute('role', 'combobox');\n input.setAttribute('aria-controls', list.id || 'vmz-doc-search-results');\n list.id = list.id || 'vmz-doc-search-results';\n input.setAttribute('aria-expanded', 'false');\n input.setAttribute('placeholder', copy.search);\n list.setAttribute('aria-label', copy.results);\n if (hint) hint.textContent = copy.hintJs;\n\n input.addEventListener('input', function () {\n render(input.value);\n });\n input.addEventListener('focus', function () {\n loadIndex();\n });\n input.addEventListener('keydown', function (ev) {\n if (ev.key === 'ArrowDown') {\n ev.preventDefault();\n move(1);\n } else if (ev.key === 'ArrowUp') {\n ev.preventDefault();\n move(-1);\n } else if (ev.key === 'Enter') {\n if (active >= 0 && current[active]) {\n ev.preventDefault();\n window.location.assign(current[active].route);\n }\n } else if (ev.key === 'Escape') {\n input.value = '';\n close();\n }\n });\n document.addEventListener('click', function (ev) {\n if (!host.contains(ev.target)) close();\n });\n }\n\n /* Code copy ----------------------------------------------------------- */\n\n function enhanceCopy() {\n if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return;\n var blocks = document.querySelectorAll('.doc-content main pre');\n blocks.forEach(function (pre) {\n if (pre.querySelector('.doc-copy')) return;\n var btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'doc-copy';\n btn.textContent = copy.copyLabel;\n btn.setAttribute('aria-label', copy.copyLabel);\n btn.addEventListener('click', function () {\n var code = pre.querySelector('code');\n var text = code ? code.innerText : pre.innerText;\n navigator.clipboard.writeText(text).then(function () {\n btn.textContent = copy.copied;\n btn.classList.add('doc-copy--done');\n setTimeout(function () {\n btn.textContent = copy.copyLabel;\n btn.classList.remove('doc-copy--done');\n }, 1200);\n });\n });\n pre.appendChild(btn);\n });\n }\n\n ready(function () {\n try {\n var host = document.querySelector('[data-vmz-search-index]');\n if (host) enhanceSearch(host);\n enhanceCopy();\n } catch (err) {\n /* Enhancement must never take the readable page down. */\n }\n });\n})();\n";
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-owned document client enhancement (`assets/vmz-document.js`).
|
|
3
|
+
*
|
|
4
|
+
* Progressive enhancement only: pages stay no-JS readable. The module wires
|
|
5
|
+
* the static DocumentSearch shell to `document.search.json` (locale-scoped
|
|
6
|
+
* substring filter, full keyboard loop) and injects copy buttons on article
|
|
7
|
+
* code blocks. No framework runtime, no full-page hydration.
|
|
8
|
+
*
|
|
9
|
+
* Emitted verbatim by `buildDocuments`; keep it dependency-free plain JS —
|
|
10
|
+
* this file is served as-authored, never bundled.
|
|
11
|
+
*/
|
|
12
|
+
export const DOCUMENT_CLIENT_HREF = 'assets/vmz-document.js';
|
|
13
|
+
export const DOCUMENT_CLIENT_JS = `/* vmz document layer — progressive enhancement (no framework runtime). */
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var doc = document.documentElement;
|
|
18
|
+
var zh = String(doc.lang || '').toLowerCase().indexOf('zh') === 0;
|
|
19
|
+
var copy = zh
|
|
20
|
+
? { search: '搜索文档', noResults: '没有匹配结果', hintJs: '输入关键词过滤文档', copyLabel: '复制', copied: '已复制', results: '搜索结果' }
|
|
21
|
+
: { search: 'Search docs', noResults: 'No matching results', hintJs: 'Type to filter the docs', copyLabel: 'Copy', copied: 'Copied', results: 'Search results' };
|
|
22
|
+
|
|
23
|
+
function ready(fn) {
|
|
24
|
+
if (document.readyState === 'loading') {
|
|
25
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
26
|
+
} else {
|
|
27
|
+
fn();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Search ------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
function enhanceSearch(host) {
|
|
34
|
+
var input = host.querySelector('.doc-search__input');
|
|
35
|
+
var list = host.querySelector('.doc-search__results');
|
|
36
|
+
var hint = host.querySelector('.doc-search__hint');
|
|
37
|
+
if (!input || !list) return;
|
|
38
|
+
var indexHref = host.getAttribute('data-vmz-search-index');
|
|
39
|
+
if (!indexHref) return;
|
|
40
|
+
|
|
41
|
+
var locale = String(doc.lang || '');
|
|
42
|
+
var records = null;
|
|
43
|
+
var loading = null;
|
|
44
|
+
var active = -1;
|
|
45
|
+
var current = [];
|
|
46
|
+
|
|
47
|
+
function loadIndex() {
|
|
48
|
+
if (records) return Promise.resolve(records);
|
|
49
|
+
if (loading) return loading;
|
|
50
|
+
loading = fetch(indexHref, { credentials: 'same-origin' })
|
|
51
|
+
.then(function (res) {
|
|
52
|
+
if (!res.ok) throw new Error('search index ' + res.status);
|
|
53
|
+
return res.json();
|
|
54
|
+
})
|
|
55
|
+
.then(function (json) {
|
|
56
|
+
records = (json && json.records ? json.records : []).filter(function (r) {
|
|
57
|
+
return r && r.locale === locale && (r.kind === 'page' || r.kind === 'heading') && r.route;
|
|
58
|
+
});
|
|
59
|
+
return records;
|
|
60
|
+
})
|
|
61
|
+
.catch(function () {
|
|
62
|
+
records = [];
|
|
63
|
+
return records;
|
|
64
|
+
});
|
|
65
|
+
return loading;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function close() {
|
|
69
|
+
list.hidden = true;
|
|
70
|
+
input.setAttribute('aria-expanded', 'false');
|
|
71
|
+
current = [];
|
|
72
|
+
active = -1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function open() {
|
|
76
|
+
list.hidden = false;
|
|
77
|
+
input.setAttribute('aria-expanded', 'true');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function render(query) {
|
|
81
|
+
var q = String(query || '').trim().toLowerCase();
|
|
82
|
+
current = [];
|
|
83
|
+
active = -1;
|
|
84
|
+
list.innerHTML = '';
|
|
85
|
+
if (!q) {
|
|
86
|
+
close();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
loadIndex().then(function (all) {
|
|
90
|
+
// Re-read the live query: a slow fetch must not paint stale hits.
|
|
91
|
+
var live = String(input.value || '').trim().toLowerCase();
|
|
92
|
+
if (live !== q) return;
|
|
93
|
+
var seen = {};
|
|
94
|
+
for (var i = 0; i < all.length && current.length < 8; i++) {
|
|
95
|
+
var r = all[i];
|
|
96
|
+
var hay = String(r.title || '') + ' ' + String(r.text || '');
|
|
97
|
+
if (hay.toLowerCase().indexOf(live) < 0) continue;
|
|
98
|
+
if (seen[r.route]) continue;
|
|
99
|
+
seen[r.route] = true;
|
|
100
|
+
current.push(r);
|
|
101
|
+
}
|
|
102
|
+
list.innerHTML = '';
|
|
103
|
+
if (!current.length) {
|
|
104
|
+
var empty = document.createElement('li');
|
|
105
|
+
empty.className = 'doc-search__empty';
|
|
106
|
+
empty.setAttribute('role', 'option');
|
|
107
|
+
empty.setAttribute('aria-disabled', 'true');
|
|
108
|
+
empty.textContent = copy.noResults;
|
|
109
|
+
list.appendChild(empty);
|
|
110
|
+
} else {
|
|
111
|
+
current.forEach(function (r, i) {
|
|
112
|
+
var li = document.createElement('li');
|
|
113
|
+
li.setAttribute('role', 'option');
|
|
114
|
+
li.id = 'vmz-doc-search-hit-' + i;
|
|
115
|
+
li.setAttribute('aria-selected', 'false');
|
|
116
|
+
var a = document.createElement('a');
|
|
117
|
+
a.href = r.route;
|
|
118
|
+
a.textContent = r.title || r.route;
|
|
119
|
+
if (r.kind === 'heading') {
|
|
120
|
+
var ctx = document.createElement('span');
|
|
121
|
+
ctx.className = 'doc-search__ctx';
|
|
122
|
+
ctx.textContent = ' · ' + (r.pageKey || '');
|
|
123
|
+
a.appendChild(ctx);
|
|
124
|
+
}
|
|
125
|
+
li.appendChild(a);
|
|
126
|
+
list.appendChild(li);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
open();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function move(delta) {
|
|
134
|
+
if (!current.length) return;
|
|
135
|
+
active = (active + delta + current.length) % current.length;
|
|
136
|
+
var rows = list.querySelectorAll('li[role="option"]');
|
|
137
|
+
rows.forEach(function (row, i) {
|
|
138
|
+
row.setAttribute('aria-selected', i === active ? 'true' : 'false');
|
|
139
|
+
});
|
|
140
|
+
input.setAttribute('aria-activedescendant', 'vmz-doc-search-hit-' + active);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
input.setAttribute('role', 'combobox');
|
|
144
|
+
input.setAttribute('aria-controls', list.id || 'vmz-doc-search-results');
|
|
145
|
+
list.id = list.id || 'vmz-doc-search-results';
|
|
146
|
+
input.setAttribute('aria-expanded', 'false');
|
|
147
|
+
input.setAttribute('placeholder', copy.search);
|
|
148
|
+
list.setAttribute('aria-label', copy.results);
|
|
149
|
+
if (hint) hint.textContent = copy.hintJs;
|
|
150
|
+
|
|
151
|
+
input.addEventListener('input', function () {
|
|
152
|
+
render(input.value);
|
|
153
|
+
});
|
|
154
|
+
input.addEventListener('focus', function () {
|
|
155
|
+
loadIndex();
|
|
156
|
+
});
|
|
157
|
+
input.addEventListener('keydown', function (ev) {
|
|
158
|
+
if (ev.key === 'ArrowDown') {
|
|
159
|
+
ev.preventDefault();
|
|
160
|
+
move(1);
|
|
161
|
+
} else if (ev.key === 'ArrowUp') {
|
|
162
|
+
ev.preventDefault();
|
|
163
|
+
move(-1);
|
|
164
|
+
} else if (ev.key === 'Enter') {
|
|
165
|
+
if (active >= 0 && current[active]) {
|
|
166
|
+
ev.preventDefault();
|
|
167
|
+
window.location.assign(current[active].route);
|
|
168
|
+
}
|
|
169
|
+
} else if (ev.key === 'Escape') {
|
|
170
|
+
input.value = '';
|
|
171
|
+
close();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
document.addEventListener('click', function (ev) {
|
|
175
|
+
if (!host.contains(ev.target)) close();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Code copy ----------------------------------------------------------- */
|
|
180
|
+
|
|
181
|
+
function enhanceCopy() {
|
|
182
|
+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return;
|
|
183
|
+
var blocks = document.querySelectorAll('.doc-content main pre');
|
|
184
|
+
blocks.forEach(function (pre) {
|
|
185
|
+
if (pre.querySelector('.doc-copy')) return;
|
|
186
|
+
var btn = document.createElement('button');
|
|
187
|
+
btn.type = 'button';
|
|
188
|
+
btn.className = 'doc-copy';
|
|
189
|
+
btn.textContent = copy.copyLabel;
|
|
190
|
+
btn.setAttribute('aria-label', copy.copyLabel);
|
|
191
|
+
btn.addEventListener('click', function () {
|
|
192
|
+
var code = pre.querySelector('code');
|
|
193
|
+
var text = code ? code.innerText : pre.innerText;
|
|
194
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
195
|
+
btn.textContent = copy.copied;
|
|
196
|
+
btn.classList.add('doc-copy--done');
|
|
197
|
+
setTimeout(function () {
|
|
198
|
+
btn.textContent = copy.copyLabel;
|
|
199
|
+
btn.classList.remove('doc-copy--done');
|
|
200
|
+
}, 1200);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
pre.appendChild(btn);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ready(function () {
|
|
208
|
+
try {
|
|
209
|
+
var host = document.querySelector('[data-vmz-search-index]');
|
|
210
|
+
if (host) enhanceSearch(host);
|
|
211
|
+
enhanceCopy();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
/* Enhancement must never take the readable page down. */
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
})();
|
|
217
|
+
`;
|
|
@@ -8,6 +8,7 @@ import { enrichDocumentContent } from './document-enrich.js';
|
|
|
8
8
|
import { enrichDocumentEvidence } from './document-evidence.js';
|
|
9
9
|
import { resolveMarkdownEngine } from './document-markdown.js';
|
|
10
10
|
import { createWorkspace } from '../workspace/public-api.js';
|
|
11
|
+
import { loadVmzConfig } from '../workspace/plugin-host.js';
|
|
11
12
|
import { log } from '../workspace/log.js';
|
|
12
13
|
import { emitPrettyJson } from '../workspace/pretty-json.js';
|
|
13
14
|
export function registerDocumentCommands(parent) {
|
|
@@ -62,7 +63,8 @@ async function cmdDocumentBuild(args) {
|
|
|
62
63
|
const strict = Boolean(args.strict);
|
|
63
64
|
let result;
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
+
const cfg = await loadVmzConfig(projectRoot).catch(() => null);
|
|
67
|
+
result = await buildDocuments({ projectRoot, outDir, strict, engines: cfg?.engines });
|
|
66
68
|
}
|
|
67
69
|
catch (e) {
|
|
68
70
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface DocumentDesignsCssResult {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** Site token stylesheet body (empty when the project has no compiled tokens). */
|
|
3
|
+
tokensCss: string;
|
|
4
|
+
tokensSource: string | null;
|
|
5
|
+
/** Site token stylesheet href (emitted name), null when no tokens exist. */
|
|
6
|
+
tokensHref: string | null;
|
|
7
|
+
/** Framework document base stylesheet href (always emitted). */
|
|
8
|
+
baseHref: string;
|
|
9
|
+
/** Combined source description for the manifest. */
|
|
10
|
+
source: string;
|
|
5
11
|
}
|
|
6
12
|
export declare function resolveDocumentDesignsCss(projectRoot: string, appDistDir?: string | null): DocumentDesignsCssResult;
|
|
@@ -1,53 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Document — wire
|
|
3
|
-
* static document assets.
|
|
2
|
+
* Document — wire the compiled app stylesheet and the framework-owned document
|
|
3
|
+
* base layer into static document assets.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `vmz.css`
|
|
5
|
+
* Layering (see 规划设计/vmz 04 文档层):
|
|
6
|
+
* - `assets/vmz-designs.css`: the site's compiled design tokens (`vmz-designs.css`
|
|
7
|
+
* / `vmz.css` from the app build, or `designs/styles/index.css` standalone).
|
|
8
|
+
* - `assets/vmz-document.css`: the framework document base (`DOCUMENT_BASE_CSS`),
|
|
9
|
+
* the single canonical layout/typography surface. Site `designs/document/*`
|
|
10
|
+
* is no longer an input — brand customization goes through `designs/tokens`.
|
|
8
11
|
*/
|
|
9
12
|
import fs from 'node:fs';
|
|
10
13
|
import path from 'node:path';
|
|
11
14
|
export function resolveDocumentDesignsCss(projectRoot, appDistDir) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return { css: '', source: null, href: null };
|
|
15
|
-
}
|
|
16
|
-
const parts = [];
|
|
17
|
-
const sources = [];
|
|
18
|
-
// Layout first so @import stays at stylesheet top; compiled tokens follow.
|
|
19
|
-
const layoutCandidates = [
|
|
20
|
-
path.join(designsDir, 'document', 'chrome.css'),
|
|
21
|
-
path.join(designsDir, 'styles', 'document.css'),
|
|
22
|
-
];
|
|
23
|
-
for (const p of layoutCandidates) {
|
|
24
|
-
if (!fs.existsSync(p))
|
|
25
|
-
continue;
|
|
26
|
-
parts.push(fs.readFileSync(p, 'utf8'));
|
|
27
|
-
sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
15
|
+
let tokensCss = '';
|
|
16
|
+
let tokensSource = null;
|
|
30
17
|
if (appDistDir) {
|
|
31
18
|
const compiled = [path.join(appDistDir, 'vmz-designs.css'), path.join(appDistDir, 'vmz.css')].find((p) => fs.existsSync(p));
|
|
32
19
|
if (!compiled) {
|
|
33
20
|
throw new Error(`vmz.document: app dist ${appDistDir} is missing vmz-designs.css / vmz.css; build the app before the documents`);
|
|
34
21
|
}
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
tokensCss = fs.readFileSync(compiled, 'utf8');
|
|
23
|
+
tokensSource = path.relative(projectRoot, compiled).replace(/\\/g, '/');
|
|
37
24
|
}
|
|
38
25
|
else {
|
|
39
|
-
const entry = path.join(
|
|
26
|
+
const entry = path.join(projectRoot, 'designs', 'styles', 'index.css');
|
|
40
27
|
if (fs.existsSync(entry)) {
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
tokensCss = fs.readFileSync(entry, 'utf8');
|
|
29
|
+
tokensSource = 'designs/styles/index.css';
|
|
43
30
|
}
|
|
44
31
|
}
|
|
45
|
-
if (!parts.length) {
|
|
46
|
-
return { css: '', source: 'designs/', href: null };
|
|
47
|
-
}
|
|
48
32
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
tokensCss,
|
|
34
|
+
tokensSource,
|
|
35
|
+
tokensHref: tokensCss ? 'assets/vmz-designs.css' : null,
|
|
36
|
+
baseHref: 'assets/vmz-document.css',
|
|
37
|
+
source: ['vmz/document-base', tokensSource].filter(Boolean).join('+'),
|
|
52
38
|
};
|
|
53
39
|
}
|
|
@@ -50,3 +50,12 @@ export declare function pageRoute(routeBase: string, locale: string, pageKey: st
|
|
|
50
50
|
/** Static file path relative to out dir (posix). */
|
|
51
51
|
export declare function pageHtmlRel(routeBase: string, locale: string, pageKey: string): string;
|
|
52
52
|
export declare function enrichDocumentContent(manifest: DocumentManifest, ctx: EnrichDocumentContentCtx): EnrichDocumentContentResult;
|
|
53
|
+
/**
|
|
54
|
+
* Author link → route-form href for emitted pages (document layer link form:
|
|
55
|
+
* `/d/<locale>/<pageKey>` without `.html`, matching application routes).
|
|
56
|
+
*
|
|
57
|
+
* Returns `null` for links that must stay as-authored: external/mailto, API
|
|
58
|
+
* refs, pure anchors, absolute application routes outside the document mount,
|
|
59
|
+
* and unresolvable relatives (the check pass reports those as broken).
|
|
60
|
+
*/
|
|
61
|
+
export declare function routeHrefForDocLink(href: string, fromPageKey: string, locale: string, routeBase: string, pageKeySet: Map<string, Set<string>>, routing?: DocumentRoutingStrategy): string | null;
|