@zeropress/build-core 0.5.0 → 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/package.json +3 -3
- package/src/api/build-site.js +331 -24
- package/src/render/content-renderer.js +201 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.5.
|
|
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": "0.5.
|
|
38
|
-
"@zeropress/theme-validator": "0.5.
|
|
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
|
}
|
|
@@ -129,6 +141,7 @@ async function createBuildState(input, options) {
|
|
|
129
141
|
options,
|
|
130
142
|
generatedAt: new Date(),
|
|
131
143
|
emitted: {
|
|
144
|
+
frontPage: null,
|
|
132
145
|
indexRoutes: [],
|
|
133
146
|
archiveRoutes: [],
|
|
134
147
|
categoryRoutes: [],
|
|
@@ -175,6 +188,10 @@ async function finalizeBuildResult(writer, summaries, options) {
|
|
|
175
188
|
|
|
176
189
|
async function renderRoute(state, templateName, route) {
|
|
177
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
|
+
});
|
|
178
195
|
let html = await state.engine.render(
|
|
179
196
|
templateName,
|
|
180
197
|
{
|
|
@@ -182,6 +199,7 @@ async function renderRoute(state, templateName, route) {
|
|
|
182
199
|
widgets: state.widgets,
|
|
183
200
|
taxonomies: state.renderData.taxonomies,
|
|
184
201
|
...route,
|
|
202
|
+
route: routeContext,
|
|
185
203
|
meta: buildPageMeta(state.previewData.site, {
|
|
186
204
|
currentUrl,
|
|
187
205
|
title: state.previewData.site.title,
|
|
@@ -197,6 +215,87 @@ async function renderRoute(state, templateName, route) {
|
|
|
197
215
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
198
216
|
}
|
|
199
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
|
+
|
|
200
299
|
async function renderPost(state, post) {
|
|
201
300
|
const currentUrl = post.url;
|
|
202
301
|
let html = await state.engine.render(
|
|
@@ -206,6 +305,7 @@ async function renderPost(state, post) {
|
|
|
206
305
|
widgets: state.widgets,
|
|
207
306
|
taxonomies: state.renderData.taxonomies,
|
|
208
307
|
post,
|
|
308
|
+
route: buildRouteContext('post', currentUrl),
|
|
209
309
|
meta: buildPageMeta(state.previewData.site, {
|
|
210
310
|
currentUrl,
|
|
211
311
|
title: buildDocumentTitle(post.title, state.previewData.site.title),
|
|
@@ -233,6 +333,8 @@ async function renderPost(state, post) {
|
|
|
233
333
|
|
|
234
334
|
async function renderPage(state, page) {
|
|
235
335
|
const currentUrl = page.url;
|
|
336
|
+
const outputPath = pageToOutputPath(page, state.previewData.site.permalinks.output_style);
|
|
337
|
+
const canonicalUrl = normalizeOptionalString(page.canonical_url) || currentUrl;
|
|
236
338
|
let html = await state.engine.render(
|
|
237
339
|
'page',
|
|
238
340
|
{
|
|
@@ -240,8 +342,10 @@ async function renderPage(state, page) {
|
|
|
240
342
|
widgets: state.widgets,
|
|
241
343
|
taxonomies: state.renderData.taxonomies,
|
|
242
344
|
page,
|
|
345
|
+
route: buildRouteContext('page', currentUrl),
|
|
243
346
|
meta: buildPageMeta(state.previewData.site, {
|
|
244
347
|
currentUrl,
|
|
348
|
+
canonicalUrl,
|
|
245
349
|
title: buildDocumentTitle(page.title, state.previewData.site.title),
|
|
246
350
|
description: page.excerpt,
|
|
247
351
|
ogType: 'website',
|
|
@@ -252,12 +356,13 @@ async function renderPage(state, page) {
|
|
|
252
356
|
);
|
|
253
357
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
254
358
|
html = injectSiteCustomizations(html, state);
|
|
255
|
-
await writeOutput(state.writer, state.summaries,
|
|
359
|
+
await writeOutput(state.writer, state.summaries, outputPath, html, 'text/html');
|
|
256
360
|
state.emitted.pages.push({
|
|
257
361
|
url: currentUrl,
|
|
258
362
|
title: page.title,
|
|
259
363
|
description: page.excerpt,
|
|
260
364
|
status: page.status,
|
|
365
|
+
includeInSitemap: page.omit_from_sitemap !== true,
|
|
261
366
|
});
|
|
262
367
|
}
|
|
263
368
|
|
|
@@ -272,6 +377,7 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
272
377
|
menus: state.previewData.menus,
|
|
273
378
|
widgets: state.widgets,
|
|
274
379
|
taxonomies: state.renderData.taxonomies,
|
|
380
|
+
route: buildRouteContext('not_found', '/404.html'),
|
|
275
381
|
meta: buildPageMeta(state.previewData.site, {
|
|
276
382
|
currentUrl: '/404.html',
|
|
277
383
|
title: state.previewData.site.title,
|
|
@@ -299,6 +405,9 @@ function normalizePreviewData(previewData) {
|
|
|
299
405
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
300
406
|
disallowComments: previewData.site.disallowComments === true,
|
|
301
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),
|
|
302
411
|
};
|
|
303
412
|
|
|
304
413
|
return {
|
|
@@ -350,6 +459,22 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
350
459
|
);
|
|
351
460
|
}
|
|
352
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
|
+
|
|
353
478
|
function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
354
479
|
const normalizedItem = {
|
|
355
480
|
...item,
|
|
@@ -406,8 +531,37 @@ function normalizePermalinks(permalinks) {
|
|
|
406
531
|
};
|
|
407
532
|
}
|
|
408
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
|
+
|
|
409
562
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
410
563
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
564
|
+
const themeSupportsPostIndex = themeMetadata?.features?.postIndex !== false;
|
|
411
565
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
412
566
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
413
567
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -436,24 +590,38 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
436
590
|
}));
|
|
437
591
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
438
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;
|
|
439
608
|
|
|
440
609
|
return {
|
|
441
610
|
posts,
|
|
442
|
-
pages,
|
|
611
|
+
pages: preparedPages,
|
|
443
612
|
postBySlug,
|
|
444
613
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
445
|
-
|
|
614
|
+
frontPageRoute,
|
|
615
|
+
indexRoutes: buildPostIndexRoutes({
|
|
616
|
+
enabled: effectivePostIndexEnabled,
|
|
617
|
+
paginate: effectivePostIndexPaginate,
|
|
446
618
|
items: previewData.content.posts,
|
|
447
619
|
postsPerPage: previewData.site.postsPerPage,
|
|
448
|
-
basePath:
|
|
620
|
+
basePath: postIndexBasePath,
|
|
449
621
|
outputStyle: previewData.site.permalinks.output_style,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
totalPages: entry.totalPages,
|
|
454
|
-
posts: buildStructuredPostCollection(entry.items, postBySlug),
|
|
455
|
-
pagination: buildStructuredPagination(entry.paginationData),
|
|
456
|
-
})),
|
|
622
|
+
postBySlug,
|
|
623
|
+
frontPage,
|
|
624
|
+
}),
|
|
457
625
|
archiveRoutes: buildPaginatedCollection({
|
|
458
626
|
items: previewData.content.posts,
|
|
459
627
|
postsPerPage: previewData.site.postsPerPage,
|
|
@@ -500,6 +668,84 @@ function buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug)
|
|
|
500
668
|
};
|
|
501
669
|
}
|
|
502
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
|
+
|
|
503
749
|
function buildGlobalTaxonomyItems(site, permalinkKind, items, countBySlug) {
|
|
504
750
|
return items.map((item) => ({
|
|
505
751
|
name: item.name,
|
|
@@ -904,6 +1150,7 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
904
1150
|
const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
|
|
905
1151
|
|
|
906
1152
|
return {
|
|
1153
|
+
enabled: true,
|
|
907
1154
|
currentPage,
|
|
908
1155
|
totalPages,
|
|
909
1156
|
totalPosts,
|
|
@@ -922,8 +1169,23 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outp
|
|
|
922
1169
|
};
|
|
923
1170
|
}
|
|
924
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
|
+
|
|
925
1186
|
function buildStructuredPagination(paginationData) {
|
|
926
1187
|
return {
|
|
1188
|
+
enabled: paginationData.enabled !== false,
|
|
927
1189
|
current_page: paginationData.currentPage,
|
|
928
1190
|
total_pages: paginationData.totalPages,
|
|
929
1191
|
total_items: paginationData.totalPosts,
|
|
@@ -1367,10 +1629,20 @@ function createRenderContext(site, currentUrl) {
|
|
|
1367
1629
|
};
|
|
1368
1630
|
}
|
|
1369
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
|
+
|
|
1370
1642
|
function buildPageMeta(site, options = {}) {
|
|
1371
1643
|
const resolvedTitle = normalizeNonEmptyString(options.title, site.title);
|
|
1372
1644
|
const resolvedDescription = normalizeOptionalString(options.description);
|
|
1373
|
-
const canonicalUrl = resolveMetaCanonicalUrl(site, options.currentUrl);
|
|
1645
|
+
const canonicalUrl = resolveMetaCanonicalUrl(site, options.canonicalUrl || options.currentUrl);
|
|
1374
1646
|
const ogImage = resolveMetaImageUrl(options.image);
|
|
1375
1647
|
const ogType = normalizeNonEmptyString(options.ogType, 'website');
|
|
1376
1648
|
const publishedTime = normalizeOptionalString(options.publishedTime);
|
|
@@ -1550,6 +1822,22 @@ function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output
|
|
|
1550
1822
|
return normalizedPath;
|
|
1551
1823
|
}
|
|
1552
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
|
+
|
|
1553
1841
|
function normalizeRoutePath(routePath) {
|
|
1554
1842
|
if (!routePath || routePath === '/') {
|
|
1555
1843
|
return '/';
|
|
@@ -1560,7 +1848,7 @@ function normalizeRoutePath(routePath) {
|
|
|
1560
1848
|
|
|
1561
1849
|
function resolvePagePermalink(site, page) {
|
|
1562
1850
|
if (normalizeOptionalString(page.path)) {
|
|
1563
|
-
return buildRouteInfo(page.path, site.permalinks.output_style);
|
|
1851
|
+
return buildRouteInfo(page.path, site.permalinks.output_style, { pagePath: true });
|
|
1564
1852
|
}
|
|
1565
1853
|
return resolvePermalink(site, 'pages', page);
|
|
1566
1854
|
}
|
|
@@ -1570,15 +1858,21 @@ function resolvePermalink(site, kind, item) {
|
|
|
1570
1858
|
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
1571
1859
|
}
|
|
1572
1860
|
|
|
1573
|
-
function buildRouteInfo(routePath, outputStyle) {
|
|
1861
|
+
function buildRouteInfo(routePath, outputStyle, options = {}) {
|
|
1574
1862
|
const path = normalizeRoutePath(routePath);
|
|
1575
1863
|
return {
|
|
1576
1864
|
path,
|
|
1577
|
-
url: routePathToPublicUrl(path, outputStyle),
|
|
1865
|
+
url: options.pagePath ? pagePathToPublicUrl(path, outputStyle) : routePathToPublicUrl(path, outputStyle),
|
|
1578
1866
|
outputPath: routePathToOutputPath(path, outputStyle),
|
|
1579
1867
|
};
|
|
1580
1868
|
}
|
|
1581
1869
|
|
|
1870
|
+
function pageToOutputPath(page, outputStyle) {
|
|
1871
|
+
return normalizeOptionalString(page.path)
|
|
1872
|
+
? routePathToOutputPath(page.path, outputStyle)
|
|
1873
|
+
: routePathToOutputPath(page.url, outputStyle);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1582
1876
|
function applyPermalinkPattern(pattern, kind, item, site) {
|
|
1583
1877
|
const tokenValues = buildPermalinkTokenValues(kind, item, site);
|
|
1584
1878
|
const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
|
|
@@ -1635,7 +1929,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1635
1929
|
}
|
|
1636
1930
|
|
|
1637
1931
|
for (const page of state.renderData.pages) {
|
|
1638
|
-
assertSafeSlugDerivedOutputPath(page.slug,
|
|
1932
|
+
assertSafeSlugDerivedOutputPath(page.slug, pageToOutputPath(page, outputStyle));
|
|
1639
1933
|
}
|
|
1640
1934
|
|
|
1641
1935
|
for (const category of state.previewData.content.categories) {
|
|
@@ -1647,6 +1941,10 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1647
1941
|
}
|
|
1648
1942
|
|
|
1649
1943
|
const routeEntries = [
|
|
1944
|
+
...(state.renderData.frontPageRoute ? [{
|
|
1945
|
+
url: '/',
|
|
1946
|
+
outputPath: 'index.html',
|
|
1947
|
+
}] : []),
|
|
1650
1948
|
...state.renderData.indexRoutes.map((route) => ({
|
|
1651
1949
|
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1652
1950
|
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
@@ -1669,7 +1967,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1669
1967
|
})),
|
|
1670
1968
|
...state.renderData.pages.map((page) => ({
|
|
1671
1969
|
url: page.url,
|
|
1672
|
-
outputPath:
|
|
1970
|
+
outputPath: pageToOutputPath(page, outputStyle),
|
|
1673
1971
|
})),
|
|
1674
1972
|
];
|
|
1675
1973
|
assertUniqueRoutes(routeEntries);
|
|
@@ -1811,12 +2109,19 @@ function injectCustomHtml(html, customHtml) {
|
|
|
1811
2109
|
|
|
1812
2110
|
function buildSitemapXml(site, emitted, generatedAt) {
|
|
1813
2111
|
const entries = [
|
|
2112
|
+
...(emitted.frontPage
|
|
2113
|
+
? [{
|
|
2114
|
+
url: emitted.frontPage.url,
|
|
2115
|
+
changefreq: 'daily',
|
|
2116
|
+
priority: 1.0,
|
|
2117
|
+
}]
|
|
2118
|
+
: []),
|
|
1814
2119
|
...emitted.indexRoutes
|
|
1815
|
-
.filter((route) => route.
|
|
2120
|
+
.filter((route) => route.page === 1)
|
|
1816
2121
|
.map((route) => ({
|
|
1817
2122
|
url: route.url,
|
|
1818
2123
|
changefreq: 'daily',
|
|
1819
|
-
priority: 1.0,
|
|
2124
|
+
priority: route.url === '/' ? 1.0 : 0.7,
|
|
1820
2125
|
})),
|
|
1821
2126
|
...emitted.posts.map((post) => ({
|
|
1822
2127
|
url: post.url,
|
|
@@ -1824,11 +2129,13 @@ function buildSitemapXml(site, emitted, generatedAt) {
|
|
|
1824
2129
|
changefreq: 'weekly',
|
|
1825
2130
|
priority: 0.8,
|
|
1826
2131
|
})),
|
|
1827
|
-
...emitted.pages
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2132
|
+
...emitted.pages
|
|
2133
|
+
.filter((page) => page.includeInSitemap !== false)
|
|
2134
|
+
.map((page) => ({
|
|
2135
|
+
url: page.url,
|
|
2136
|
+
changefreq: 'monthly',
|
|
2137
|
+
priority: 0.7,
|
|
2138
|
+
})),
|
|
1832
2139
|
];
|
|
1833
2140
|
|
|
1834
2141
|
const body = entries.map((entry) => {
|
|
@@ -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
|
});
|