@zeropress/build-core 0.2.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -55,7 +55,6 @@ import {
|
|
|
55
55
|
- `sitemap.xml`
|
|
56
56
|
- `feed.xml`
|
|
57
57
|
- `robots.txt`
|
|
58
|
-
- `meta.json`
|
|
59
58
|
- writing outputs through a pluggable writer
|
|
60
59
|
|
|
61
60
|
It does not:
|
|
@@ -120,7 +119,7 @@ Notes:
|
|
|
120
119
|
- `previewData` must already satisfy the canonical preview-data contract
|
|
121
120
|
- `themePackage` must already be a validated in-memory theme package
|
|
122
121
|
- `sitemap.xml` and `feed.xml` are emitted only when `site.url` is a non-empty canonical URL
|
|
123
|
-
- `robots.txt`
|
|
122
|
+
- `robots.txt` is still emitted when `generateSpecialFiles` is enabled
|
|
124
123
|
|
|
125
124
|
### `buildSiteFromThemeDir(input)`
|
|
126
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared ZeroPress build core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@zeropress/slug-policy": "^0.1.0",
|
|
37
|
-
"@zeropress/preview-data-validator": "
|
|
38
|
-
"@zeropress/theme-validator": "
|
|
37
|
+
"@zeropress/preview-data-validator": "0.5.0",
|
|
38
|
+
"@zeropress/theme-validator": "0.5.1",
|
|
39
39
|
"highlight.js": "^11.11.1",
|
|
40
40
|
"markdown-it": "^14.1.0",
|
|
41
41
|
"markdown-it-anchor": "^9.2.0"
|
package/src/api/build-site.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertPreviewData } from '@zeropress/preview-data-validator';
|
|
|
3
3
|
import { isSafeSlugSegment, normalizeStoredSlug } from '@zeropress/slug-policy';
|
|
4
4
|
import { validateThemeFiles } from '@zeropress/theme-validator';
|
|
5
5
|
import { AssetProcessor } from '../assets/asset-processor.js';
|
|
6
|
-
import { renderDocumentContent } from '../render/content-renderer.js';
|
|
6
|
+
import { renderDocument, renderDocumentContent } from '../render/content-renderer.js';
|
|
7
7
|
import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
8
8
|
|
|
9
9
|
const DEFAULT_OPTIONS = {
|
|
@@ -17,6 +17,14 @@ const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
|
17
17
|
const DEFAULT_TIME_FORMAT = 'HH:mm';
|
|
18
18
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
19
19
|
const DEFAULT_LOCALE = 'en-US';
|
|
20
|
+
const DEFAULT_PERMALINKS = Object.freeze({
|
|
21
|
+
output_style: 'directory',
|
|
22
|
+
posts: '/posts/:slug/',
|
|
23
|
+
pages: '/:slug/',
|
|
24
|
+
categories: '/categories/:slug/',
|
|
25
|
+
tags: '/tags/:slug/',
|
|
26
|
+
});
|
|
27
|
+
const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
|
|
20
28
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
21
29
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
22
30
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
@@ -75,7 +83,6 @@ export async function buildSite(input) {
|
|
|
75
83
|
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
76
84
|
}
|
|
77
85
|
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
78
|
-
await writeOutput(state.writer, state.summaries, 'meta.json', buildMetaJson(state.previewData.site, state.emitted, state.generatedAt), 'application/json');
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -86,7 +93,7 @@ async function createBuildState(input, options) {
|
|
|
86
93
|
throw new Error('buildSite requires a writer with an async write(file) method');
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
assertBuildPreviewData(input.previewData);
|
|
90
97
|
const themePackage = await normalizeAndValidateThemePackage(input.themePackage);
|
|
91
98
|
|
|
92
99
|
const engine = new ZeroPressEngine();
|
|
@@ -117,6 +124,7 @@ async function createBuildState(input, options) {
|
|
|
117
124
|
assetOutputs,
|
|
118
125
|
assetMap,
|
|
119
126
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
127
|
+
customHtml: previewData.custom_html,
|
|
120
128
|
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
121
129
|
options,
|
|
122
130
|
generatedAt: new Date(),
|
|
@@ -131,6 +139,18 @@ async function createBuildState(input, options) {
|
|
|
131
139
|
};
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
function assertBuildPreviewData(previewData) {
|
|
143
|
+
try {
|
|
144
|
+
assertPreviewData(previewData);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
if (message.startsWith('Invalid preview-data')) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Invalid preview-data: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
async function finalizeBuildResult(writer, summaries, options) {
|
|
135
155
|
let manifest;
|
|
136
156
|
if (options.writeManifest) {
|
|
@@ -154,12 +174,13 @@ async function finalizeBuildResult(writer, summaries, options) {
|
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
async function renderRoute(state, templateName, route) {
|
|
157
|
-
const currentUrl =
|
|
177
|
+
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
158
178
|
let html = await state.engine.render(
|
|
159
179
|
templateName,
|
|
160
180
|
{
|
|
161
181
|
menus: state.previewData.menus,
|
|
162
182
|
widgets: state.widgets,
|
|
183
|
+
taxonomies: state.renderData.taxonomies,
|
|
163
184
|
...route,
|
|
164
185
|
meta: buildPageMeta(state.previewData.site, {
|
|
165
186
|
currentUrl,
|
|
@@ -171,18 +192,19 @@ async function renderRoute(state, templateName, route) {
|
|
|
171
192
|
createRenderContext(state.previewData.site, currentUrl),
|
|
172
193
|
);
|
|
173
194
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
174
|
-
html =
|
|
175
|
-
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path), html, 'text/html');
|
|
195
|
+
html = injectSiteCustomizations(html, state);
|
|
196
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
176
197
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
async function renderPost(state, post) {
|
|
180
|
-
const currentUrl =
|
|
201
|
+
const currentUrl = post.url;
|
|
181
202
|
let html = await state.engine.render(
|
|
182
203
|
'post',
|
|
183
204
|
{
|
|
184
205
|
menus: state.previewData.menus,
|
|
185
206
|
widgets: state.widgets,
|
|
207
|
+
taxonomies: state.renderData.taxonomies,
|
|
186
208
|
post,
|
|
187
209
|
meta: buildPageMeta(state.previewData.site, {
|
|
188
210
|
currentUrl,
|
|
@@ -197,8 +219,8 @@ async function renderPost(state, post) {
|
|
|
197
219
|
createRenderContext(state.previewData.site, currentUrl),
|
|
198
220
|
);
|
|
199
221
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
200
|
-
html =
|
|
201
|
-
await writeOutput(state.writer, state.summaries,
|
|
222
|
+
html = injectSiteCustomizations(html, state);
|
|
223
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
202
224
|
state.emitted.posts.push({
|
|
203
225
|
url: currentUrl,
|
|
204
226
|
title: post.title,
|
|
@@ -210,12 +232,13 @@ async function renderPost(state, post) {
|
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
async function renderPage(state, page) {
|
|
213
|
-
const currentUrl =
|
|
235
|
+
const currentUrl = page.url;
|
|
214
236
|
let html = await state.engine.render(
|
|
215
237
|
'page',
|
|
216
238
|
{
|
|
217
239
|
menus: state.previewData.menus,
|
|
218
240
|
widgets: state.widgets,
|
|
241
|
+
taxonomies: state.renderData.taxonomies,
|
|
219
242
|
page,
|
|
220
243
|
meta: buildPageMeta(state.previewData.site, {
|
|
221
244
|
currentUrl,
|
|
@@ -228,8 +251,8 @@ async function renderPage(state, page) {
|
|
|
228
251
|
createRenderContext(state.previewData.site, currentUrl),
|
|
229
252
|
);
|
|
230
253
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
231
|
-
html =
|
|
232
|
-
await writeOutput(state.writer, state.summaries,
|
|
254
|
+
html = injectSiteCustomizations(html, state);
|
|
255
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(page.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
233
256
|
state.emitted.pages.push({
|
|
234
257
|
url: currentUrl,
|
|
235
258
|
title: page.title,
|
|
@@ -248,6 +271,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
248
271
|
{
|
|
249
272
|
menus: state.previewData.menus,
|
|
250
273
|
widgets: state.widgets,
|
|
274
|
+
taxonomies: state.renderData.taxonomies,
|
|
251
275
|
meta: buildPageMeta(state.previewData.site, {
|
|
252
276
|
currentUrl: '/404.html',
|
|
253
277
|
title: state.previewData.site.title,
|
|
@@ -258,7 +282,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
258
282
|
createRenderContext(state.previewData.site, '/404.html'),
|
|
259
283
|
);
|
|
260
284
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
261
|
-
html =
|
|
285
|
+
html = injectSiteCustomizations(html, state);
|
|
262
286
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
263
287
|
}
|
|
264
288
|
|
|
@@ -274,6 +298,7 @@ function normalizePreviewData(previewData) {
|
|
|
274
298
|
timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
|
|
275
299
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
276
300
|
disallowComments: previewData.site.disallowComments === true,
|
|
301
|
+
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
277
302
|
};
|
|
278
303
|
|
|
279
304
|
return {
|
|
@@ -281,6 +306,7 @@ function normalizePreviewData(previewData) {
|
|
|
281
306
|
site: normalizedSite,
|
|
282
307
|
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.mediaBaseUrl),
|
|
283
308
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
309
|
+
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
284
310
|
content: {
|
|
285
311
|
...previewData.content,
|
|
286
312
|
authors: previewData.content.authors.map((author) => ({
|
|
@@ -348,6 +374,38 @@ function normalizeCustomCss(customCss) {
|
|
|
348
374
|
return content ? { content } : undefined;
|
|
349
375
|
}
|
|
350
376
|
|
|
377
|
+
function normalizeCustomHtml(customHtml) {
|
|
378
|
+
if (!customHtml || typeof customHtml !== 'object') {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const headEnd = normalizeOptionalRawString(customHtml.head_end?.content);
|
|
383
|
+
const bodyEnd = normalizeOptionalRawString(customHtml.body_end?.content);
|
|
384
|
+
if (!headEnd && !bodyEnd) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
...(headEnd ? { head_end: { content: headEnd } } : {}),
|
|
390
|
+
...(bodyEnd ? { body_end: { content: bodyEnd } } : {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function normalizePermalinks(permalinks) {
|
|
395
|
+
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
396
|
+
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
397
|
+
? source.output_style
|
|
398
|
+
: DEFAULT_PERMALINKS.output_style;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
output_style: outputStyle,
|
|
402
|
+
posts: normalizeNonEmptyString(source.posts, DEFAULT_PERMALINKS.posts),
|
|
403
|
+
pages: normalizeNonEmptyString(source.pages, DEFAULT_PERMALINKS.pages),
|
|
404
|
+
categories: normalizeNonEmptyString(source.categories, DEFAULT_PERMALINKS.categories),
|
|
405
|
+
tags: normalizeNonEmptyString(source.tags, DEFAULT_PERMALINKS.tags),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
351
409
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
352
410
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
353
411
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
@@ -376,17 +434,19 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
376
434
|
prev: index > 0 ? buildAdjacentPostSummary(preparedPosts[index - 1]) : null,
|
|
377
435
|
next: index < preparedPosts.length - 1 ? buildAdjacentPostSummary(preparedPosts[index + 1]) : null,
|
|
378
436
|
}));
|
|
379
|
-
const pages = previewData.content.pages.map((page) => preparePage(page));
|
|
437
|
+
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
380
438
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
381
439
|
|
|
382
440
|
return {
|
|
383
441
|
posts,
|
|
384
442
|
pages,
|
|
385
443
|
postBySlug,
|
|
444
|
+
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
386
445
|
indexRoutes: buildPaginatedCollection({
|
|
387
446
|
items: previewData.content.posts,
|
|
388
447
|
postsPerPage: previewData.site.postsPerPage,
|
|
389
448
|
basePath: '/',
|
|
449
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
390
450
|
}).map((entry) => ({
|
|
391
451
|
path: entry.path,
|
|
392
452
|
page: entry.page,
|
|
@@ -398,6 +458,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
398
458
|
items: previewData.content.posts,
|
|
399
459
|
postsPerPage: previewData.site.postsPerPage,
|
|
400
460
|
basePath: '/archive/',
|
|
461
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
401
462
|
}).map((entry) => ({
|
|
402
463
|
path: entry.path,
|
|
403
464
|
page: entry.page,
|
|
@@ -412,7 +473,8 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
412
473
|
postsBySlug: categoryPostsBySlug,
|
|
413
474
|
postBySlug,
|
|
414
475
|
postsPerPage: previewData.site.postsPerPage,
|
|
415
|
-
|
|
476
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
477
|
+
buildBasePath: (category) => resolvePermalink(previewData.site, 'categories', category).path,
|
|
416
478
|
renderExtras: (category) => ({
|
|
417
479
|
taxonomy: buildTaxonomyRouteData('category', category, categoryCountBySlug),
|
|
418
480
|
}),
|
|
@@ -422,7 +484,8 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
422
484
|
postsBySlug: tagPostsBySlug,
|
|
423
485
|
postBySlug,
|
|
424
486
|
postsPerPage: previewData.site.postsPerPage,
|
|
425
|
-
|
|
487
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
488
|
+
buildBasePath: (tag) => resolvePermalink(previewData.site, 'tags', tag).path,
|
|
426
489
|
renderExtras: (tag) => ({
|
|
427
490
|
taxonomy: buildTaxonomyRouteData('tag', tag, tagCountBySlug),
|
|
428
491
|
}),
|
|
@@ -430,6 +493,23 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
430
493
|
};
|
|
431
494
|
}
|
|
432
495
|
|
|
496
|
+
function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug) {
|
|
497
|
+
return {
|
|
498
|
+
categories: buildGlobalTaxonomyItems(previewData.site, 'categories', previewData.content.categories, categoryCountBySlug),
|
|
499
|
+
tags: buildGlobalTaxonomyItems(previewData.site, 'tags', previewData.content.tags, tagCountBySlug),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildGlobalTaxonomyItems(site, permalinkKind, items, countBySlug) {
|
|
504
|
+
return items.map((item) => ({
|
|
505
|
+
name: item.name,
|
|
506
|
+
slug: item.slug,
|
|
507
|
+
url: resolvePermalink(site, permalinkKind, item).url,
|
|
508
|
+
count: countBySlug.get(item.slug) || 0,
|
|
509
|
+
description: typeof item.description === 'string' ? item.description : '',
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
|
|
433
513
|
function resolveWidgetAreas(previewData, renderData) {
|
|
434
514
|
if (!previewData.widgets || typeof previewData.widgets !== 'object') {
|
|
435
515
|
return {};
|
|
@@ -521,7 +601,7 @@ function resolveCategoriesWidget(baseWidget, settings, previewData) {
|
|
|
521
601
|
.map((category) => ({
|
|
522
602
|
name: category.name,
|
|
523
603
|
slug: category.slug,
|
|
524
|
-
url:
|
|
604
|
+
url: resolvePermalink(previewData.site, 'categories', category).url,
|
|
525
605
|
count: countBySlug.get(category.slug) || 0,
|
|
526
606
|
depth: 0,
|
|
527
607
|
}))
|
|
@@ -546,7 +626,7 @@ function resolveTagsWidget(baseWidget, settings, previewData) {
|
|
|
546
626
|
.map((tag) => ({
|
|
547
627
|
name: tag.name,
|
|
548
628
|
slug: tag.slug,
|
|
549
|
-
url:
|
|
629
|
+
url: resolvePermalink(previewData.site, 'tags', tag).url,
|
|
550
630
|
count: countBySlug.get(tag.slug) || 0,
|
|
551
631
|
}))
|
|
552
632
|
.filter((tag) => tag.count > 0)
|
|
@@ -569,7 +649,7 @@ function resolveArchivesWidget(baseWidget, settings, previewData) {
|
|
|
569
649
|
.slice(0, limit)
|
|
570
650
|
.map((entry) => ({
|
|
571
651
|
label: entry.label,
|
|
572
|
-
url: '/archive/',
|
|
652
|
+
url: routePathToPublicUrl('/archive/', previewData.site.permalinks.output_style),
|
|
573
653
|
count: entry.count,
|
|
574
654
|
year: entry.year,
|
|
575
655
|
month: entry.month,
|
|
@@ -657,27 +737,32 @@ function resolveProfileWidget(baseWidget, settings) {
|
|
|
657
737
|
};
|
|
658
738
|
}
|
|
659
739
|
|
|
660
|
-
function preparePage(page) {
|
|
740
|
+
function preparePage(page, site) {
|
|
661
741
|
const documentType = normalizeDocumentType(page.document_type);
|
|
742
|
+
const renderedDocument = renderDocument(page.content, documentType);
|
|
743
|
+
const permalink = resolvePagePermalink(site, page);
|
|
662
744
|
|
|
663
745
|
return {
|
|
664
746
|
...page,
|
|
747
|
+
url: permalink.url,
|
|
665
748
|
document_type: documentType,
|
|
666
|
-
html:
|
|
749
|
+
html: renderedDocument.html,
|
|
750
|
+
toc: renderedDocument.toc,
|
|
667
751
|
};
|
|
668
752
|
}
|
|
669
753
|
|
|
670
754
|
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments) {
|
|
671
755
|
const documentType = normalizeDocumentType(post.document_type);
|
|
672
|
-
const
|
|
756
|
+
const renderedDocument = renderDocument(post.content, documentType);
|
|
673
757
|
const author = authorsById.get(post.author_id);
|
|
758
|
+
const permalink = resolvePermalink(site, 'posts', post);
|
|
674
759
|
const categories = post.category_slugs
|
|
675
760
|
.map((slug) => categoriesBySlug.get(slug))
|
|
676
761
|
.filter(Boolean)
|
|
677
762
|
.map((category) => ({
|
|
678
763
|
name: category.name,
|
|
679
764
|
slug: category.slug,
|
|
680
|
-
url:
|
|
765
|
+
url: resolvePermalink(site, 'categories', category).url,
|
|
681
766
|
}));
|
|
682
767
|
const tags = post.tag_slugs
|
|
683
768
|
.map((slug) => tagsBySlug.get(slug))
|
|
@@ -685,14 +770,14 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
685
770
|
.map((tag) => ({
|
|
686
771
|
name: tag.name,
|
|
687
772
|
slug: tag.slug,
|
|
688
|
-
url:
|
|
773
|
+
url: resolvePermalink(site, 'tags', tag).url,
|
|
689
774
|
}));
|
|
690
775
|
|
|
691
776
|
return {
|
|
692
777
|
public_id: post.public_id,
|
|
693
778
|
title: post.title,
|
|
694
779
|
slug: post.slug,
|
|
695
|
-
url:
|
|
780
|
+
url: permalink.url,
|
|
696
781
|
content: post.content,
|
|
697
782
|
document_type: documentType,
|
|
698
783
|
excerpt: post.excerpt,
|
|
@@ -700,6 +785,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
700
785
|
updated_at_iso: post.updated_at_iso,
|
|
701
786
|
author_id: post.author_id,
|
|
702
787
|
featured_image: post.featured_image,
|
|
788
|
+
meta: post.meta,
|
|
703
789
|
status: post.status,
|
|
704
790
|
allow_comments: post.allow_comments,
|
|
705
791
|
category_slugs: post.category_slugs,
|
|
@@ -711,10 +797,11 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
711
797
|
},
|
|
712
798
|
categories,
|
|
713
799
|
tags,
|
|
714
|
-
html,
|
|
800
|
+
html: renderedDocument.html,
|
|
801
|
+
toc: renderedDocument.toc,
|
|
715
802
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
716
803
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
717
|
-
reading_time: calculateReadingTime(html),
|
|
804
|
+
reading_time: calculateReadingTime(renderedDocument.html),
|
|
718
805
|
comments_enabled: themeSupportsComments && site.disallowComments !== true && post.allow_comments === true,
|
|
719
806
|
};
|
|
720
807
|
}
|
|
@@ -744,6 +831,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
744
831
|
items: matchedPosts,
|
|
745
832
|
postsPerPage: options.postsPerPage,
|
|
746
833
|
basePath: options.buildBasePath(item),
|
|
834
|
+
outputStyle: options.outputStyle,
|
|
747
835
|
});
|
|
748
836
|
|
|
749
837
|
for (const entry of paginated) {
|
|
@@ -764,6 +852,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
764
852
|
|
|
765
853
|
function buildPaginatedCollection(options) {
|
|
766
854
|
const basePath = normalizePaginationBasePath(options.basePath);
|
|
855
|
+
const outputStyle = PERMALINK_OUTPUT_STYLES.has(options.outputStyle) ? options.outputStyle : DEFAULT_PERMALINKS.output_style;
|
|
767
856
|
const postsPerPage = Number.isInteger(options.postsPerPage) && options.postsPerPage > 0 ? options.postsPerPage : DEFAULT_POSTS_PER_PAGE;
|
|
768
857
|
const totalPosts = options.items.length;
|
|
769
858
|
const totalPages = Math.max(1, Math.ceil(totalPosts / postsPerPage));
|
|
@@ -778,7 +867,7 @@ function buildPaginatedCollection(options) {
|
|
|
778
867
|
page,
|
|
779
868
|
totalPages,
|
|
780
869
|
items: options.items.slice(start, end),
|
|
781
|
-
paginationData: buildPaginationData(page, totalPages, totalPosts, basePath),
|
|
870
|
+
paginationData: buildPaginationData(page, totalPages, totalPosts, basePath, outputStyle),
|
|
782
871
|
});
|
|
783
872
|
}
|
|
784
873
|
|
|
@@ -811,20 +900,22 @@ function normalizePaginationBasePath(basePath) {
|
|
|
811
900
|
return `/${normalized}/`;
|
|
812
901
|
}
|
|
813
902
|
|
|
814
|
-
function buildPaginationData(currentPage, totalPages, totalPosts, basePath) {
|
|
903
|
+
function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
904
|
+
const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
|
|
905
|
+
|
|
815
906
|
return {
|
|
816
907
|
currentPage,
|
|
817
908
|
totalPages,
|
|
818
909
|
totalPosts,
|
|
819
910
|
hasNext: currentPage < totalPages,
|
|
820
911
|
hasPrev: currentPage > 1,
|
|
821
|
-
nextUrl: currentPage < totalPages ?
|
|
822
|
-
prevUrl: currentPage > 1 ?
|
|
912
|
+
nextUrl: currentPage < totalPages ? buildPageUrl(currentPage + 1) : undefined,
|
|
913
|
+
prevUrl: currentPage > 1 ? buildPageUrl(currentPage - 1) : undefined,
|
|
823
914
|
pages: Array.from({ length: totalPages }, (_, index) => {
|
|
824
915
|
const page = index + 1;
|
|
825
916
|
return {
|
|
826
917
|
number: page,
|
|
827
|
-
url:
|
|
918
|
+
url: buildPageUrl(page),
|
|
828
919
|
current: page === currentPage,
|
|
829
920
|
};
|
|
830
921
|
}),
|
|
@@ -1105,6 +1196,10 @@ function normalizeOptionalString(value) {
|
|
|
1105
1196
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
1106
1197
|
}
|
|
1107
1198
|
|
|
1199
|
+
function normalizeOptionalRawString(value) {
|
|
1200
|
+
return typeof value === 'string' && value.trim() ? value : '';
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1108
1203
|
function normalizeLocale(value) {
|
|
1109
1204
|
if (typeof value !== 'string' || !value.trim()) {
|
|
1110
1205
|
return DEFAULT_LOCALE;
|
|
@@ -1433,14 +1528,28 @@ async function writeOutput(writer, summaries, path, content, contentType) {
|
|
|
1433
1528
|
});
|
|
1434
1529
|
}
|
|
1435
1530
|
|
|
1436
|
-
function routePathToOutputPath(routePath) {
|
|
1531
|
+
function routePathToOutputPath(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
1437
1532
|
const normalizedPath = normalizeRoutePath(routePath);
|
|
1438
1533
|
if (normalizedPath === '/') {
|
|
1439
1534
|
return 'index.html';
|
|
1440
1535
|
}
|
|
1536
|
+
if (outputStyle === 'html-extension') {
|
|
1537
|
+
return `${normalizedPath.replace(/^\/+|\/+$/g, '')}.html`;
|
|
1538
|
+
}
|
|
1441
1539
|
return `${normalizedPath.replace(/^\//, '')}index.html`;
|
|
1442
1540
|
}
|
|
1443
1541
|
|
|
1542
|
+
function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
1543
|
+
const normalizedPath = normalizeRoutePath(routePath);
|
|
1544
|
+
if (normalizedPath === '/') {
|
|
1545
|
+
return '/';
|
|
1546
|
+
}
|
|
1547
|
+
if (outputStyle === 'html-extension') {
|
|
1548
|
+
return normalizedPath.replace(/\/$/, '');
|
|
1549
|
+
}
|
|
1550
|
+
return normalizedPath;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1444
1553
|
function normalizeRoutePath(routePath) {
|
|
1445
1554
|
if (!routePath || routePath === '/') {
|
|
1446
1555
|
return '/';
|
|
@@ -1449,40 +1558,130 @@ function normalizeRoutePath(routePath) {
|
|
|
1449
1558
|
return `/${normalized}/`;
|
|
1450
1559
|
}
|
|
1451
1560
|
|
|
1561
|
+
function resolvePagePermalink(site, page) {
|
|
1562
|
+
if (normalizeOptionalString(page.path)) {
|
|
1563
|
+
return buildRouteInfo(page.path, site.permalinks.output_style);
|
|
1564
|
+
}
|
|
1565
|
+
return resolvePermalink(site, 'pages', page);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function resolvePermalink(site, kind, item) {
|
|
1569
|
+
const pattern = normalizeNonEmptyString(site.permalinks?.[kind], DEFAULT_PERMALINKS[kind]);
|
|
1570
|
+
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function buildRouteInfo(routePath, outputStyle) {
|
|
1574
|
+
const path = normalizeRoutePath(routePath);
|
|
1575
|
+
return {
|
|
1576
|
+
path,
|
|
1577
|
+
url: routePathToPublicUrl(path, outputStyle),
|
|
1578
|
+
outputPath: routePathToOutputPath(path, outputStyle),
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function applyPermalinkPattern(pattern, kind, item, site) {
|
|
1583
|
+
const tokenValues = buildPermalinkTokenValues(kind, item, site);
|
|
1584
|
+
const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
|
|
1585
|
+
const segments = body.split('/').filter(Boolean).map((segment) => {
|
|
1586
|
+
if (segment.startsWith(':')) {
|
|
1587
|
+
return tokenValues[segment.slice(1)] || '';
|
|
1588
|
+
}
|
|
1589
|
+
return segment;
|
|
1590
|
+
});
|
|
1591
|
+
return `/${segments.join('/')}/`;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function buildPermalinkTokenValues(kind, item, site) {
|
|
1595
|
+
const values = {
|
|
1596
|
+
slug: encodeSlugSegment(item.slug),
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
if (kind === 'posts') {
|
|
1600
|
+
const parts = getPermalinkDateParts(item.published_at_iso, site);
|
|
1601
|
+
values.public_id = String(item.public_id);
|
|
1602
|
+
values.year = parts.year;
|
|
1603
|
+
values.month = parts.month;
|
|
1604
|
+
values.day = parts.day;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return values;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function getPermalinkDateParts(value, site) {
|
|
1611
|
+
const date = toDate(value);
|
|
1612
|
+
const timeZone = normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE);
|
|
1613
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
1614
|
+
timeZone,
|
|
1615
|
+
year: 'numeric',
|
|
1616
|
+
month: '2-digit',
|
|
1617
|
+
day: '2-digit',
|
|
1618
|
+
}).formatToParts(date);
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
year: parts.find((part) => part.type === 'year')?.value || String(date.getUTCFullYear()),
|
|
1622
|
+
month: parts.find((part) => part.type === 'month')?.value || String(date.getUTCMonth() + 1).padStart(2, '0'),
|
|
1623
|
+
day: parts.find((part) => part.type === 'day')?.value || String(date.getUTCDate()).padStart(2, '0'),
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1452
1627
|
function normalizeOutputPath(filePath) {
|
|
1453
1628
|
return String(filePath || '').replace(/^\/+/, '');
|
|
1454
1629
|
}
|
|
1455
1630
|
|
|
1456
1631
|
function assertPlannedOutputPathsSafe(state) {
|
|
1632
|
+
const outputStyle = state.previewData.site.permalinks.output_style;
|
|
1457
1633
|
for (const post of state.renderData.posts) {
|
|
1458
|
-
assertSafeSlugDerivedOutputPath(post.slug,
|
|
1634
|
+
assertSafeSlugDerivedOutputPath(post.slug, routePathToOutputPath(post.url, outputStyle));
|
|
1459
1635
|
}
|
|
1460
1636
|
|
|
1461
1637
|
for (const page of state.renderData.pages) {
|
|
1462
|
-
assertSafeSlugDerivedOutputPath(page.slug,
|
|
1638
|
+
assertSafeSlugDerivedOutputPath(page.slug, routePathToOutputPath(page.url, outputStyle));
|
|
1463
1639
|
}
|
|
1464
1640
|
|
|
1465
1641
|
for (const category of state.previewData.content.categories) {
|
|
1466
|
-
assertSafeSlugDerivedOutputPath(category.slug,
|
|
1642
|
+
assertSafeSlugDerivedOutputPath(category.slug, resolvePermalink(state.previewData.site, 'categories', category).outputPath);
|
|
1467
1643
|
}
|
|
1468
1644
|
|
|
1469
1645
|
for (const tag of state.previewData.content.tags) {
|
|
1470
|
-
assertSafeSlugDerivedOutputPath(tag.slug,
|
|
1646
|
+
assertSafeSlugDerivedOutputPath(tag.slug, resolvePermalink(state.previewData.site, 'tags', tag).outputPath);
|
|
1471
1647
|
}
|
|
1472
1648
|
|
|
1649
|
+
const routeEntries = [
|
|
1650
|
+
...state.renderData.indexRoutes.map((route) => ({
|
|
1651
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1652
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1653
|
+
})),
|
|
1654
|
+
...state.renderData.archiveRoutes.map((route) => ({
|
|
1655
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1656
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1657
|
+
})),
|
|
1658
|
+
...state.renderData.categoryRoutes.map((route) => ({
|
|
1659
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1660
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1661
|
+
})),
|
|
1662
|
+
...state.renderData.tagRoutes.map((route) => ({
|
|
1663
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1664
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1665
|
+
})),
|
|
1666
|
+
...state.renderData.posts.map((post) => ({
|
|
1667
|
+
url: post.url,
|
|
1668
|
+
outputPath: routePathToOutputPath(post.url, outputStyle),
|
|
1669
|
+
})),
|
|
1670
|
+
...state.renderData.pages.map((page) => ({
|
|
1671
|
+
url: page.url,
|
|
1672
|
+
outputPath: routePathToOutputPath(page.url, outputStyle),
|
|
1673
|
+
})),
|
|
1674
|
+
];
|
|
1675
|
+
assertUniqueRoutes(routeEntries);
|
|
1676
|
+
|
|
1473
1677
|
const plannedPaths = [
|
|
1474
|
-
...
|
|
1475
|
-
...state.renderData.archiveRoutes.map((route) => routePathToOutputPath(route.path)),
|
|
1476
|
-
...state.renderData.categoryRoutes.map((route) => routePathToOutputPath(route.path)),
|
|
1477
|
-
...state.renderData.tagRoutes.map((route) => routePathToOutputPath(route.path)),
|
|
1478
|
-
...state.renderData.posts.map((post) => `posts/${encodeSlugSegment(post.slug)}/index.html`),
|
|
1479
|
-
...state.renderData.pages.map((page) => `${encodeSlugSegment(page.slug)}/index.html`),
|
|
1678
|
+
...routeEntries.map((entry) => entry.outputPath),
|
|
1480
1679
|
...state.assetOutputs.map((assetOutput) => assetOutput.path),
|
|
1481
1680
|
COMMENT_POLICY_OUTPUT_PATH,
|
|
1482
1681
|
];
|
|
1483
1682
|
|
|
1484
1683
|
if (state.options.generateSpecialFiles) {
|
|
1485
|
-
plannedPaths.push('404.html', 'robots.txt'
|
|
1684
|
+
plannedPaths.push('404.html', 'robots.txt');
|
|
1486
1685
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
1487
1686
|
plannedPaths.push('sitemap.xml', 'feed.xml');
|
|
1488
1687
|
}
|
|
@@ -1493,6 +1692,34 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1493
1692
|
const normalizedPath = normalizeOutputPath(rawPath);
|
|
1494
1693
|
assertSafeRelativeOutputPath(rawPath, normalizedPath);
|
|
1495
1694
|
}
|
|
1695
|
+
|
|
1696
|
+
assertUniqueOutputPaths(plannedPaths);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function assertUniqueRoutes(routeEntries) {
|
|
1700
|
+
const seenUrls = new Map();
|
|
1701
|
+
for (const entry of routeEntries) {
|
|
1702
|
+
const normalizedUrl = normalizeRouteCollisionKey(entry.url);
|
|
1703
|
+
if (seenUrls.has(normalizedUrl)) {
|
|
1704
|
+
throw new Error(`Duplicate public URL detected: ${entry.url}`);
|
|
1705
|
+
}
|
|
1706
|
+
seenUrls.set(normalizedUrl, entry);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function assertUniqueOutputPaths(plannedPaths) {
|
|
1711
|
+
const seenPaths = new Set();
|
|
1712
|
+
for (const plannedPath of plannedPaths) {
|
|
1713
|
+
const normalizedPath = normalizeOutputPath(plannedPath);
|
|
1714
|
+
if (seenPaths.has(normalizedPath)) {
|
|
1715
|
+
throw new Error(`Duplicate output path detected: ${plannedPath}`);
|
|
1716
|
+
}
|
|
1717
|
+
seenPaths.add(normalizedPath);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function normalizeRouteCollisionKey(url) {
|
|
1722
|
+
return String(url || '').replace(/\/+$/, '') || '/';
|
|
1496
1723
|
}
|
|
1497
1724
|
|
|
1498
1725
|
function assertSafeSlugDerivedOutputPath(rawSlug, outputPath) {
|
|
@@ -1553,6 +1780,12 @@ function sha256(content) {
|
|
|
1553
1780
|
return hash.digest('hex');
|
|
1554
1781
|
}
|
|
1555
1782
|
|
|
1783
|
+
function injectSiteCustomizations(html, state) {
|
|
1784
|
+
let next = injectCustomCssAssetLink(html, state.customCssHref);
|
|
1785
|
+
next = injectCustomHtml(next, state.customHtml);
|
|
1786
|
+
return next;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1556
1789
|
function injectCustomCssAssetLink(html, href) {
|
|
1557
1790
|
if (!normalizeOptionalString(href)) {
|
|
1558
1791
|
return html;
|
|
@@ -1561,6 +1794,21 @@ function injectCustomCssAssetLink(html, href) {
|
|
|
1561
1794
|
return html.replace('</head>', ` <link rel="stylesheet" href="${escapeHtml(href)}">\n</head>`);
|
|
1562
1795
|
}
|
|
1563
1796
|
|
|
1797
|
+
function injectCustomHtml(html, customHtml) {
|
|
1798
|
+
let next = html;
|
|
1799
|
+
const headEnd = normalizeOptionalRawString(customHtml?.head_end?.content);
|
|
1800
|
+
const bodyEnd = normalizeOptionalRawString(customHtml?.body_end?.content);
|
|
1801
|
+
|
|
1802
|
+
if (headEnd) {
|
|
1803
|
+
next = next.replace('</head>', `${headEnd}\n</head>`);
|
|
1804
|
+
}
|
|
1805
|
+
if (bodyEnd) {
|
|
1806
|
+
next = next.replace('</body>', `${bodyEnd}\n</body>`);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return next;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1564
1812
|
function buildSitemapXml(site, emitted, generatedAt) {
|
|
1565
1813
|
const entries = [
|
|
1566
1814
|
...emitted.indexRoutes
|
|
@@ -1617,38 +1865,6 @@ function buildRobotsTxt(site) {
|
|
|
1617
1865
|
return `${lines.join('\n')}\n`;
|
|
1618
1866
|
}
|
|
1619
1867
|
|
|
1620
|
-
function buildMetaJson(site, emitted, generatedAt) {
|
|
1621
|
-
const pages = [
|
|
1622
|
-
...emitted.posts.map((post) => ({
|
|
1623
|
-
url: post.url,
|
|
1624
|
-
title: post.title,
|
|
1625
|
-
description: post.description,
|
|
1626
|
-
publishedAt: post.publishedAt,
|
|
1627
|
-
updatedAt: post.updatedAt,
|
|
1628
|
-
type: 'post',
|
|
1629
|
-
metadata: { status: post.status },
|
|
1630
|
-
})),
|
|
1631
|
-
...emitted.pages.map((page) => ({
|
|
1632
|
-
url: page.url,
|
|
1633
|
-
title: page.title,
|
|
1634
|
-
description: page.description,
|
|
1635
|
-
type: 'page',
|
|
1636
|
-
metadata: { status: page.status },
|
|
1637
|
-
})),
|
|
1638
|
-
];
|
|
1639
|
-
|
|
1640
|
-
return JSON.stringify({
|
|
1641
|
-
generated: formatUtcIsoSeconds(generatedAt),
|
|
1642
|
-
site: {
|
|
1643
|
-
title: site.title,
|
|
1644
|
-
description: site.description,
|
|
1645
|
-
url: site.url,
|
|
1646
|
-
locale: site.locale,
|
|
1647
|
-
},
|
|
1648
|
-
pages,
|
|
1649
|
-
}, null, 2);
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
1868
|
function getContentType(assetPath) {
|
|
1653
1869
|
const ext = assetPath.split('.').pop()?.toLowerCase();
|
|
1654
1870
|
const contentTypes = {
|
|
@@ -2,46 +2,84 @@ import MarkdownIt from 'markdown-it';
|
|
|
2
2
|
import hljs from 'highlight.js';
|
|
3
3
|
import anchor from 'markdown-it-anchor';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
html: true,
|
|
7
|
-
linkify: true,
|
|
8
|
-
typographer: true,
|
|
9
|
-
breaks: true,
|
|
10
|
-
highlight(value, language) {
|
|
11
|
-
if (language && hljs.getLanguage(language)) {
|
|
12
|
-
try {
|
|
13
|
-
return hljs.highlight(value, { language }).value;
|
|
14
|
-
} catch {
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
return hljs.highlightAuto(value).value;
|
|
21
|
-
} catch {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
markdown.use(anchor, {
|
|
28
|
-
permalink: anchor.permalink.headerLink(),
|
|
29
|
-
slugify: slugify,
|
|
30
|
-
});
|
|
5
|
+
const TOC_LEVELS = new Set([2, 3, 4]);
|
|
31
6
|
|
|
32
7
|
export function renderDocumentContent(content, documentType = 'markdown') {
|
|
8
|
+
return renderDocument(content, documentType).html;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderDocument(content, documentType = 'markdown') {
|
|
33
12
|
const normalizedContent = typeof content === 'string' ? content : '';
|
|
34
13
|
const normalizedType = normalizeDocumentType(documentType);
|
|
35
14
|
|
|
36
15
|
if (normalizedType === 'plaintext') {
|
|
37
|
-
return
|
|
16
|
+
return {
|
|
17
|
+
html: transformPlaintext(normalizedContent),
|
|
18
|
+
toc: [],
|
|
19
|
+
};
|
|
38
20
|
}
|
|
39
21
|
|
|
40
22
|
if (normalizedType === 'html') {
|
|
41
|
-
return
|
|
23
|
+
return {
|
|
24
|
+
html: sanitizeHtml(normalizedContent),
|
|
25
|
+
toc: [],
|
|
26
|
+
};
|
|
42
27
|
}
|
|
43
28
|
|
|
44
|
-
return
|
|
29
|
+
return renderMarkdownDocument(normalizedContent);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderMarkdownDocument(content) {
|
|
33
|
+
const toc = [];
|
|
34
|
+
const markdown = createMarkdownRenderer(toc);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
html: sanitizeHtml(markdown.render(content)),
|
|
38
|
+
toc,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createMarkdownRenderer(toc) {
|
|
43
|
+
const markdown = new MarkdownIt({
|
|
44
|
+
html: true,
|
|
45
|
+
linkify: true,
|
|
46
|
+
typographer: true,
|
|
47
|
+
breaks: true,
|
|
48
|
+
highlight(value, language) {
|
|
49
|
+
if (language && hljs.getLanguage(language)) {
|
|
50
|
+
try {
|
|
51
|
+
return hljs.highlight(value, { language }).value;
|
|
52
|
+
} catch {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return hljs.highlightAuto(value).value;
|
|
59
|
+
} catch {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
markdown.use(anchor, {
|
|
66
|
+
slugify: slugify,
|
|
67
|
+
callback(token, { slug, title }) {
|
|
68
|
+
const level = Number(token.tag.slice(1));
|
|
69
|
+
if (!TOC_LEVELS.has(level)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toc.push({
|
|
74
|
+
level,
|
|
75
|
+
id: slug,
|
|
76
|
+
title,
|
|
77
|
+
href: `#${slug}`,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return markdown;
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
function normalizeDocumentType(value) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { parsePartialToken } from './partial-resolver.js';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const PATH_SEGMENT_SOURCE = '[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*';
|
|
4
|
+
const PATH_REGEX = new RegExp(`^${PATH_SEGMENT_SOURCE}(?:\\.${PATH_SEGMENT_SOURCE})*$`);
|
|
5
|
+
const FOR_EXPRESSION_REGEX = new RegExp(`^([a-zA-Z_][a-zA-Z0-9_]*)\\s+in\\s+(${PATH_SEGMENT_SOURCE}(?:\\.${PATH_SEGMENT_SOURCE})*)$`);
|
|
6
|
+
const IF_EQ_EXPRESSION_REGEX = new RegExp(`^(${PATH_SEGMENT_SOURCE}(?:\\.${PATH_SEGMENT_SOURCE})*)\\s+("(?:[^"\\\\]|\\\\.)*")$`);
|
|
6
7
|
|
|
7
8
|
export class ControlFlowRenderer {
|
|
8
9
|
constructor(options = {}) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export class VariableResolver {
|
|
2
2
|
static MENU_PATTERN = /\{\{menu:([a-z][a-z0-9_-]{0,63})\}\}/g;
|
|
3
|
-
static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\}\}/g;
|
|
4
|
-
static STANDALONE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
3
|
+
static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*(?:\.[a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*)+)\}\}/g;
|
|
4
|
+
static STANDALONE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:-[a-zA-Z0-9_]+)*)\}\}/g;
|
|
5
5
|
|
|
6
6
|
resolve(template, data, options = {}) {
|
|
7
7
|
let result = template.replace(VariableResolver.MENU_PATTERN, (_, menuId) => {
|