@zeropress/build-pages 0.5.1 → 0.5.4
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 +171 -43
- package/action.yml +6 -6
- package/dist/action.js +37 -5
- package/dist/prebuild.js +4629 -0
- package/package.json +3 -2
- package/schemas/zeropress-build-pages.config.v0.1.schema.json +2 -62
- package/src/action.js +2 -2
- package/src/index.js +65 -24
- package/src/prebuild.js +338 -53
package/src/prebuild.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import matter from 'gray-matter';
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const rootDir = process.cwd();
|
|
7
|
-
const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'
|
|
8
|
+
const sourceDir = resolveEnvPath(['ZEROPRESS_BUILD_PAGES_SOURCE'], 'docs');
|
|
8
9
|
const defaultConfigPath = path.join(sourceDir, '.zeropress', 'config.json');
|
|
9
10
|
const configPath = resolveOptionalEnvPath(['ZEROPRESS_BUILD_PAGES_CONFIG'], defaultConfigPath);
|
|
10
11
|
const outDir = path.join(rootDir, '.zeropress');
|
|
12
|
+
const buildPagesConfigPath = path.join(outDir, 'build-pages-config.json');
|
|
11
13
|
const previewDataPath = path.join(outDir, 'preview-data.json');
|
|
12
|
-
const
|
|
14
|
+
const buildReportPath = path.join(outDir, 'build-report.json');
|
|
13
15
|
const skipUntitledMarkdown = readBooleanEnv('ZEROPRESS_SKIP_UNTITLED_MARKDOWN');
|
|
14
16
|
const FRONT_PAGE_TYPES = new Set(['theme_index', 'markdown', 'html']);
|
|
17
|
+
const BUILD_PAGES_CONFIG_SCHEMA_URL = 'https://zeropress.dev/schemas/zeropress-build-pages.config.v0.1.schema.json';
|
|
18
|
+
const PREVIEW_DATA_SCHEMA_URL = 'https://zeropress.dev/schemas/preview-data.v0.5.schema.json';
|
|
15
19
|
|
|
16
20
|
class PrebuildMarkdownError extends Error {
|
|
17
21
|
constructor(sourcePath, reason, expected = '', code = 'invalid_markdown') {
|
|
@@ -41,23 +45,43 @@ async function main() {
|
|
|
41
45
|
normalizeFrontPageConfig(config.front_page),
|
|
42
46
|
config.front_page,
|
|
43
47
|
);
|
|
48
|
+
const menus = normalizeMenus(config.menus);
|
|
49
|
+
const customHtmlConfig = normalizeCustomHtmlConfig(config.custom_html);
|
|
50
|
+
const resolvedConfig = buildResolvedConfig(config, {
|
|
51
|
+
frontPageConfig,
|
|
52
|
+
menus,
|
|
53
|
+
customHtmlConfig,
|
|
54
|
+
});
|
|
44
55
|
const sourceFiles = await listMarkdownFiles(sourceDir);
|
|
45
56
|
const skippedMarkdown = [];
|
|
46
57
|
const pageInputs = [];
|
|
47
58
|
|
|
48
59
|
for (const sourcePath of sourceFiles) {
|
|
49
60
|
const rawMarkdown = await fs.readFile(sourcePath, 'utf8');
|
|
50
|
-
const
|
|
61
|
+
const parsedMarkdown = parseMarkdownSource(rawMarkdown, sourcePath);
|
|
62
|
+
const frontMatterStatus = readFrontMatterStatus(parsedMarkdown.frontMatter.status, sourcePath);
|
|
63
|
+
if (frontMatterStatus !== 'published') {
|
|
64
|
+
recordSkippedMarkdown(skippedMarkdown, sourcePath, frontMatterStatus.reason);
|
|
65
|
+
if (frontMatterStatus.warning) {
|
|
66
|
+
console.warn(formatSkippedMarkdownWarning(sourcePath, frontMatterStatus.reason, frontMatterStatus.expected));
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const frontMatter = normalizePublishedFrontMatter(parsedMarkdown.frontMatter, sourcePath);
|
|
72
|
+
const title = extractTitleOrSkip(parsedMarkdown.bodyMarkdown, sourcePath, skippedMarkdown, frontMatter.title);
|
|
51
73
|
if (!title) {
|
|
52
74
|
continue;
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
pageInputs.push({
|
|
56
78
|
sourcePath,
|
|
57
|
-
|
|
79
|
+
bodyMarkdown: parsedMarkdown.bodyMarkdown,
|
|
80
|
+
frontMatter,
|
|
58
81
|
title,
|
|
59
82
|
route: buildPageRoute(sourcePath, {
|
|
60
83
|
allowRootIndex: shouldAllowRootMarkdownIndex(frontPageConfig),
|
|
84
|
+
routePath: frontMatter.path,
|
|
61
85
|
}),
|
|
62
86
|
});
|
|
63
87
|
}
|
|
@@ -66,29 +90,30 @@ async function main() {
|
|
|
66
90
|
pageInputs.map(({ sourcePath, route }) => [sourcePath, route]),
|
|
67
91
|
);
|
|
68
92
|
|
|
69
|
-
const pages = pageInputs.map(({ sourcePath,
|
|
93
|
+
const pages = pageInputs.map(({ sourcePath, bodyMarkdown, frontMatter, title, route }) => ({
|
|
70
94
|
title,
|
|
71
95
|
slug: route.slug,
|
|
72
96
|
path: route.path,
|
|
73
97
|
meta: {
|
|
98
|
+
...frontMatter.meta,
|
|
74
99
|
source_markdown_url: buildSourceMarkdownUrl(sourcePath),
|
|
75
100
|
},
|
|
76
|
-
content: rewriteMarkdownLinks(
|
|
101
|
+
content: rewriteMarkdownLinks(bodyMarkdown, sourcePath, routeBySourcePath),
|
|
77
102
|
document_type: 'markdown',
|
|
78
|
-
excerpt: extractExcerpt(
|
|
103
|
+
excerpt: frontMatter.description || extractExcerpt(bodyMarkdown, title),
|
|
79
104
|
status: 'published',
|
|
80
105
|
}));
|
|
81
106
|
|
|
82
|
-
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs,
|
|
107
|
+
const frontPageResult = await buildFrontPageData(frontPageConfig, pageInputs, resolvedConfig);
|
|
83
108
|
if (frontPageResult.page) {
|
|
84
109
|
pages.push(frontPageResult.page);
|
|
85
110
|
}
|
|
86
111
|
|
|
87
|
-
const site = buildSiteData(
|
|
88
|
-
const
|
|
89
|
-
const customHtml = await buildCustomHtmlData(config.custom_html);
|
|
112
|
+
const site = buildSiteData(resolvedConfig, frontPageResult.frontPage);
|
|
113
|
+
const customHtml = await buildCustomHtmlData(customHtmlConfig);
|
|
90
114
|
|
|
91
115
|
const previewData = {
|
|
116
|
+
$schema: PREVIEW_DATA_SCHEMA_URL,
|
|
92
117
|
version: '0.5',
|
|
93
118
|
generator: 'zeropress-build-pages',
|
|
94
119
|
generated_at: new Date().toISOString(),
|
|
@@ -108,6 +133,7 @@ async function main() {
|
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
await fs.mkdir(outDir, { recursive: true });
|
|
136
|
+
await fs.writeFile(buildPagesConfigPath, `${JSON.stringify(resolvedConfig, null, 2)}\n`, 'utf8');
|
|
111
137
|
await fs.writeFile(previewDataPath, `${JSON.stringify(previewData, null, 2)}\n`, 'utf8');
|
|
112
138
|
|
|
113
139
|
const report = buildPrebuildReport({
|
|
@@ -119,7 +145,7 @@ async function main() {
|
|
|
119
145
|
frontPage: frontPageResult.frontPage,
|
|
120
146
|
customHtml,
|
|
121
147
|
});
|
|
122
|
-
await fs.writeFile(
|
|
148
|
+
await fs.writeFile(buildReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
123
149
|
|
|
124
150
|
console.log(`Wrote ${path.relative(rootDir, previewDataPath)} with ${pages.length} pages`);
|
|
125
151
|
printPrebuildSummary(report);
|
|
@@ -189,27 +215,66 @@ async function loadPrebuildConfig() {
|
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
function buildSiteData(config, frontPage) {
|
|
192
|
-
const configuredSite = isPlainObject(config.site) ? config.site :
|
|
193
|
-
const footer = normalizeFooter(configuredSite.footer);
|
|
218
|
+
const configuredSite = isPlainObject(config.site) ? config.site : normalizeSiteConfig(undefined);
|
|
194
219
|
|
|
195
220
|
const site = {
|
|
196
|
-
title:
|
|
197
|
-
description:
|
|
198
|
-
url:
|
|
199
|
-
mediaBaseUrl:
|
|
200
|
-
locale:
|
|
201
|
-
postsPerPage:
|
|
202
|
-
dateFormat:
|
|
203
|
-
timeFormat:
|
|
204
|
-
timezone:
|
|
205
|
-
permalinks:
|
|
221
|
+
title: configuredSite.title,
|
|
222
|
+
description: configuredSite.description,
|
|
223
|
+
url: configuredSite.url,
|
|
224
|
+
mediaBaseUrl: '',
|
|
225
|
+
locale: 'en-US',
|
|
226
|
+
postsPerPage: 10,
|
|
227
|
+
dateFormat: 'YYYY-MM-DD',
|
|
228
|
+
timeFormat: 'HH:mm',
|
|
229
|
+
timezone: 'UTC',
|
|
230
|
+
permalinks: defaultPermalinks(),
|
|
206
231
|
front_page: frontPage,
|
|
207
232
|
post_index: {
|
|
208
233
|
enabled: false,
|
|
209
234
|
},
|
|
210
|
-
disallowComments:
|
|
235
|
+
disallowComments: true,
|
|
211
236
|
};
|
|
212
237
|
|
|
238
|
+
if (configuredSite.footer) {
|
|
239
|
+
site.footer = configuredSite.footer;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return site;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildResolvedConfig(config, { frontPageConfig, menus, customHtmlConfig }) {
|
|
246
|
+
const resolvedConfig = {
|
|
247
|
+
$schema: BUILD_PAGES_CONFIG_SCHEMA_URL,
|
|
248
|
+
version: '0.1',
|
|
249
|
+
site: normalizeSiteConfig(config.site),
|
|
250
|
+
front_page: frontPageConfig,
|
|
251
|
+
menus,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (customHtmlConfig) {
|
|
255
|
+
resolvedConfig.custom_html = customHtmlConfig;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return resolvedConfig;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeSiteConfig(value) {
|
|
262
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
263
|
+
throw new PrebuildConfigError(
|
|
264
|
+
'site must be an object.',
|
|
265
|
+
' "site": { "title": "My Docs", "description": "Project documentation" }',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const configuredSite = isPlainObject(value) ? value : {};
|
|
270
|
+
assertKnownConfigKeys(configuredSite, ['title', 'description', 'url', 'footer'], 'site');
|
|
271
|
+
const site = {
|
|
272
|
+
title: readConfigString(configuredSite.title, 'Documentation'),
|
|
273
|
+
description: readConfigString(configuredSite.description, 'A documentation site.'),
|
|
274
|
+
url: readEnv('ZEROPRESS_SITE_URL', readConfigString(configuredSite.url, '')),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const footer = normalizeFooter(configuredSite.footer);
|
|
213
278
|
if (footer) {
|
|
214
279
|
site.footer = footer;
|
|
215
280
|
}
|
|
@@ -218,9 +283,13 @@ function buildSiteData(config, frontPage) {
|
|
|
218
283
|
}
|
|
219
284
|
|
|
220
285
|
function normalizeFooter(value) {
|
|
221
|
-
if (
|
|
286
|
+
if (value === undefined) {
|
|
222
287
|
return undefined;
|
|
223
288
|
}
|
|
289
|
+
if (!isPlainObject(value)) {
|
|
290
|
+
throw new PrebuildConfigError('site.footer must be an object.');
|
|
291
|
+
}
|
|
292
|
+
assertKnownConfigKeys(value, ['copyright_text', 'attribution'], 'site.footer');
|
|
224
293
|
|
|
225
294
|
const footer = {};
|
|
226
295
|
const copyrightText = readConfigString(value.copyright_text, '');
|
|
@@ -228,6 +297,15 @@ function normalizeFooter(value) {
|
|
|
228
297
|
footer.copyright_text = copyrightText;
|
|
229
298
|
}
|
|
230
299
|
|
|
300
|
+
if (value.attribution !== undefined && !isPlainObject(value.attribution)) {
|
|
301
|
+
throw new PrebuildConfigError('site.footer.attribution must be an object.');
|
|
302
|
+
}
|
|
303
|
+
if (isPlainObject(value.attribution)) {
|
|
304
|
+
assertKnownConfigKeys(value.attribution, ['enabled'], 'site.footer.attribution');
|
|
305
|
+
if (value.attribution.enabled !== undefined && typeof value.attribution.enabled !== 'boolean') {
|
|
306
|
+
throw new PrebuildConfigError('site.footer.attribution.enabled must be a boolean when provided.');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
231
309
|
if (isPlainObject(value.attribution) && typeof value.attribution.enabled === 'boolean') {
|
|
232
310
|
footer.attribution = {
|
|
233
311
|
enabled: value.attribution.enabled,
|
|
@@ -390,7 +468,7 @@ function isZeropressHtmlFile(filePath) {
|
|
|
390
468
|
return filePath.startsWith('.zeropress/') && filePath.toLowerCase().endsWith('.html');
|
|
391
469
|
}
|
|
392
470
|
|
|
393
|
-
|
|
471
|
+
function normalizeCustomHtmlConfig(value) {
|
|
394
472
|
if (value === undefined) {
|
|
395
473
|
return undefined;
|
|
396
474
|
}
|
|
@@ -408,18 +486,18 @@ async function buildCustomHtmlData(value) {
|
|
|
408
486
|
);
|
|
409
487
|
}
|
|
410
488
|
|
|
411
|
-
const
|
|
489
|
+
const customHtmlConfig = {};
|
|
412
490
|
if (value.head_end !== undefined) {
|
|
413
|
-
|
|
491
|
+
customHtmlConfig.head_end = normalizeCustomHtmlSlotConfig(value.head_end, 'custom_html.head_end');
|
|
414
492
|
}
|
|
415
493
|
if (value.body_end !== undefined) {
|
|
416
|
-
|
|
494
|
+
customHtmlConfig.body_end = normalizeCustomHtmlSlotConfig(value.body_end, 'custom_html.body_end');
|
|
417
495
|
}
|
|
418
496
|
|
|
419
|
-
return
|
|
497
|
+
return customHtmlConfig;
|
|
420
498
|
}
|
|
421
499
|
|
|
422
|
-
|
|
500
|
+
function normalizeCustomHtmlSlotConfig(value, pathLabel) {
|
|
423
501
|
if (!isPlainObject(value)) {
|
|
424
502
|
throw new PrebuildConfigError(`${pathLabel} must be an object.`);
|
|
425
503
|
}
|
|
@@ -439,7 +517,29 @@ async function buildCustomHtmlSlotData(value, pathLabel) {
|
|
|
439
517
|
);
|
|
440
518
|
}
|
|
441
519
|
|
|
442
|
-
|
|
520
|
+
return {
|
|
521
|
+
file,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function buildCustomHtmlData(config) {
|
|
526
|
+
if (!config) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const customHtml = {};
|
|
531
|
+
if (config.head_end) {
|
|
532
|
+
customHtml.head_end = await buildCustomHtmlSlotData(config.head_end, 'custom_html.head_end');
|
|
533
|
+
}
|
|
534
|
+
if (config.body_end) {
|
|
535
|
+
customHtml.body_end = await buildCustomHtmlSlotData(config.body_end, 'custom_html.body_end');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return customHtml;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function buildCustomHtmlSlotData(slotConfig, pathLabel) {
|
|
542
|
+
const sourcePath = resolveConfiguredSourceFile(slotConfig.file, '.html', `${pathLabel}.file`);
|
|
443
543
|
return {
|
|
444
544
|
content: await readRequiredSourceFile(sourcePath, `${pathLabel}.file`),
|
|
445
545
|
};
|
|
@@ -549,13 +649,13 @@ function isPathInside(parentPath, childPath) {
|
|
|
549
649
|
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
550
650
|
}
|
|
551
651
|
|
|
552
|
-
function
|
|
652
|
+
function defaultPermalinks() {
|
|
553
653
|
return {
|
|
554
|
-
output_style:
|
|
555
|
-
posts:
|
|
556
|
-
pages:
|
|
557
|
-
categories:
|
|
558
|
-
tags:
|
|
654
|
+
output_style: 'html-extension',
|
|
655
|
+
posts: '/posts/:slug/',
|
|
656
|
+
pages: '/:slug/',
|
|
657
|
+
categories: '/categories/:slug/',
|
|
658
|
+
tags: '/tags/:slug/',
|
|
559
659
|
};
|
|
560
660
|
}
|
|
561
661
|
|
|
@@ -629,8 +729,9 @@ function buildPrebuildReport({
|
|
|
629
729
|
generated_at: new Date().toISOString(),
|
|
630
730
|
source_dir: formatSourcePath(sourceDir),
|
|
631
731
|
config_path: formatSourcePath(configPath),
|
|
732
|
+
build_pages_config_path: formatSourcePath(buildPagesConfigPath),
|
|
632
733
|
preview_data_path: formatSourcePath(previewDataPath),
|
|
633
|
-
report_path: formatSourcePath(
|
|
734
|
+
report_path: formatSourcePath(buildReportPath),
|
|
634
735
|
skip_untitled_markdown: skipUntitledMarkdown,
|
|
635
736
|
markdown: {
|
|
636
737
|
discovered: sourceFiles.length,
|
|
@@ -651,7 +752,7 @@ function buildPrebuildReport({
|
|
|
651
752
|
|
|
652
753
|
function printPrebuildSummary(report) {
|
|
653
754
|
const lines = [
|
|
654
|
-
'ZeroPress
|
|
755
|
+
'ZeroPress build report',
|
|
655
756
|
`- Public root: ${report.source_dir}`,
|
|
656
757
|
`- Markdown discovered: ${report.markdown.discovered}`,
|
|
657
758
|
`- Markdown pages generated: ${report.markdown.generated_pages}`,
|
|
@@ -659,6 +760,7 @@ function printPrebuildSummary(report) {
|
|
|
659
760
|
`- Total preview pages: ${report.pages.total}`,
|
|
660
761
|
`- Front page: ${formatFrontPageSummary(report.front_page)}`,
|
|
661
762
|
`- Custom HTML slots: ${report.custom_html.length ? report.custom_html.join(', ') : 'none'}`,
|
|
763
|
+
`- Resolved config: ${report.build_pages_config_path}`,
|
|
662
764
|
`- Report: ${report.report_path}`,
|
|
663
765
|
];
|
|
664
766
|
|
|
@@ -680,7 +782,171 @@ function formatFrontPageSummary(frontPageReport) {
|
|
|
680
782
|
return `html ${config.file} -> / (${previewData.page_slug})`;
|
|
681
783
|
}
|
|
682
784
|
|
|
683
|
-
function
|
|
785
|
+
function parseMarkdownSource(rawMarkdown, sourcePath) {
|
|
786
|
+
try {
|
|
787
|
+
const parsed = matter(rawMarkdown);
|
|
788
|
+
if (!isPlainObject(parsed.data)) {
|
|
789
|
+
throw new PrebuildMarkdownError(
|
|
790
|
+
sourcePath,
|
|
791
|
+
'front matter must be a YAML object.',
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
bodyMarkdown: parsed.content,
|
|
797
|
+
frontMatter: parsed.data,
|
|
798
|
+
};
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error instanceof PrebuildMarkdownError) {
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
throw new PrebuildMarkdownError(
|
|
805
|
+
sourcePath,
|
|
806
|
+
`invalid YAML front matter: ${error instanceof Error ? error.message : String(error)}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function readFrontMatterStatus(value, sourcePath) {
|
|
812
|
+
if (value === undefined || value === 'published') {
|
|
813
|
+
return 'published';
|
|
814
|
+
}
|
|
815
|
+
if (value === 'draft') {
|
|
816
|
+
return {
|
|
817
|
+
reason: 'front matter status is "draft".',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
reason: `unsupported front matter status ${formatFrontMatterValue(value)}.`,
|
|
823
|
+
expected: 'Expected status: published or draft.',
|
|
824
|
+
warning: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function normalizePublishedFrontMatter(frontMatter, sourcePath) {
|
|
829
|
+
return {
|
|
830
|
+
title: normalizeFrontMatterTitle(frontMatter.title, sourcePath),
|
|
831
|
+
description: normalizeFrontMatterDescription(frontMatter.description, sourcePath),
|
|
832
|
+
path: normalizeFrontMatterRoutePath(frontMatter.path, sourcePath),
|
|
833
|
+
meta: normalizeFrontMatterMeta(frontMatter.meta, sourcePath),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeFrontMatterTitle(value, sourcePath) {
|
|
838
|
+
if (value === undefined) {
|
|
839
|
+
return '';
|
|
840
|
+
}
|
|
841
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
842
|
+
throw new PrebuildMarkdownError(
|
|
843
|
+
sourcePath,
|
|
844
|
+
'front matter title must be a non-empty string when provided.',
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return value.trim();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function normalizeFrontMatterDescription(value, sourcePath) {
|
|
852
|
+
if (value === undefined) {
|
|
853
|
+
return '';
|
|
854
|
+
}
|
|
855
|
+
if (typeof value !== 'string') {
|
|
856
|
+
throw new PrebuildMarkdownError(
|
|
857
|
+
sourcePath,
|
|
858
|
+
'front matter description must be a string when provided.',
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return value.trim();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function normalizeFrontMatterRoutePath(value, sourcePath) {
|
|
866
|
+
if (value === undefined) {
|
|
867
|
+
return '';
|
|
868
|
+
}
|
|
869
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
870
|
+
throw new PrebuildMarkdownError(
|
|
871
|
+
sourcePath,
|
|
872
|
+
'front matter path must be a non-empty string when provided.',
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const routePath = value.trim();
|
|
877
|
+
const segments = routePath.split('/');
|
|
878
|
+
if (
|
|
879
|
+
routePath.startsWith('/')
|
|
880
|
+
|| routePath.endsWith('/')
|
|
881
|
+
|| routePath.includes('\\')
|
|
882
|
+
|| routePath.includes('?')
|
|
883
|
+
|| routePath.includes('#')
|
|
884
|
+
|| segments.some((segment) => !isSafeRoutePathSegment(segment))
|
|
885
|
+
) {
|
|
886
|
+
throw new PrebuildMarkdownError(
|
|
887
|
+
sourcePath,
|
|
888
|
+
'front matter path must be a safe generated route path.',
|
|
889
|
+
' path: guides/install\n path: spec/preview-data-v0.5',
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return routePath;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function isSafeRoutePathSegment(segment) {
|
|
897
|
+
return (
|
|
898
|
+
/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(segment)
|
|
899
|
+
&& !segment.includes('..')
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function normalizeFrontMatterMeta(value, sourcePath) {
|
|
904
|
+
if (value === undefined) {
|
|
905
|
+
return {};
|
|
906
|
+
}
|
|
907
|
+
if (!isPlainObject(value)) {
|
|
908
|
+
throw new PrebuildMarkdownError(
|
|
909
|
+
sourcePath,
|
|
910
|
+
'front matter meta must be an object when provided.',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const meta = {};
|
|
915
|
+
for (const [key, metaValue] of Object.entries(value)) {
|
|
916
|
+
if (!isPreviewMetaValue(metaValue)) {
|
|
917
|
+
throw new PrebuildMarkdownError(
|
|
918
|
+
sourcePath,
|
|
919
|
+
`front matter meta.${key} must be a string, number, boolean, or null.`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
meta[key] = metaValue;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return meta;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function isPreviewMetaValue(value) {
|
|
929
|
+
return (
|
|
930
|
+
value === null
|
|
931
|
+
|| typeof value === 'string'
|
|
932
|
+
|| typeof value === 'number'
|
|
933
|
+
|| typeof value === 'boolean'
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function formatFrontMatterValue(value) {
|
|
938
|
+
if (typeof value === 'string') {
|
|
939
|
+
return `"${value}"`;
|
|
940
|
+
}
|
|
941
|
+
const serialized = JSON.stringify(value);
|
|
942
|
+
return serialized === undefined ? String(value) : serialized;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown, frontMatterTitle = '') {
|
|
946
|
+
if (frontMatterTitle) {
|
|
947
|
+
return frontMatterTitle;
|
|
948
|
+
}
|
|
949
|
+
|
|
684
950
|
try {
|
|
685
951
|
return extractTitle(markdown, sourcePath);
|
|
686
952
|
} catch (error) {
|
|
@@ -689,11 +955,8 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
|
|
|
689
955
|
&& error instanceof PrebuildMarkdownError
|
|
690
956
|
&& error.code === 'untitled_markdown'
|
|
691
957
|
) {
|
|
692
|
-
console.warn(
|
|
693
|
-
skippedMarkdown.
|
|
694
|
-
file: formatSourcePath(error.sourcePath),
|
|
695
|
-
reason: error.reason,
|
|
696
|
-
});
|
|
958
|
+
console.warn(formatSkippedMarkdownWarning(error.sourcePath, error.reason, '', 'Skipped untitled Markdown'));
|
|
959
|
+
recordSkippedMarkdown(skippedMarkdown, error.sourcePath, error.reason);
|
|
697
960
|
return '';
|
|
698
961
|
}
|
|
699
962
|
|
|
@@ -701,12 +964,23 @@ function extractTitleOrSkip(markdown, sourcePath, skippedMarkdown) {
|
|
|
701
964
|
}
|
|
702
965
|
}
|
|
703
966
|
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
967
|
+
function recordSkippedMarkdown(skippedMarkdown, sourcePath, reason) {
|
|
968
|
+
skippedMarkdown.push({
|
|
969
|
+
file: formatSourcePath(sourcePath),
|
|
970
|
+
reason,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function formatSkippedMarkdownWarning(sourcePath, reason, expected = '', label = 'Skipped Markdown') {
|
|
975
|
+
const lines = [
|
|
976
|
+
`[zeropress-build-pages] ${label}: ${formatSourcePath(sourcePath)}`,
|
|
977
|
+
`Reason: ${reason}`,
|
|
978
|
+
];
|
|
979
|
+
if (expected) {
|
|
980
|
+
lines.push(expected);
|
|
981
|
+
}
|
|
982
|
+
lines.push('This file was not added to preview-data pages.');
|
|
983
|
+
return lines.join('\n');
|
|
710
984
|
}
|
|
711
985
|
|
|
712
986
|
async function listMarkdownFiles(dir) {
|
|
@@ -780,6 +1054,17 @@ function buildHtmlPageRoute(sourcePath, options = {}) {
|
|
|
780
1054
|
}
|
|
781
1055
|
|
|
782
1056
|
function buildRoutePath(relativeSourcePath, sourcePath, options = {}) {
|
|
1057
|
+
if (options.routePath) {
|
|
1058
|
+
if (options.routePath === 'index' && !options.allowRootIndex) {
|
|
1059
|
+
throw new PrebuildMarkdownError(
|
|
1060
|
+
sourcePath,
|
|
1061
|
+
'front matter path "index" is reserved for the front page.',
|
|
1062
|
+
' path: docs/index\n path: guide',
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return options.routePath;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
783
1068
|
const extensionPattern = options.extensionPattern || /\.md$/i;
|
|
784
1069
|
const withoutExtension = relativeSourcePath.replace(extensionPattern, '').toLowerCase();
|
|
785
1070
|
const segments = withoutExtension
|