@zeropress/build-core 0.2.2 → 0.5.1
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.1",
|
|
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.1",
|
|
38
|
+
"@zeropress/theme-validator": "0.5.2",
|
|
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
|
@@ -24,6 +24,14 @@ const DEFAULT_PERMALINKS = Object.freeze({
|
|
|
24
24
|
categories: '/categories/:slug/',
|
|
25
25
|
tags: '/tags/:slug/',
|
|
26
26
|
});
|
|
27
|
+
const DEFAULT_FRONT_PAGE = Object.freeze({
|
|
28
|
+
type: 'theme_index',
|
|
29
|
+
});
|
|
30
|
+
const DEFAULT_POST_INDEX = Object.freeze({
|
|
31
|
+
enabled: true,
|
|
32
|
+
path: '/',
|
|
33
|
+
paginate: true,
|
|
34
|
+
});
|
|
27
35
|
const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
|
|
28
36
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
29
37
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
@@ -34,6 +42,10 @@ export async function buildSite(input) {
|
|
|
34
42
|
const state = await createBuildState(input, options);
|
|
35
43
|
assertPlannedOutputPathsSafe(state);
|
|
36
44
|
|
|
45
|
+
if (state.renderData.frontPageRoute) {
|
|
46
|
+
await renderFrontPage(state, state.renderData.frontPageRoute);
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
for (const route of state.renderData.indexRoutes) {
|
|
38
50
|
await renderRoute(state, 'index', route);
|
|
39
51
|
}
|
|
@@ -83,7 +95,6 @@ export async function buildSite(input) {
|
|
|
83
95
|
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
84
96
|
}
|
|
85
97
|
await writeOutput(state.writer, state.summaries, 'robots.txt', buildRobotsTxt(state.previewData.site), 'text/plain');
|
|
86
|
-
await writeOutput(state.writer, state.summaries, 'meta.json', buildMetaJson(state.previewData.site, state.emitted, state.generatedAt), 'application/json');
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
return finalizeBuildResult(state.writer, state.summaries, options);
|
|
@@ -94,7 +105,7 @@ async function createBuildState(input, options) {
|
|
|
94
105
|
throw new Error('buildSite requires a writer with an async write(file) method');
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
assertBuildPreviewData(input.previewData);
|
|
98
109
|
const themePackage = await normalizeAndValidateThemePackage(input.themePackage);
|
|
99
110
|
|
|
100
111
|
const engine = new ZeroPressEngine();
|
|
@@ -125,10 +136,12 @@ async function createBuildState(input, options) {
|
|
|
125
136
|
assetOutputs,
|
|
126
137
|
assetMap,
|
|
127
138
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
139
|
+
customHtml: previewData.custom_html,
|
|
128
140
|
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
129
141
|
options,
|
|
130
142
|
generatedAt: new Date(),
|
|
131
143
|
emitted: {
|
|
144
|
+
frontPage: null,
|
|
132
145
|
indexRoutes: [],
|
|
133
146
|
archiveRoutes: [],
|
|
134
147
|
categoryRoutes: [],
|
|
@@ -139,6 +152,18 @@ async function createBuildState(input, options) {
|
|
|
139
152
|
};
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
function assertBuildPreviewData(previewData) {
|
|
156
|
+
try {
|
|
157
|
+
assertPreviewData(previewData);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
if (message.startsWith('Invalid preview-data')) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Invalid preview-data: ${message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
async function finalizeBuildResult(writer, summaries, options) {
|
|
143
168
|
let manifest;
|
|
144
169
|
if (options.writeManifest) {
|
|
@@ -163,12 +188,18 @@ async function finalizeBuildResult(writer, summaries, options) {
|
|
|
163
188
|
|
|
164
189
|
async function renderRoute(state, templateName, route) {
|
|
165
190
|
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
191
|
+
const routeContext = buildRouteContext(route.route_type || templateName, currentUrl, {
|
|
192
|
+
isFrontPage: route.is_front_page === true,
|
|
193
|
+
isPostIndex: route.is_post_index === true,
|
|
194
|
+
});
|
|
166
195
|
let html = await state.engine.render(
|
|
167
196
|
templateName,
|
|
168
197
|
{
|
|
169
198
|
menus: state.previewData.menus,
|
|
170
199
|
widgets: state.widgets,
|
|
200
|
+
taxonomies: state.renderData.taxonomies,
|
|
171
201
|
...route,
|
|
202
|
+
route: routeContext,
|
|
172
203
|
meta: buildPageMeta(state.previewData.site, {
|
|
173
204
|
currentUrl,
|
|
174
205
|
title: state.previewData.site.title,
|
|
@@ -179,11 +210,92 @@ async function renderRoute(state, templateName, route) {
|
|
|
179
210
|
createRenderContext(state.previewData.site, currentUrl),
|
|
180
211
|
);
|
|
181
212
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
182
|
-
html =
|
|
213
|
+
html = injectSiteCustomizations(html, state);
|
|
183
214
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
184
215
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
185
216
|
}
|
|
186
217
|
|
|
218
|
+
async function renderFrontPage(state, route) {
|
|
219
|
+
const currentUrl = '/';
|
|
220
|
+
const routeContext = buildRouteContext('front_page', currentUrl, {
|
|
221
|
+
isFrontPage: true,
|
|
222
|
+
isPostIndex: false,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (route.front_page_type === 'standalone_html') {
|
|
226
|
+
await writeOutput(state.writer, state.summaries, 'index.html', route.html, 'text/html');
|
|
227
|
+
state.emitted.frontPage = {
|
|
228
|
+
url: currentUrl,
|
|
229
|
+
title: state.previewData.site.title,
|
|
230
|
+
description: state.previewData.site.description,
|
|
231
|
+
includeInFeed: false,
|
|
232
|
+
};
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (route.front_page_type === 'page') {
|
|
237
|
+
const page = {
|
|
238
|
+
...route.page,
|
|
239
|
+
url: currentUrl,
|
|
240
|
+
};
|
|
241
|
+
let html = await state.engine.render(
|
|
242
|
+
'page',
|
|
243
|
+
{
|
|
244
|
+
menus: state.previewData.menus,
|
|
245
|
+
widgets: state.widgets,
|
|
246
|
+
taxonomies: state.renderData.taxonomies,
|
|
247
|
+
page,
|
|
248
|
+
route: routeContext,
|
|
249
|
+
meta: buildPageMeta(state.previewData.site, {
|
|
250
|
+
currentUrl,
|
|
251
|
+
title: buildDocumentTitle(page.title, state.previewData.site.title),
|
|
252
|
+
description: page.excerpt,
|
|
253
|
+
ogType: 'website',
|
|
254
|
+
image: page.featured_image,
|
|
255
|
+
}),
|
|
256
|
+
},
|
|
257
|
+
createRenderContext(state.previewData.site, currentUrl),
|
|
258
|
+
);
|
|
259
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
260
|
+
html = injectSiteCustomizations(html, state);
|
|
261
|
+
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
262
|
+
state.emitted.frontPage = {
|
|
263
|
+
url: currentUrl,
|
|
264
|
+
title: page.title,
|
|
265
|
+
description: page.excerpt,
|
|
266
|
+
includeInFeed: false,
|
|
267
|
+
};
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let html = await state.engine.render(
|
|
272
|
+
'index',
|
|
273
|
+
{
|
|
274
|
+
menus: state.previewData.menus,
|
|
275
|
+
widgets: state.widgets,
|
|
276
|
+
taxonomies: state.renderData.taxonomies,
|
|
277
|
+
...route,
|
|
278
|
+
route: routeContext,
|
|
279
|
+
meta: buildPageMeta(state.previewData.site, {
|
|
280
|
+
currentUrl,
|
|
281
|
+
title: state.previewData.site.title,
|
|
282
|
+
description: state.previewData.site.description,
|
|
283
|
+
ogType: 'website',
|
|
284
|
+
}),
|
|
285
|
+
},
|
|
286
|
+
createRenderContext(state.previewData.site, currentUrl),
|
|
287
|
+
);
|
|
288
|
+
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
289
|
+
html = injectSiteCustomizations(html, state);
|
|
290
|
+
await writeOutput(state.writer, state.summaries, 'index.html', html, 'text/html');
|
|
291
|
+
state.emitted.frontPage = {
|
|
292
|
+
url: currentUrl,
|
|
293
|
+
title: state.previewData.site.title,
|
|
294
|
+
description: state.previewData.site.description,
|
|
295
|
+
includeInFeed: false,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
187
299
|
async function renderPost(state, post) {
|
|
188
300
|
const currentUrl = post.url;
|
|
189
301
|
let html = await state.engine.render(
|
|
@@ -191,7 +303,9 @@ async function renderPost(state, post) {
|
|
|
191
303
|
{
|
|
192
304
|
menus: state.previewData.menus,
|
|
193
305
|
widgets: state.widgets,
|
|
306
|
+
taxonomies: state.renderData.taxonomies,
|
|
194
307
|
post,
|
|
308
|
+
route: buildRouteContext('post', currentUrl),
|
|
195
309
|
meta: buildPageMeta(state.previewData.site, {
|
|
196
310
|
currentUrl,
|
|
197
311
|
title: buildDocumentTitle(post.title, state.previewData.site.title),
|
|
@@ -205,7 +319,7 @@ async function renderPost(state, post) {
|
|
|
205
319
|
createRenderContext(state.previewData.site, currentUrl),
|
|
206
320
|
);
|
|
207
321
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
208
|
-
html =
|
|
322
|
+
html = injectSiteCustomizations(html, state);
|
|
209
323
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
210
324
|
state.emitted.posts.push({
|
|
211
325
|
url: currentUrl,
|
|
@@ -219,14 +333,19 @@ async function renderPost(state, post) {
|
|
|
219
333
|
|
|
220
334
|
async function renderPage(state, page) {
|
|
221
335
|
const currentUrl = page.url;
|
|
336
|
+
const outputPath = pageToOutputPath(page, state.previewData.site.permalinks.output_style);
|
|
337
|
+
const canonicalUrl = normalizeOptionalString(page.canonical_url) || currentUrl;
|
|
222
338
|
let html = await state.engine.render(
|
|
223
339
|
'page',
|
|
224
340
|
{
|
|
225
341
|
menus: state.previewData.menus,
|
|
226
342
|
widgets: state.widgets,
|
|
343
|
+
taxonomies: state.renderData.taxonomies,
|
|
227
344
|
page,
|
|
345
|
+
route: buildRouteContext('page', currentUrl),
|
|
228
346
|
meta: buildPageMeta(state.previewData.site, {
|
|
229
347
|
currentUrl,
|
|
348
|
+
canonicalUrl,
|
|
230
349
|
title: buildDocumentTitle(page.title, state.previewData.site.title),
|
|
231
350
|
description: page.excerpt,
|
|
232
351
|
ogType: 'website',
|
|
@@ -236,13 +355,14 @@ async function renderPage(state, page) {
|
|
|
236
355
|
createRenderContext(state.previewData.site, currentUrl),
|
|
237
356
|
);
|
|
238
357
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
239
|
-
html =
|
|
240
|
-
await writeOutput(state.writer, state.summaries,
|
|
358
|
+
html = injectSiteCustomizations(html, state);
|
|
359
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
241
360
|
state.emitted.pages.push({
|
|
242
361
|
url: currentUrl,
|
|
243
362
|
title: page.title,
|
|
244
363
|
description: page.excerpt,
|
|
245
364
|
status: page.status,
|
|
365
|
+
includeInSitemap: page.omit_from_sitemap !== true,
|
|
246
366
|
});
|
|
247
367
|
}
|
|
248
368
|
|
|
@@ -256,6 +376,8 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
256
376
|
{
|
|
257
377
|
menus: state.previewData.menus,
|
|
258
378
|
widgets: state.widgets,
|
|
379
|
+
taxonomies: state.renderData.taxonomies,
|
|
380
|
+
route: buildRouteContext('not_found', '/404.html'),
|
|
259
381
|
meta: buildPageMeta(state.previewData.site, {
|
|
260
382
|
currentUrl: '/404.html',
|
|
261
383
|
title: state.previewData.site.title,
|
|
@@ -266,7 +388,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
266
388
|
createRenderContext(state.previewData.site, '/404.html'),
|
|
267
389
|
);
|
|
268
390
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
269
|
-
html =
|
|
391
|
+
html = injectSiteCustomizations(html, state);
|
|
270
392
|
await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
|
|
271
393
|
}
|
|
272
394
|
|
|
@@ -283,6 +405,9 @@ function normalizePreviewData(previewData) {
|
|
|
283
405
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
284
406
|
disallowComments: previewData.site.disallowComments === true,
|
|
285
407
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
408
|
+
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
409
|
+
post_index: normalizePostIndex(previewData.site.post_index),
|
|
410
|
+
footer: normalizeSiteFooter(previewData.site.footer),
|
|
286
411
|
};
|
|
287
412
|
|
|
288
413
|
return {
|
|
@@ -290,6 +415,7 @@ function normalizePreviewData(previewData) {
|
|
|
290
415
|
site: normalizedSite,
|
|
291
416
|
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.mediaBaseUrl),
|
|
292
417
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
418
|
+
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
293
419
|
content: {
|
|
294
420
|
...previewData.content,
|
|
295
421
|
authors: previewData.content.authors.map((author) => ({
|
|
@@ -333,6 +459,22 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
333
459
|
);
|
|
334
460
|
}
|
|
335
461
|
|
|
462
|
+
function normalizeSiteFooter(footer) {
|
|
463
|
+
const source = footer && typeof footer === 'object' && !Array.isArray(footer) ? footer : {};
|
|
464
|
+
const attribution = source.attribution && typeof source.attribution === 'object' && !Array.isArray(source.attribution)
|
|
465
|
+
? source.attribution
|
|
466
|
+
: {};
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
...source,
|
|
470
|
+
copyright_text: normalizeOptionalString(source.copyright_text),
|
|
471
|
+
attribution: {
|
|
472
|
+
...attribution,
|
|
473
|
+
enabled: attribution.enabled !== false,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
336
478
|
function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
337
479
|
const normalizedItem = {
|
|
338
480
|
...item,
|
|
@@ -357,6 +499,23 @@ function normalizeCustomCss(customCss) {
|
|
|
357
499
|
return content ? { content } : undefined;
|
|
358
500
|
}
|
|
359
501
|
|
|
502
|
+
function normalizeCustomHtml(customHtml) {
|
|
503
|
+
if (!customHtml || typeof customHtml !== 'object') {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const headEnd = normalizeOptionalRawString(customHtml.head_end?.content);
|
|
508
|
+
const bodyEnd = normalizeOptionalRawString(customHtml.body_end?.content);
|
|
509
|
+
if (!headEnd && !bodyEnd) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
...(headEnd ? { head_end: { content: headEnd } } : {}),
|
|
515
|
+
...(bodyEnd ? { body_end: { content: bodyEnd } } : {}),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
360
519
|
function normalizePermalinks(permalinks) {
|
|
361
520
|
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
362
521
|
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
@@ -372,8 +531,37 @@ function normalizePermalinks(permalinks) {
|
|
|
372
531
|
};
|
|
373
532
|
}
|
|
374
533
|
|
|
534
|
+
function normalizeFrontPage(frontPage) {
|
|
535
|
+
if (!frontPage || typeof frontPage !== 'object') {
|
|
536
|
+
return { ...DEFAULT_FRONT_PAGE };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const type = ['theme_index', 'page', 'standalone_html'].includes(frontPage.type)
|
|
540
|
+
? frontPage.type
|
|
541
|
+
: DEFAULT_FRONT_PAGE.type;
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
type,
|
|
545
|
+
...(type === 'page' ? { page_slug: normalizeOptionalString(frontPage.page_slug) } : {}),
|
|
546
|
+
...(type === 'standalone_html' ? { html: normalizeOptionalRawString(frontPage.html) } : {}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function normalizePostIndex(postIndex) {
|
|
551
|
+
if (!postIndex || typeof postIndex !== 'object') {
|
|
552
|
+
return { ...DEFAULT_POST_INDEX };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
enabled: postIndex.enabled !== false,
|
|
557
|
+
path: normalizeNonEmptyString(postIndex.path, DEFAULT_POST_INDEX.path),
|
|
558
|
+
paginate: postIndex.paginate !== false,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
375
562
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
376
563
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
564
|
+
const themeSupportsPostIndex = themeMetadata?.features?.postIndex !== false;
|
|
377
565
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
378
566
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
379
567
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -402,23 +590,38 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
402
590
|
}));
|
|
403
591
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
404
592
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
593
|
+
const frontPage = previewData.site.front_page;
|
|
594
|
+
const postIndex = previewData.site.post_index;
|
|
595
|
+
const effectivePostIndexEnabled = postIndex.enabled !== false && themeSupportsPostIndex;
|
|
596
|
+
const effectivePostIndexPaginate = effectivePostIndexEnabled && postIndex.paginate !== false;
|
|
597
|
+
const postIndexBasePath = normalizeRoutePath(postIndex.path || DEFAULT_POST_INDEX.path);
|
|
598
|
+
|
|
599
|
+
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && postIndexBasePath === '/') {
|
|
600
|
+
throw new Error('Invalid front page configuration: site.front_page occupies "/" so site.post_index.path must not be "/". Set site.post_index.path to a non-root path or disable site.post_index.');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath);
|
|
604
|
+
const pageFrontPageSlug = frontPage.type === 'page' ? frontPage.page_slug : '';
|
|
605
|
+
const preparedPages = pageFrontPageSlug
|
|
606
|
+
? pages.filter((page) => page.slug !== pageFrontPageSlug)
|
|
607
|
+
: pages;
|
|
405
608
|
|
|
406
609
|
return {
|
|
407
610
|
posts,
|
|
408
|
-
pages,
|
|
611
|
+
pages: preparedPages,
|
|
409
612
|
postBySlug,
|
|
410
|
-
|
|
613
|
+
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
614
|
+
frontPageRoute,
|
|
615
|
+
indexRoutes: buildPostIndexRoutes({
|
|
616
|
+
enabled: effectivePostIndexEnabled,
|
|
617
|
+
paginate: effectivePostIndexPaginate,
|
|
411
618
|
items: previewData.content.posts,
|
|
412
619
|
postsPerPage: previewData.site.postsPerPage,
|
|
413
|
-
basePath:
|
|
620
|
+
basePath: postIndexBasePath,
|
|
414
621
|
outputStyle: previewData.site.permalinks.output_style,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
totalPages: entry.totalPages,
|
|
419
|
-
posts: buildStructuredPostCollection(entry.items, postBySlug),
|
|
420
|
-
pagination: buildStructuredPagination(entry.paginationData),
|
|
421
|
-
})),
|
|
622
|
+
postBySlug,
|
|
623
|
+
frontPage,
|
|
624
|
+
}),
|
|
422
625
|
archiveRoutes: buildPaginatedCollection({
|
|
423
626
|
items: previewData.content.posts,
|
|
424
627
|
postsPerPage: previewData.site.postsPerPage,
|
|
@@ -458,6 +661,101 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
458
661
|
};
|
|
459
662
|
}
|
|
460
663
|
|
|
664
|
+
function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug) {
|
|
665
|
+
return {
|
|
666
|
+
categories: buildGlobalTaxonomyItems(previewData.site, 'categories', previewData.content.categories, categoryCountBySlug),
|
|
667
|
+
tags: buildGlobalTaxonomyItems(previewData.site, 'tags', previewData.content.tags, tagCountBySlug),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, postIndexBasePath) {
|
|
672
|
+
if (frontPage.type === 'theme_index') {
|
|
673
|
+
if (effectivePostIndexEnabled && postIndexBasePath === '/') {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
path: '/',
|
|
679
|
+
front_page_type: 'theme_index',
|
|
680
|
+
posts: buildStructuredPostCollection([], new Map()),
|
|
681
|
+
pagination: buildStructuredPagination(buildDisabledPaginationData(0)),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (frontPage.type === 'page') {
|
|
686
|
+
const page = pages.find((entry) => entry.slug === frontPage.page_slug);
|
|
687
|
+
if (!page) {
|
|
688
|
+
throw new Error(`Invalid front page configuration: site.front_page.page_slug "${frontPage.page_slug}" does not match a page.`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
path: '/',
|
|
693
|
+
front_page_type: 'page',
|
|
694
|
+
page,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (frontPage.type === 'standalone_html') {
|
|
699
|
+
if (!normalizeOptionalRawString(frontPage.html)) {
|
|
700
|
+
throw new Error('Invalid front page configuration: site.front_page.html is required for standalone_html.');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
path: '/',
|
|
705
|
+
front_page_type: 'standalone_html',
|
|
706
|
+
html: frontPage.html,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildPostIndexRoutes(options) {
|
|
714
|
+
if (!options.enabled) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!options.paginate) {
|
|
719
|
+
const items = options.items.slice(0, options.postsPerPage);
|
|
720
|
+
return [{
|
|
721
|
+
path: options.basePath,
|
|
722
|
+
route_type: 'post_index',
|
|
723
|
+
is_front_page: options.basePath === '/' && options.frontPage.type === 'theme_index',
|
|
724
|
+
is_post_index: true,
|
|
725
|
+
page: 1,
|
|
726
|
+
totalPages: 1,
|
|
727
|
+
posts: buildStructuredPostCollection(items, options.postBySlug),
|
|
728
|
+
pagination: buildStructuredPagination(buildDisabledPaginationData(options.items.length)),
|
|
729
|
+
}];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return buildPaginatedCollection({
|
|
733
|
+
items: options.items,
|
|
734
|
+
postsPerPage: options.postsPerPage,
|
|
735
|
+
basePath: options.basePath,
|
|
736
|
+
outputStyle: options.outputStyle,
|
|
737
|
+
}).map((entry) => ({
|
|
738
|
+
path: entry.path,
|
|
739
|
+
route_type: 'post_index',
|
|
740
|
+
is_front_page: entry.path === '/' && options.frontPage.type === 'theme_index',
|
|
741
|
+
is_post_index: true,
|
|
742
|
+
page: entry.page,
|
|
743
|
+
totalPages: entry.totalPages,
|
|
744
|
+
posts: buildStructuredPostCollection(entry.items, options.postBySlug),
|
|
745
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function buildGlobalTaxonomyItems(site, permalinkKind, items, countBySlug) {
|
|
750
|
+
return items.map((item) => ({
|
|
751
|
+
name: item.name,
|
|
752
|
+
slug: item.slug,
|
|
753
|
+
url: resolvePermalink(site, permalinkKind, item).url,
|
|
754
|
+
count: countBySlug.get(item.slug) || 0,
|
|
755
|
+
description: typeof item.description === 'string' ? item.description : '',
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
|
|
461
759
|
function resolveWidgetAreas(previewData, renderData) {
|
|
462
760
|
if (!previewData.widgets || typeof previewData.widgets !== 'object') {
|
|
463
761
|
return {};
|
|
@@ -852,6 +1150,7 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
852
1150
|
const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
|
|
853
1151
|
|
|
854
1152
|
return {
|
|
1153
|
+
enabled: true,
|
|
855
1154
|
currentPage,
|
|
856
1155
|
totalPages,
|
|
857
1156
|
totalPosts,
|
|
@@ -870,8 +1169,23 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
870
1169
|
};
|
|
871
1170
|
}
|
|
872
1171
|
|
|
1172
|
+
function buildDisabledPaginationData(totalPosts) {
|
|
1173
|
+
return {
|
|
1174
|
+
enabled: false,
|
|
1175
|
+
currentPage: 1,
|
|
1176
|
+
totalPages: 1,
|
|
1177
|
+
totalPosts,
|
|
1178
|
+
hasNext: false,
|
|
1179
|
+
hasPrev: false,
|
|
1180
|
+
nextUrl: undefined,
|
|
1181
|
+
prevUrl: undefined,
|
|
1182
|
+
pages: [],
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
873
1186
|
function buildStructuredPagination(paginationData) {
|
|
874
1187
|
return {
|
|
1188
|
+
enabled: paginationData.enabled !== false,
|
|
875
1189
|
current_page: paginationData.currentPage,
|
|
876
1190
|
total_pages: paginationData.totalPages,
|
|
877
1191
|
total_items: paginationData.totalPosts,
|
|
@@ -1144,6 +1458,10 @@ function normalizeOptionalString(value) {
|
|
|
1144
1458
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
1145
1459
|
}
|
|
1146
1460
|
|
|
1461
|
+
function normalizeOptionalRawString(value) {
|
|
1462
|
+
return typeof value === 'string' && value.trim() ? value : '';
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1147
1465
|
function normalizeLocale(value) {
|
|
1148
1466
|
if (typeof value !== 'string' || !value.trim()) {
|
|
1149
1467
|
return DEFAULT_LOCALE;
|
|
@@ -1311,10 +1629,20 @@ function createRenderContext(site, currentUrl) {
|
|
|
1311
1629
|
};
|
|
1312
1630
|
}
|
|
1313
1631
|
|
|
1632
|
+
function buildRouteContext(type, url, options = {}) {
|
|
1633
|
+
return {
|
|
1634
|
+
type,
|
|
1635
|
+
is_front_page: options.isFrontPage === true,
|
|
1636
|
+
is_post_index: options.isPostIndex === true,
|
|
1637
|
+
path: url,
|
|
1638
|
+
url,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1314
1642
|
function buildPageMeta(site, options = {}) {
|
|
1315
1643
|
const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
|
|
1316
1644
|
const resolvedDescription = normalizeOptionalString(options.description);
|
|
1317
|
-
const canonicalUrl = resolveMetaCanonicalUrl(site, options.currentUrl);
|
|
1645
|
+
const canonicalUrl = resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl);
|
|
1318
1646
|
const ogImage = resolveMetaImageUrl(options.image);
|
|
1319
1647
|
const ogType = normalizeNonEmptyString(options.ogType, 'website');
|
|
1320
1648
|
const publishedTime = normalizeOptionalString(options.publishedTime);
|
|
@@ -1494,6 +1822,22 @@ function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output
|
|
|
1494
1822
|
return normalizedPath;
|
|
1495
1823
|
}
|
|
1496
1824
|
|
|
1825
|
+
function pagePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
1826
|
+
const normalizedPath = normalizeRoutePath(routePath);
|
|
1827
|
+
if (outputStyle !== 'html-extension') {
|
|
1828
|
+
return routePathToPublicUrl(normalizedPath, outputStyle);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const withoutTrailingSlash = normalizedPath.replace(/\/$/, '');
|
|
1832
|
+
if (withoutTrailingSlash === '/index') {
|
|
1833
|
+
return '/';
|
|
1834
|
+
}
|
|
1835
|
+
if (withoutTrailingSlash.endsWith('/index')) {
|
|
1836
|
+
return `${withoutTrailingSlash.slice(0, -'/index'.length)}/`;
|
|
1837
|
+
}
|
|
1838
|
+
return withoutTrailingSlash;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1497
1841
|
function normalizeRoutePath(routePath) {
|
|
1498
1842
|
if (!routePath || routePath === '/') {
|
|
1499
1843
|
return '/';
|
|
@@ -1504,7 +1848,7 @@ function normalizeRoutePath(routePath) {
|
|
|
1504
1848
|
|
|
1505
1849
|
function resolvePagePermalink(site, page) {
|
|
1506
1850
|
if (normalizeOptionalString(page.path)) {
|
|
1507
|
-
return buildRouteInfo(page.path, site.permalinks.output_style);
|
|
1851
|
+
return buildRouteInfo(page.path, site.permalinks.output_style, { pagePath: true });
|
|
1508
1852
|
}
|
|
1509
1853
|
return resolvePermalink(site, 'pages', page);
|
|
1510
1854
|
}
|
|
@@ -1514,15 +1858,21 @@ function resolvePermalink(site, kind, item) {
|
|
|
1514
1858
|
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
1515
1859
|
}
|
|
1516
1860
|
|
|
1517
|
-
function buildRouteInfo(routePath, outputStyle) {
|
|
1861
|
+
function buildRouteInfo(routePath, outputStyle, options = {}) {
|
|
1518
1862
|
const path = normalizeRoutePath(routePath);
|
|
1519
1863
|
return {
|
|
1520
1864
|
path,
|
|
1521
|
-
url: routePathToPublicUrl(path, outputStyle),
|
|
1865
|
+
url: options.pagePath ? pagePathToPublicUrl(path, outputStyle) : routePathToPublicUrl(path, outputStyle),
|
|
1522
1866
|
outputPath: routePathToOutputPath(path, outputStyle),
|
|
1523
1867
|
};
|
|
1524
1868
|
}
|
|
1525
1869
|
|
|
1870
|
+
function pageToOutputPath(page, outputStyle) {
|
|
1871
|
+
return normalizeOptionalString(page.path)
|
|
1872
|
+
? routePathToOutputPath(page.path, outputStyle)
|
|
1873
|
+
: routePathToOutputPath(page.url, outputStyle);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1526
1876
|
function applyPermalinkPattern(pattern, kind, item, site) {
|
|
1527
1877
|
const tokenValues = buildPermalinkTokenValues(kind, item, site);
|
|
1528
1878
|
const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
|
|
@@ -1579,7 +1929,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1579
1929
|
}
|
|
1580
1930
|
|
|
1581
1931
|
for (const page of state.renderData.pages) {
|
|
1582
|
-
assertSafeSlugDerivedOutputPath(page.slug,
|
|
1932
|
+
assertSafeSlugDerivedOutputPath(page.slug, pageToOutputPath(page, outputStyle));
|
|
1583
1933
|
}
|
|
1584
1934
|
|
|
1585
1935
|
for (const category of state.previewData.content.categories) {
|
|
@@ -1591,6 +1941,10 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1591
1941
|
}
|
|
1592
1942
|
|
|
1593
1943
|
const routeEntries = [
|
|
1944
|
+
...(state.renderData.frontPageRoute ? [{
|
|
1945
|
+
url: '/',
|
|
1946
|
+
outputPath: 'index.html',
|
|
1947
|
+
}] : []),
|
|
1594
1948
|
...state.renderData.indexRoutes.map((route) => ({
|
|
1595
1949
|
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1596
1950
|
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
@@ -1613,7 +1967,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1613
1967
|
})),
|
|
1614
1968
|
...state.renderData.pages.map((page) => ({
|
|
1615
1969
|
url: page.url,
|
|
1616
|
-
outputPath:
|
|
1970
|
+
outputPath: pageToOutputPath(page, outputStyle),
|
|
1617
1971
|
})),
|
|
1618
1972
|
];
|
|
1619
1973
|
assertUniqueRoutes(routeEntries);
|
|
@@ -1625,7 +1979,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1625
1979
|
];
|
|
1626
1980
|
|
|
1627
1981
|
if (state.options.generateSpecialFiles) {
|
|
1628
|
-
plannedPaths.push('404.html', 'robots.txt'
|
|
1982
|
+
plannedPaths.push('404.html', 'robots.txt');
|
|
1629
1983
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
1630
1984
|
plannedPaths.push('sitemap.xml', 'feed.xml');
|
|
1631
1985
|
}
|
|
@@ -1724,6 +2078,12 @@ function sha256(content) {
|
|
|
1724
2078
|
return hash.digest('hex');
|
|
1725
2079
|
}
|
|
1726
2080
|
|
|
2081
|
+
function injectSiteCustomizations(html, state) {
|
|
2082
|
+
let next = injectCustomCssAssetLink(html, state.customCssHref);
|
|
2083
|
+
next = injectCustomHtml(next, state.customHtml);
|
|
2084
|
+
return next;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1727
2087
|
function injectCustomCssAssetLink(html, href) {
|
|
1728
2088
|
if (!normalizeOptionalString(href)) {
|
|
1729
2089
|
return html;
|
|
@@ -1732,14 +2092,36 @@ function injectCustomCssAssetLink(html, href) {
|
|
|
1732
2092
|
return html.replace('</head>', ` <link rel="stylesheet" href="${escapeHtml(href)}">\n</head>`);
|
|
1733
2093
|
}
|
|
1734
2094
|
|
|
2095
|
+
function injectCustomHtml(html, customHtml) {
|
|
2096
|
+
let next = html;
|
|
2097
|
+
const headEnd = normalizeOptionalRawString(customHtml?.head_end?.content);
|
|
2098
|
+
const bodyEnd = normalizeOptionalRawString(customHtml?.body_end?.content);
|
|
2099
|
+
|
|
2100
|
+
if (headEnd) {
|
|
2101
|
+
next = next.replace('</head>', `${headEnd}\n</head>`);
|
|
2102
|
+
}
|
|
2103
|
+
if (bodyEnd) {
|
|
2104
|
+
next = next.replace('</body>', `${bodyEnd}\n</body>`);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
return next;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1735
2110
|
function buildSitemapXml(site, emitted, generatedAt) {
|
|
1736
2111
|
const entries = [
|
|
2112
|
+
...(emitted.frontPage
|
|
2113
|
+
? [{
|
|
2114
|
+
url: emitted.frontPage.url,
|
|
2115
|
+
changefreq: 'daily',
|
|
2116
|
+
priority: 1.0,
|
|
2117
|
+
}]
|
|
2118
|
+
: []),
|
|
1737
2119
|
...emitted.indexRoutes
|
|
1738
|
-
.filter((route) => route.
|
|
2120
|
+
.filter((route) => route.page === 1)
|
|
1739
2121
|
.map((route) => ({
|
|
1740
2122
|
url: route.url,
|
|
1741
2123
|
changefreq: 'daily',
|
|
1742
|
-
priority: 1.0,
|
|
2124
|
+
priority: route.url === '/' ? 1.0 : 0.7,
|
|
1743
2125
|
})),
|
|
1744
2126
|
...emitted.posts.map((post) => ({
|
|
1745
2127
|
url: post.url,
|
|
@@ -1747,11 +2129,13 @@ function buildSitemapXml(site, emitted, generatedAt) {
|
|
|
1747
2129
|
changefreq: 'weekly',
|
|
1748
2130
|
priority: 0.8,
|
|
1749
2131
|
})),
|
|
1750
|
-
...emitted.pages
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2132
|
+
...emitted.pages
|
|
2133
|
+
.filter((page) => page.includeInSitemap !== false)
|
|
2134
|
+
.map((page) => ({
|
|
2135
|
+
url: page.url,
|
|
2136
|
+
changefreq: 'monthly',
|
|
2137
|
+
priority: 0.7,
|
|
2138
|
+
})),
|
|
1755
2139
|
];
|
|
1756
2140
|
|
|
1757
2141
|
const body = entries.map((entry) => {
|
|
@@ -1788,38 +2172,6 @@ function buildRobotsTxt(site) {
|
|
|
1788
2172
|
return `${lines.join('\n')}\n`;
|
|
1789
2173
|
}
|
|
1790
2174
|
|
|
1791
|
-
function buildMetaJson(site, emitted, generatedAt) {
|
|
1792
|
-
const pages = [
|
|
1793
|
-
...emitted.posts.map((post) => ({
|
|
1794
|
-
url: post.url,
|
|
1795
|
-
title: post.title,
|
|
1796
|
-
description: post.description,
|
|
1797
|
-
publishedAt: post.publishedAt,
|
|
1798
|
-
updatedAt: post.updatedAt,
|
|
1799
|
-
type: 'post',
|
|
1800
|
-
metadata: { status: post.status },
|
|
1801
|
-
})),
|
|
1802
|
-
...emitted.pages.map((page) => ({
|
|
1803
|
-
url: page.url,
|
|
1804
|
-
title: page.title,
|
|
1805
|
-
description: page.description,
|
|
1806
|
-
type: 'page',
|
|
1807
|
-
metadata: { status: page.status },
|
|
1808
|
-
})),
|
|
1809
|
-
];
|
|
1810
|
-
|
|
1811
|
-
return JSON.stringify({
|
|
1812
|
-
generated: formatUtcIsoSeconds(generatedAt),
|
|
1813
|
-
site: {
|
|
1814
|
-
title: site.title,
|
|
1815
|
-
description: site.description,
|
|
1816
|
-
url: site.url,
|
|
1817
|
-
locale: site.locale,
|
|
1818
|
-
},
|
|
1819
|
-
pages,
|
|
1820
|
-
}, null, 2);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
2175
|
function getContentType(assetPath) {
|
|
1824
2176
|
const ext = assetPath.split('.').pop()?.toLowerCase();
|
|
1825
2177
|
const contentTypes = {
|
|
@@ -3,6 +3,13 @@ import hljs from 'highlight.js';
|
|
|
3
3
|
import anchor from 'markdown-it-anchor';
|
|
4
4
|
|
|
5
5
|
const TOC_LEVELS = new Set([2, 3, 4]);
|
|
6
|
+
const ALERT_TYPES = new Map([
|
|
7
|
+
['NOTE', { className: 'note', title: 'Note' }],
|
|
8
|
+
['TIP', { className: 'tip', title: 'Tip' }],
|
|
9
|
+
['IMPORTANT', { className: 'important', title: 'Important' }],
|
|
10
|
+
['WARNING', { className: 'warning', title: 'Warning' }],
|
|
11
|
+
['CAUTION', { className: 'caution', title: 'Caution' }],
|
|
12
|
+
]);
|
|
6
13
|
|
|
7
14
|
export function renderDocumentContent(content, documentType = 'markdown') {
|
|
8
15
|
return renderDocument(content, documentType).html;
|
|
@@ -62,6 +69,8 @@ function createMarkdownRenderer(toc) {
|
|
|
62
69
|
},
|
|
63
70
|
});
|
|
64
71
|
|
|
72
|
+
markdown.use(markdownTaskLists);
|
|
73
|
+
markdown.use(markdownAlerts);
|
|
65
74
|
markdown.use(anchor, {
|
|
66
75
|
slugify: slugify,
|
|
67
76
|
callback(token, { slug, title }) {
|
|
@@ -82,6 +91,189 @@ function createMarkdownRenderer(toc) {
|
|
|
82
91
|
return markdown;
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
function markdownTaskLists(markdown) {
|
|
95
|
+
markdown.core.ruler.after('inline', 'zeropress_task_lists', (state) => {
|
|
96
|
+
const listStack = [];
|
|
97
|
+
const listItemStack = [];
|
|
98
|
+
|
|
99
|
+
for (let index = 0; index < state.tokens.length; index += 1) {
|
|
100
|
+
const token = state.tokens[index];
|
|
101
|
+
|
|
102
|
+
if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
|
|
103
|
+
listStack.push(index);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (token.type === 'bullet_list_close' || token.type === 'ordered_list_close') {
|
|
108
|
+
listStack.pop();
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (token.type === 'list_item_open') {
|
|
113
|
+
listItemStack.push({
|
|
114
|
+
index,
|
|
115
|
+
listIndex: listStack[listStack.length - 1],
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (token.type === 'list_item_close') {
|
|
121
|
+
listItemStack.pop();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (token.type !== 'inline' || listItemStack.length === 0) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const markerMatch = token.content.match(/^\s*\[([ xX])\](?:\s+|$)/);
|
|
130
|
+
if (!markerMatch) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const listItem = listItemStack[listItemStack.length - 1];
|
|
135
|
+
const listToken = state.tokens[listItem.listIndex];
|
|
136
|
+
const listItemToken = state.tokens[listItem.index];
|
|
137
|
+
if (!listToken || !listItemToken) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
addTokenClass(listToken, 'contains-task-list');
|
|
142
|
+
addTokenClass(listItemToken, 'task-list-item');
|
|
143
|
+
|
|
144
|
+
const checkboxToken = new state.Token('html_inline', '', 0);
|
|
145
|
+
const checked = markerMatch[1].toLowerCase() === 'x' ? ' checked' : '';
|
|
146
|
+
checkboxToken.content = `<input class="task-list-item-checkbox" type="checkbox"${checked} disabled> `;
|
|
147
|
+
|
|
148
|
+
const strippedContent = token.content.slice(markerMatch[0].length);
|
|
149
|
+
token.content = strippedContent;
|
|
150
|
+
token.children = [];
|
|
151
|
+
state.md.inline.parse(strippedContent, state.md, state.env, token.children);
|
|
152
|
+
token.children.unshift(checkboxToken);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function markdownAlerts(markdown) {
|
|
158
|
+
markdown.core.ruler.after('inline', 'zeropress_alerts', (state) => {
|
|
159
|
+
for (let index = 0; index < state.tokens.length; index += 1) {
|
|
160
|
+
const token = state.tokens[index];
|
|
161
|
+
if (token.type !== 'blockquote_open') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const closeIndex = findMatchingBlockquoteClose(state.tokens, index);
|
|
166
|
+
if (closeIndex === -1) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const firstParagraph = findFirstParagraph(state.tokens, index + 1, closeIndex);
|
|
171
|
+
if (!firstParagraph) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const inlineToken = state.tokens[firstParagraph.inlineIndex];
|
|
176
|
+
const alert = stripAlertMarker(inlineToken, state);
|
|
177
|
+
if (!alert) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
token.tag = 'aside';
|
|
182
|
+
token.attrSet('class', `zp-alert zp-alert--${alert.className}`);
|
|
183
|
+
token.attrSet('role', 'note');
|
|
184
|
+
state.tokens[closeIndex].tag = 'aside';
|
|
185
|
+
|
|
186
|
+
if (inlineToken.content.trim() === '') {
|
|
187
|
+
state.tokens.splice(firstParagraph.openIndex, 3);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
state.tokens.splice(index + 1, 0, ...createAlertTitleTokens(state, alert.title));
|
|
191
|
+
index += 3;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findMatchingBlockquoteClose(tokens, openIndex) {
|
|
197
|
+
let depth = 0;
|
|
198
|
+
|
|
199
|
+
for (let index = openIndex; index < tokens.length; index += 1) {
|
|
200
|
+
if (tokens[index].type === 'blockquote_open') {
|
|
201
|
+
depth += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (tokens[index].type === 'blockquote_close') {
|
|
206
|
+
depth -= 1;
|
|
207
|
+
if (depth === 0) {
|
|
208
|
+
return index;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return -1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findFirstParagraph(tokens, startIndex, endIndex) {
|
|
217
|
+
if (
|
|
218
|
+
startIndex + 2 < endIndex
|
|
219
|
+
&& tokens[startIndex].type === 'paragraph_open'
|
|
220
|
+
&& tokens[startIndex + 1].type === 'inline'
|
|
221
|
+
&& tokens[startIndex + 2].type === 'paragraph_close'
|
|
222
|
+
) {
|
|
223
|
+
return {
|
|
224
|
+
openIndex: startIndex,
|
|
225
|
+
inlineIndex: startIndex + 1,
|
|
226
|
+
closeIndex: startIndex + 2,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stripAlertMarker(inlineToken, state) {
|
|
234
|
+
const markerMatch = inlineToken.content.match(/^\s*\[!([A-Za-z]+)\][ \t]*(?:\n)?/);
|
|
235
|
+
if (!markerMatch) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const alertType = ALERT_TYPES.get(markerMatch[1].toUpperCase());
|
|
240
|
+
if (!alertType) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const strippedContent = inlineToken.content.slice(markerMatch[0].length);
|
|
245
|
+
inlineToken.content = strippedContent;
|
|
246
|
+
inlineToken.children = [];
|
|
247
|
+
state.md.inline.parse(strippedContent, state.md, state.env, inlineToken.children);
|
|
248
|
+
|
|
249
|
+
return alertType;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createAlertTitleTokens(state, title) {
|
|
253
|
+
const openToken = new state.Token('paragraph_open', 'p', 1);
|
|
254
|
+
openToken.attrSet('class', 'zp-alert__title');
|
|
255
|
+
|
|
256
|
+
const inlineToken = new state.Token('inline', '', 0);
|
|
257
|
+
inlineToken.content = title;
|
|
258
|
+
const textToken = new state.Token('text', '', 0);
|
|
259
|
+
textToken.content = title;
|
|
260
|
+
inlineToken.children = [textToken];
|
|
261
|
+
|
|
262
|
+
const closeToken = new state.Token('paragraph_close', 'p', -1);
|
|
263
|
+
|
|
264
|
+
return [openToken, inlineToken, closeToken];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function addTokenClass(token, className) {
|
|
268
|
+
const existingValue = token.attrGet('class') || '';
|
|
269
|
+
const existingClasses = existingValue.split(/\s+/).filter(Boolean);
|
|
270
|
+
if (existingClasses.includes(className)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
token.attrJoin('class', className);
|
|
275
|
+
}
|
|
276
|
+
|
|
85
277
|
function normalizeDocumentType(value) {
|
|
86
278
|
return value === 'plaintext' || value === 'html' ? value : 'markdown';
|
|
87
279
|
}
|
|
@@ -113,16 +305,18 @@ function sanitizeHtml(html) {
|
|
|
113
305
|
'strong', 'em', 'u', 's', 'code', 'pre',
|
|
114
306
|
'a', 'img',
|
|
115
307
|
'ul', 'ol', 'li',
|
|
116
|
-
'blockquote',
|
|
308
|
+
'blockquote', 'aside',
|
|
117
309
|
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
118
310
|
'div', 'span', 'nav',
|
|
119
|
-
'iframe',
|
|
311
|
+
'iframe', 'input',
|
|
120
312
|
]);
|
|
121
313
|
|
|
122
314
|
const allowedAttributes = {
|
|
123
315
|
a: new Set(['href', 'title', 'class', 'id']),
|
|
316
|
+
aside: new Set(['role', 'class', 'id']),
|
|
124
317
|
img: new Set(['src', 'alt', 'title', 'class', 'id', 'width', 'height']),
|
|
125
318
|
iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class']),
|
|
319
|
+
input: new Set(['type', 'checked', 'disabled', 'class', 'id']),
|
|
126
320
|
'*': new Set(['class', 'id']),
|
|
127
321
|
};
|
|
128
322
|
|
|
@@ -163,11 +357,15 @@ function sanitizeHtml(html) {
|
|
|
163
357
|
continue;
|
|
164
358
|
}
|
|
165
359
|
|
|
360
|
+
if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
166
364
|
filteredAttributes.push(`${attributeName}="${attributeValue}"`);
|
|
167
365
|
}
|
|
168
366
|
}
|
|
169
367
|
|
|
170
|
-
const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img';
|
|
368
|
+
const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input';
|
|
171
369
|
const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
|
|
172
370
|
return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
|
|
173
371
|
});
|
|
@@ -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) => {
|