@webstir-io/webstir-frontend 0.1.40 → 0.1.41
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 +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- package/src/watch/watchReporter.ts +0 -98
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
2
|
import { marked } from 'marked';
|
|
4
3
|
import { load } from 'cheerio';
|
|
5
4
|
import hljs from 'highlight.js/lib/common';
|
|
6
5
|
import { FOLDERS, FILES, FILE_NAMES, EXTENSIONS } from '../core/constants.js';
|
|
7
6
|
import { ensureDir, pathExists, readFile, readJson, remove, writeFile } from '../utils/fs.js';
|
|
7
|
+
import { scanGlob } from '../utils/glob.js';
|
|
8
8
|
import { shouldProcess } from '../utils/changedFile.js';
|
|
9
9
|
import { getPageDirectories } from '../core/pages.js';
|
|
10
10
|
import { readPageManifest, readSharedAssets } from '../assets/assetManifest.js';
|
|
@@ -20,7 +20,7 @@ export function createContentBuilder(context) {
|
|
|
20
20
|
async publish() {
|
|
21
21
|
await publishContentPages(context);
|
|
22
22
|
await publishContentManifests(context);
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
async function buildContentPages(context) {
|
|
@@ -29,13 +29,11 @@ async function buildContentPages(context) {
|
|
|
29
29
|
if (!(await pathExists(contentRoot))) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
if (!
|
|
32
|
+
if (!isSidebarOverrideChange(context, contentRoot) &&
|
|
33
|
+
!shouldProcess(context, [{ directory: contentRoot, extensions: ['.md'] }])) {
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const files = await
|
|
36
|
-
cwd: contentRoot,
|
|
37
|
-
nodir: true
|
|
38
|
-
});
|
|
36
|
+
const files = await scanGlob('**/*.md', { cwd: contentRoot });
|
|
39
37
|
if (files.length === 0) {
|
|
40
38
|
return;
|
|
41
39
|
}
|
|
@@ -46,9 +44,7 @@ async function buildContentPages(context) {
|
|
|
46
44
|
const templateHtml = await readFile(appTemplatePath);
|
|
47
45
|
validateAppTemplate(templateHtml, appTemplatePath);
|
|
48
46
|
const buildPagesUrlPrefix = resolvePagesUrlPrefix(config.paths.build.frontend, config.paths.build.pages);
|
|
49
|
-
const navEntries = context.enable?.contentNav === true
|
|
50
|
-
? await collectContentManifests(context)
|
|
51
|
-
: [];
|
|
47
|
+
const navEntries = context.enable?.contentNav === true ? await collectContentManifests(context) : [];
|
|
52
48
|
await removeStaleContentOutputs(context, files, buildPagesUrlPrefix);
|
|
53
49
|
for (const relative of files) {
|
|
54
50
|
const sourcePath = path.join(contentRoot, relative);
|
|
@@ -57,7 +53,7 @@ async function buildContentPages(context) {
|
|
|
57
53
|
const htmlBody = (await renderMarkdownDoc(content)).html;
|
|
58
54
|
const segments = resolveDocsSegments(relative);
|
|
59
55
|
const pagePath = path.join(...segments);
|
|
60
|
-
const href =
|
|
56
|
+
const href = `/${segments.join('/')}/`;
|
|
61
57
|
const pageTitle = resolveTitle(frontmatter, content, segments);
|
|
62
58
|
const mergedHtml = mergeContentIntoTemplate(templateHtml, pageTitle, htmlBody, frontmatter.description, context.enable?.contentNav === true, buildPagesUrlPrefix, navEntries, href);
|
|
63
59
|
const mergedWithOptIn = injectGlobalOptInScripts(mergedHtml, context.enable);
|
|
@@ -74,10 +70,7 @@ async function publishContentPages(context) {
|
|
|
74
70
|
if (!(await pathExists(contentRoot))) {
|
|
75
71
|
return;
|
|
76
72
|
}
|
|
77
|
-
const files = await
|
|
78
|
-
cwd: contentRoot,
|
|
79
|
-
nodir: true
|
|
80
|
-
});
|
|
73
|
+
const files = await scanGlob('**/*.md', { cwd: contentRoot });
|
|
81
74
|
if (files.length === 0) {
|
|
82
75
|
return;
|
|
83
76
|
}
|
|
@@ -91,9 +84,7 @@ async function publishContentPages(context) {
|
|
|
91
84
|
const buildPagesUrlPrefix = resolvePagesUrlPrefix(config.paths.build.frontend, config.paths.build.pages);
|
|
92
85
|
await removeStaleContentOutputsForRoot(config.paths.dist.content, files, pagesUrlPrefix);
|
|
93
86
|
const shared = await readSharedAssets(config.paths.dist.frontend);
|
|
94
|
-
const navEntries = context.enable?.contentNav === true
|
|
95
|
-
? await collectContentManifests(context)
|
|
96
|
-
: [];
|
|
87
|
+
const navEntries = context.enable?.contentNav === true ? await collectContentManifests(context) : [];
|
|
97
88
|
const docsManifestRoot = path.join(config.paths.dist.pages, 'docs');
|
|
98
89
|
const docsManifest = await readPageManifest(docsManifestRoot, 'docs');
|
|
99
90
|
if (!docsManifest.css || !docsManifest.js) {
|
|
@@ -106,7 +97,7 @@ async function publishContentPages(context) {
|
|
|
106
97
|
const { frontmatter, content } = extractFrontmatter(markdown);
|
|
107
98
|
const segments = resolveDocsSegments(relative);
|
|
108
99
|
const pagePath = path.join(...segments);
|
|
109
|
-
const href =
|
|
100
|
+
const href = `/${segments.join('/')}/`;
|
|
110
101
|
const pageTitle = resolveTitle(frontmatter, content, segments);
|
|
111
102
|
const rendered = await renderMarkdownDoc(content);
|
|
112
103
|
const htmlBody = rendered.html;
|
|
@@ -114,7 +105,7 @@ async function publishContentPages(context) {
|
|
|
114
105
|
const mergedWithOptIn = injectGlobalOptInScripts(mergedHtml, context.enable);
|
|
115
106
|
const rewritten = await rewriteContentForPublish(mergedWithOptIn, shared, docsManifest, {
|
|
116
107
|
pagesUrlPrefix,
|
|
117
|
-
buildPagesUrlPrefix
|
|
108
|
+
buildPagesUrlPrefix,
|
|
118
109
|
});
|
|
119
110
|
const distDir = path.join(config.paths.dist.pages, pagePath);
|
|
120
111
|
const distPath = path.join(distDir, FILES.indexHtml);
|
|
@@ -124,7 +115,7 @@ async function publishContentPages(context) {
|
|
|
124
115
|
outputPath: distPath,
|
|
125
116
|
html: rewritten,
|
|
126
117
|
headingIds: rendered.headingIds,
|
|
127
|
-
sourcePath
|
|
118
|
+
sourcePath,
|
|
128
119
|
});
|
|
129
120
|
}
|
|
130
121
|
validateRenderedContentPages(renderedPages);
|
|
@@ -145,10 +136,7 @@ async function removeStaleContentOutputsForRoot(docsRoot, contentFiles, pagesUrl
|
|
|
145
136
|
const segments = resolveDocsSegments(relative);
|
|
146
137
|
expected.add(path.join(...segments.slice(1)));
|
|
147
138
|
}
|
|
148
|
-
const candidateIndexes = await
|
|
149
|
-
cwd: docsRoot,
|
|
150
|
-
nodir: true
|
|
151
|
-
});
|
|
139
|
+
const candidateIndexes = await scanGlob('**/index.html', { cwd: docsRoot });
|
|
152
140
|
const docsPrefix = resolvePageAssetUrl(pagesUrlPrefix, 'docs', '');
|
|
153
141
|
const docsAssetToken = docsPrefix.endsWith('/') ? docsPrefix : `${docsPrefix}/`;
|
|
154
142
|
for (const relativeIndex of candidateIndexes) {
|
|
@@ -163,8 +151,7 @@ async function removeStaleContentOutputsForRoot(docsRoot, contentFiles, pagesUrl
|
|
|
163
151
|
const absoluteIndex = path.join(docsRoot, relativeIndex);
|
|
164
152
|
const html = await readFile(absoluteIndex);
|
|
165
153
|
// Only remove pages that were generated by the content pipeline (avoid deleting user-owned pages under /docs).
|
|
166
|
-
const looksLikeContentOutput = html.includes('class="docs-article"')
|
|
167
|
-
&& html.includes(docsAssetToken);
|
|
154
|
+
const looksLikeContentOutput = html.includes('class="docs-article"') && html.includes(docsAssetToken);
|
|
168
155
|
if (!looksLikeContentOutput) {
|
|
169
156
|
continue;
|
|
170
157
|
}
|
|
@@ -184,11 +171,12 @@ async function buildContentManifests(context) {
|
|
|
184
171
|
}
|
|
185
172
|
return;
|
|
186
173
|
}
|
|
187
|
-
if (!
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
174
|
+
if (!isSidebarOverrideChange(context, contentRoot) &&
|
|
175
|
+
!shouldProcess(context, [
|
|
176
|
+
{ directory: contentRoot, extensions: ['.md'] },
|
|
177
|
+
// `webstir enable search` updates package.json and should emit the index immediately.
|
|
178
|
+
{ directory: config.paths.workspace, extensions: ['.json'] },
|
|
179
|
+
])) {
|
|
192
180
|
return;
|
|
193
181
|
}
|
|
194
182
|
const navEntries = await collectContentManifests(context);
|
|
@@ -199,7 +187,7 @@ async function buildContentManifests(context) {
|
|
|
199
187
|
if (context.enable?.search === true) {
|
|
200
188
|
const [docEntries, pageEntries] = await Promise.all([
|
|
201
189
|
collectContentSearchEntries(context),
|
|
202
|
-
collectPageSearchEntries(context)
|
|
190
|
+
collectPageSearchEntries(context),
|
|
203
191
|
]);
|
|
204
192
|
const searchEntries = [...docEntries, ...pageEntries];
|
|
205
193
|
if (searchEntries.length > 0) {
|
|
@@ -218,7 +206,7 @@ async function publishContentManifests(context) {
|
|
|
218
206
|
if (context.enable?.search === true) {
|
|
219
207
|
const [docEntries, pageEntries] = await Promise.all([
|
|
220
208
|
hasContent ? collectContentSearchEntries(context) : Promise.resolve([]),
|
|
221
|
-
collectPageSearchEntries(context)
|
|
209
|
+
collectPageSearchEntries(context),
|
|
222
210
|
]);
|
|
223
211
|
const searchEntries = [...docEntries, ...pageEntries];
|
|
224
212
|
if (searchEntries.length > 0) {
|
|
@@ -230,10 +218,7 @@ async function collectContentManifests(context) {
|
|
|
230
218
|
const { config } = context;
|
|
231
219
|
const contentRoot = config.paths.src.content;
|
|
232
220
|
const overrides = await loadSidebarOverrides(contentRoot);
|
|
233
|
-
const files = await
|
|
234
|
-
cwd: contentRoot,
|
|
235
|
-
nodir: true
|
|
236
|
-
});
|
|
221
|
+
const files = await scanGlob('**/*.md', { cwd: contentRoot });
|
|
237
222
|
if (files.length === 0) {
|
|
238
223
|
return [];
|
|
239
224
|
}
|
|
@@ -244,17 +229,15 @@ async function collectContentManifests(context) {
|
|
|
244
229
|
const { frontmatter, content } = extractFrontmatter(markdown);
|
|
245
230
|
const segments = resolveDocsSegments(relative);
|
|
246
231
|
const parsed = path.parse(relative);
|
|
247
|
-
const section = parsed.dir && parsed.dir.trim().length > 0
|
|
248
|
-
|
|
249
|
-
: undefined;
|
|
250
|
-
const href = '/' + segments.join('/') + '/';
|
|
232
|
+
const section = parsed.dir && parsed.dir.trim().length > 0 ? parsed.dir.split(path.sep)[0] : undefined;
|
|
233
|
+
const href = `/${segments.join('/')}/`;
|
|
251
234
|
const title = resolveTitle(frontmatter, content, segments);
|
|
252
235
|
const order = frontmatter.order;
|
|
253
236
|
const baseEntry = {
|
|
254
237
|
path: href,
|
|
255
238
|
title,
|
|
256
239
|
section,
|
|
257
|
-
order
|
|
240
|
+
order,
|
|
258
241
|
};
|
|
259
242
|
const merged = applySidebarOverride(baseEntry, overrides);
|
|
260
243
|
if (merged) {
|
|
@@ -287,10 +270,7 @@ async function collectContentSearchEntries(context) {
|
|
|
287
270
|
const { config } = context;
|
|
288
271
|
const contentRoot = config.paths.src.content;
|
|
289
272
|
const overrides = await loadSidebarOverrides(contentRoot);
|
|
290
|
-
const files = await
|
|
291
|
-
cwd: contentRoot,
|
|
292
|
-
nodir: true
|
|
293
|
-
});
|
|
273
|
+
const files = await scanGlob('**/*.md', { cwd: contentRoot });
|
|
294
274
|
if (files.length === 0) {
|
|
295
275
|
return [];
|
|
296
276
|
}
|
|
@@ -300,7 +280,7 @@ async function collectContentSearchEntries(context) {
|
|
|
300
280
|
const markdown = await readFile(sourcePath);
|
|
301
281
|
const { frontmatter, content } = extractFrontmatter(markdown);
|
|
302
282
|
const segments = resolveDocsSegments(relative);
|
|
303
|
-
const href =
|
|
283
|
+
const href = `/${segments.join('/')}/`;
|
|
304
284
|
const rawTitle = resolveTitle(frontmatter, content, segments);
|
|
305
285
|
const title = applySidebarTitleOverride(href, rawTitle, overrides);
|
|
306
286
|
if (!title) {
|
|
@@ -320,7 +300,7 @@ async function collectContentSearchEntries(context) {
|
|
|
320
300
|
description: frontmatter.description?.trim() ? frontmatter.description.trim() : undefined,
|
|
321
301
|
headings,
|
|
322
302
|
excerpt,
|
|
323
|
-
kind: 'docs'
|
|
303
|
+
kind: 'docs',
|
|
324
304
|
});
|
|
325
305
|
}
|
|
326
306
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -357,7 +337,7 @@ async function loadSidebarOverrides(contentRoot) {
|
|
|
357
337
|
map.set(normalized, {
|
|
358
338
|
...entry,
|
|
359
339
|
path: normalized,
|
|
360
|
-
order: defaultOrder
|
|
340
|
+
order: defaultOrder,
|
|
361
341
|
});
|
|
362
342
|
}
|
|
363
343
|
return map;
|
|
@@ -367,14 +347,22 @@ async function loadSidebarOverrides(contentRoot) {
|
|
|
367
347
|
if (!value || typeof value !== 'object') {
|
|
368
348
|
continue;
|
|
369
349
|
}
|
|
370
|
-
const rawPath = typeof value.path === 'string'
|
|
350
|
+
const rawPath = typeof value.path === 'string'
|
|
351
|
+
? String(value.path)
|
|
352
|
+
: key;
|
|
371
353
|
const normalized = normalizeDocsOverrideHref(rawPath);
|
|
372
354
|
if (!normalized) {
|
|
373
355
|
continue;
|
|
374
356
|
}
|
|
375
|
-
const title = typeof value.title === 'string'
|
|
376
|
-
|
|
377
|
-
|
|
357
|
+
const title = typeof value.title === 'string'
|
|
358
|
+
? String(value.title)
|
|
359
|
+
: undefined;
|
|
360
|
+
const section = typeof value.section === 'string'
|
|
361
|
+
? String(value.section)
|
|
362
|
+
: undefined;
|
|
363
|
+
const hidden = typeof value.hidden === 'boolean'
|
|
364
|
+
? Boolean(value.hidden)
|
|
365
|
+
: undefined;
|
|
378
366
|
const orderValue = value.order;
|
|
379
367
|
const order = typeof orderValue === 'number' && Number.isFinite(orderValue) ? orderValue : undefined;
|
|
380
368
|
map.set(normalized, { path: normalized, title, section, hidden, order });
|
|
@@ -382,6 +370,12 @@ async function loadSidebarOverrides(contentRoot) {
|
|
|
382
370
|
}
|
|
383
371
|
return map;
|
|
384
372
|
}
|
|
373
|
+
function isSidebarOverrideChange(context, contentRoot) {
|
|
374
|
+
if (!context.changedFile) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
return (path.resolve(context.changedFile) === path.join(path.resolve(contentRoot), '_sidebar.json'));
|
|
378
|
+
}
|
|
385
379
|
function applySidebarOverride(entry, overrides) {
|
|
386
380
|
const key = normalizeDocsOverrideHref(entry.path);
|
|
387
381
|
const override = key ? overrides.get(key) : undefined;
|
|
@@ -391,14 +385,20 @@ function applySidebarOverride(entry, overrides) {
|
|
|
391
385
|
if (override.hidden === true) {
|
|
392
386
|
return null;
|
|
393
387
|
}
|
|
394
|
-
const title = typeof override.title === 'string' && override.title.trim().length > 0
|
|
395
|
-
|
|
396
|
-
|
|
388
|
+
const title = typeof override.title === 'string' && override.title.trim().length > 0
|
|
389
|
+
? override.title.trim()
|
|
390
|
+
: entry.title;
|
|
391
|
+
const section = typeof override.section === 'string' && override.section.trim().length > 0
|
|
392
|
+
? override.section.trim()
|
|
393
|
+
: entry.section;
|
|
394
|
+
const order = typeof override.order === 'number' && Number.isFinite(override.order)
|
|
395
|
+
? override.order
|
|
396
|
+
: entry.order;
|
|
397
397
|
return {
|
|
398
398
|
path: entry.path,
|
|
399
399
|
title,
|
|
400
400
|
section,
|
|
401
|
-
order
|
|
401
|
+
order,
|
|
402
402
|
};
|
|
403
403
|
}
|
|
404
404
|
function applySidebarTitleOverride(href, fallbackTitle, overrides) {
|
|
@@ -410,7 +410,9 @@ function applySidebarTitleOverride(href, fallbackTitle, overrides) {
|
|
|
410
410
|
if (override.hidden === true) {
|
|
411
411
|
return null;
|
|
412
412
|
}
|
|
413
|
-
const title = typeof override.title === 'string' && override.title.trim().length > 0
|
|
413
|
+
const title = typeof override.title === 'string' && override.title.trim().length > 0
|
|
414
|
+
? override.title.trim()
|
|
415
|
+
: fallbackTitle;
|
|
414
416
|
return title;
|
|
415
417
|
}
|
|
416
418
|
function normalizeDocsOverrideHref(value) {
|
|
@@ -444,13 +446,14 @@ async function collectPageSearchEntries(context) {
|
|
|
444
446
|
const titleFromTag = document('title').first().text().trim();
|
|
445
447
|
const titleFromH1 = document('h1').first().text().trim();
|
|
446
448
|
const title = titleFromTag || titleFromH1 || toTitleCase(page.name);
|
|
447
|
-
const description = document('meta[name="description"]').first().attr('content')?.trim()
|
|
448
|
-
|| undefined;
|
|
449
|
+
const description = document('meta[name="description"]').first().attr('content')?.trim() || undefined;
|
|
449
450
|
const headings = document('h2, h3')
|
|
450
451
|
.toArray()
|
|
451
452
|
.map((element) => document(element).text().trim())
|
|
452
453
|
.filter((text) => text.length > 0);
|
|
453
|
-
const mainText = (document('main').first().text() || document.text())
|
|
454
|
+
const mainText = (document('main').first().text() || document.text())
|
|
455
|
+
.replace(/\s+/g, ' ')
|
|
456
|
+
.trim();
|
|
454
457
|
const excerpt = mainText.length > 240 ? `${mainText.slice(0, 240).trim()}…` : mainText;
|
|
455
458
|
entries.push({
|
|
456
459
|
path: resolvePageHref(page.name),
|
|
@@ -458,7 +461,7 @@ async function collectPageSearchEntries(context) {
|
|
|
458
461
|
description,
|
|
459
462
|
headings,
|
|
460
463
|
excerpt,
|
|
461
|
-
kind: 'page'
|
|
464
|
+
kind: 'page',
|
|
462
465
|
});
|
|
463
466
|
}
|
|
464
467
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -534,7 +537,7 @@ function extractFrontmatter(markdown) {
|
|
|
534
537
|
return { frontmatter, content };
|
|
535
538
|
}
|
|
536
539
|
function resolveTitle(frontmatter, content, segments) {
|
|
537
|
-
if (frontmatter.title
|
|
540
|
+
if (frontmatter.title?.trim()) {
|
|
538
541
|
return frontmatter.title.trim();
|
|
539
542
|
}
|
|
540
543
|
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
@@ -568,7 +571,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
|
|
|
568
571
|
if (main.length === 0 || head.length === 0) {
|
|
569
572
|
throw new Error('Base application template for content pages must include <head> and <main> elements.');
|
|
570
573
|
}
|
|
571
|
-
if (description
|
|
574
|
+
if (description?.trim()) {
|
|
572
575
|
const meta = head.find('meta[name="description"]').first();
|
|
573
576
|
if (meta.length > 0) {
|
|
574
577
|
meta.attr('content', description.trim());
|
|
@@ -581,8 +584,11 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
|
|
|
581
584
|
const effectiveDescription = (description ?? '').trim() || defaultDescription;
|
|
582
585
|
// Ensure content pages load the shared app styles.
|
|
583
586
|
const cssHref = `/${FOLDERS.app}/app.css`;
|
|
584
|
-
const existingStylesheet = head.find(`link[rel="stylesheet"][href="${cssHref}"]`).first().length > 0
|
|
585
|
-
|
|
587
|
+
const existingStylesheet = head.find(`link[rel="stylesheet"][href="${cssHref}"]`).first().length > 0 ||
|
|
588
|
+
head
|
|
589
|
+
.find('link[rel="stylesheet"]')
|
|
590
|
+
.toArray()
|
|
591
|
+
.some((element) => {
|
|
586
592
|
const href = document(element).attr('href');
|
|
587
593
|
return typeof href === 'string' && href.includes('/app/app.css');
|
|
588
594
|
});
|
|
@@ -591,8 +597,11 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
|
|
|
591
597
|
}
|
|
592
598
|
// Ensure docs pages load the docs layout styles.
|
|
593
599
|
const docsCssHref = resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`);
|
|
594
|
-
const existingDocsStylesheet = head.find(`link[rel="stylesheet"][href="${docsCssHref}"]`).first().length > 0
|
|
595
|
-
|
|
600
|
+
const existingDocsStylesheet = head.find(`link[rel="stylesheet"][href="${docsCssHref}"]`).first().length > 0 ||
|
|
601
|
+
head
|
|
602
|
+
.find('link[rel="stylesheet"]')
|
|
603
|
+
.toArray()
|
|
604
|
+
.some((element) => {
|
|
596
605
|
const href = document(element).attr('href');
|
|
597
606
|
return typeof href === 'string' && href.includes('/docs/index.css');
|
|
598
607
|
});
|
|
@@ -646,7 +655,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
|
|
|
646
655
|
' </div>',
|
|
647
656
|
' </div>',
|
|
648
657
|
' </div>',
|
|
649
|
-
'</section>'
|
|
658
|
+
'</section>',
|
|
650
659
|
].join('\n')
|
|
651
660
|
: [
|
|
652
661
|
'<section class="docs-layout" data-scope="docs">',
|
|
@@ -655,7 +664,7 @@ function mergeContentIntoTemplate(appHtml, pageName, bodyHtml, description, enab
|
|
|
655
664
|
` <article class="docs-article ws-markdown">${bodyHtml}</article>`,
|
|
656
665
|
' </div>',
|
|
657
666
|
' </div>',
|
|
658
|
-
'</section>'
|
|
667
|
+
'</section>',
|
|
659
668
|
].join('\n');
|
|
660
669
|
main.html(docsLayoutHtml);
|
|
661
670
|
return document.root().html() ?? '';
|
|
@@ -689,7 +698,7 @@ function buildContentNavTree(entries) {
|
|
|
689
698
|
title: 'Docs',
|
|
690
699
|
children: [],
|
|
691
700
|
isPage: false,
|
|
692
|
-
position: position
|
|
701
|
+
position: position++,
|
|
693
702
|
};
|
|
694
703
|
for (const entry of entries) {
|
|
695
704
|
const normalizedPath = normalizeDocsPath(entry.path);
|
|
@@ -709,7 +718,7 @@ function buildContentNavTree(entries) {
|
|
|
709
718
|
title: toTitleCase(segment.replace(/[-_]/g, ' ')),
|
|
710
719
|
children: [],
|
|
711
720
|
isPage: false,
|
|
712
|
-
position: position
|
|
721
|
+
position: position++,
|
|
713
722
|
};
|
|
714
723
|
current.children.push(child);
|
|
715
724
|
}
|
|
@@ -734,9 +743,7 @@ function renderContentNavList(nodes, currentPath, depth = 0) {
|
|
|
734
743
|
const label = node.isPage
|
|
735
744
|
? `<a class="docs-nav__link" href="${node.path}"${isActive ? ' aria-current="page"' : ''}>${escapeHtml(node.title)}</a>`
|
|
736
745
|
: `<span class="docs-nav__label">${escapeHtml(node.title)}</span>`;
|
|
737
|
-
const nested = node.children.length > 0
|
|
738
|
-
? renderContentNavList(node.children, currentPath, depth + 1)
|
|
739
|
-
: '';
|
|
746
|
+
const nested = node.children.length > 0 ? renderContentNavList(node.children, currentPath, depth + 1) : '';
|
|
740
747
|
return `<li class="docs-nav__item"${activeAttr}>${label}${nested}</li>`;
|
|
741
748
|
});
|
|
742
749
|
return `<ol class="${listClass}">${items.join('')}</ol>`;
|
|
@@ -748,7 +755,10 @@ function renderContentBreadcrumb(titleByPath, currentPath) {
|
|
|
748
755
|
const crumbs = [];
|
|
749
756
|
const rootTitle = titleByPath.get('/docs/') ?? 'Docs';
|
|
750
757
|
crumbs.push({ title: rootTitle, href: '/docs/' });
|
|
751
|
-
const segments = currentPath
|
|
758
|
+
const segments = currentPath
|
|
759
|
+
.replace(/^\/docs\/?/, '')
|
|
760
|
+
.split('/')
|
|
761
|
+
.filter(Boolean);
|
|
752
762
|
let href = '/docs/';
|
|
753
763
|
for (const segment of segments) {
|
|
754
764
|
href = `${href}${segment}/`;
|
|
@@ -784,7 +794,7 @@ function ensureMetaName(head, name, content) {
|
|
|
784
794
|
async function renderMarkdownDoc(markdown) {
|
|
785
795
|
const renderer = getMarkdownRenderer();
|
|
786
796
|
const expanded = await expandAdmonitions(markdown, renderer);
|
|
787
|
-
const rawHtml = await marked.parse(expanded, { renderer
|
|
797
|
+
const rawHtml = await marked.parse(expanded, { renderer });
|
|
788
798
|
const linked = rewriteMarkdownLinks(rawHtml);
|
|
789
799
|
const { html, headingIds } = ensureHeadingIds(linked);
|
|
790
800
|
return { html, headingIds };
|
|
@@ -821,7 +831,7 @@ const ADMONITION_TITLES = {
|
|
|
821
831
|
tip: 'Tip',
|
|
822
832
|
info: 'Info',
|
|
823
833
|
warning: 'Warning',
|
|
824
|
-
danger: 'Danger'
|
|
834
|
+
danger: 'Danger',
|
|
825
835
|
};
|
|
826
836
|
async function expandAdmonitions(markdown, renderer) {
|
|
827
837
|
const lines = markdown.split(/\r?\n/);
|
|
@@ -856,18 +866,22 @@ async function expandAdmonitions(markdown, renderer) {
|
|
|
856
866
|
break;
|
|
857
867
|
}
|
|
858
868
|
const bodyMarkdown = inner.join('\n').trim();
|
|
859
|
-
const bodyHtml = bodyMarkdown.length > 0 ? await marked.parse(bodyMarkdown, { renderer
|
|
869
|
+
const bodyHtml = bodyMarkdown.length > 0 ? await marked.parse(bodyMarkdown, { renderer }) : '';
|
|
860
870
|
out.push([
|
|
861
871
|
`<aside class="docs-callout docs-callout--${kindRaw}">`,
|
|
862
872
|
` <div class="docs-callout__title">${escapeHtml(title)}</div>`,
|
|
863
873
|
` <div class="docs-callout__body">${bodyHtml}</div>`,
|
|
864
|
-
`</aside
|
|
874
|
+
`</aside>`,
|
|
865
875
|
].join('\n'));
|
|
866
876
|
}
|
|
867
877
|
return out.join('\n');
|
|
868
878
|
}
|
|
869
879
|
function isAdmonitionKind(value) {
|
|
870
|
-
return value === 'note' ||
|
|
880
|
+
return (value === 'note' ||
|
|
881
|
+
value === 'tip' ||
|
|
882
|
+
value === 'info' ||
|
|
883
|
+
value === 'warning' ||
|
|
884
|
+
value === 'danger');
|
|
871
885
|
}
|
|
872
886
|
function ensureHeadingIds(html) {
|
|
873
887
|
const document = load(html);
|
|
@@ -928,9 +942,9 @@ function validateRenderedContentPages(pages) {
|
|
|
928
942
|
if (!resolved) {
|
|
929
943
|
continue;
|
|
930
944
|
}
|
|
931
|
-
const isDocsPage = resolved.pathname === '/docs'
|
|
932
|
-
|
|
933
|
-
|
|
945
|
+
const isDocsPage = resolved.pathname === '/docs' ||
|
|
946
|
+
resolved.pathname === '/docs/' ||
|
|
947
|
+
resolved.pathname.startsWith('/docs/');
|
|
934
948
|
if (!isDocsPage) {
|
|
935
949
|
continue;
|
|
936
950
|
}
|
|
@@ -995,21 +1009,19 @@ async function rewriteContentForPublish(html, shared, docsManifest, options) {
|
|
|
995
1009
|
document(`link[href="/app/app.css"]`).attr('href', `/app/${shared.css}`);
|
|
996
1010
|
}
|
|
997
1011
|
if (shared?.js) {
|
|
998
|
-
document(`script[src="/app/app.js"]`)
|
|
999
|
-
.attr('src', `/app/${shared.js}`)
|
|
1000
|
-
.attr('type', 'module');
|
|
1012
|
+
document(`script[src="/app/app.js"]`).attr('src', `/app/${shared.js}`).attr('type', 'module');
|
|
1001
1013
|
}
|
|
1002
1014
|
if (docsManifest.css) {
|
|
1003
1015
|
const selector = [
|
|
1004
1016
|
`link[href="${resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]`,
|
|
1005
|
-
`link[href="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]
|
|
1017
|
+
`link[href="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.css}`)}"]`,
|
|
1006
1018
|
].join(', ');
|
|
1007
1019
|
document(selector).attr('href', resolvePageAssetUrl(pagesUrlPrefix, 'docs', docsManifest.css));
|
|
1008
1020
|
}
|
|
1009
1021
|
if (docsManifest.js) {
|
|
1010
1022
|
const selector = [
|
|
1011
1023
|
`script[src="${resolvePageAssetUrl(pagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]`,
|
|
1012
|
-
`script[src="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]
|
|
1024
|
+
`script[src="${resolvePageAssetUrl(buildPagesUrlPrefix, 'docs', `${FILES.index}${EXTENSIONS.js}`)}"]`,
|
|
1013
1025
|
].join(', ');
|
|
1014
1026
|
document(selector)
|
|
1015
1027
|
.attr('src', resolvePageAssetUrl(pagesUrlPrefix, 'docs', docsManifest.js))
|