@zeropress/build-core 0.2.1 → 0.2.2
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 +2 -2
- package/src/api/build-site.js +208 -37
- package/src/render/content-renderer.js +67 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Shared ZeroPress build core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@zeropress/slug-policy": "^0.1.0",
|
|
37
|
-
"@zeropress/preview-data-validator": "^0.4.
|
|
37
|
+
"@zeropress/preview-data-validator": "^0.4.5",
|
|
38
38
|
"@zeropress/theme-validator": "^0.5.0",
|
|
39
39
|
"highlight.js": "^11.11.1",
|
|
40
40
|
"markdown-it": "^14.1.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:']);
|
|
@@ -154,7 +162,7 @@ async function finalizeBuildResult(writer, summaries, options) {
|
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
async function renderRoute(state, templateName, route) {
|
|
157
|
-
const currentUrl =
|
|
165
|
+
const currentUrl = routePathToPublicUrl(route.path, state.previewData.site.permalinks.output_style);
|
|
158
166
|
let html = await state.engine.render(
|
|
159
167
|
templateName,
|
|
160
168
|
{
|
|
@@ -172,12 +180,12 @@ async function renderRoute(state, templateName, route) {
|
|
|
172
180
|
);
|
|
173
181
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
174
182
|
html = injectCustomCssAssetLink(html, state.customCssHref);
|
|
175
|
-
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path), html, 'text/html');
|
|
183
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(route.path, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
176
184
|
recordRouteEmission(state, templateName, route, currentUrl);
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
async function renderPost(state, post) {
|
|
180
|
-
const currentUrl =
|
|
188
|
+
const currentUrl = post.url;
|
|
181
189
|
let html = await state.engine.render(
|
|
182
190
|
'post',
|
|
183
191
|
{
|
|
@@ -198,7 +206,7 @@ async function renderPost(state, post) {
|
|
|
198
206
|
);
|
|
199
207
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
200
208
|
html = injectCustomCssAssetLink(html, state.customCssHref);
|
|
201
|
-
await writeOutput(state.writer, state.summaries,
|
|
209
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
202
210
|
state.emitted.posts.push({
|
|
203
211
|
url: currentUrl,
|
|
204
212
|
title: post.title,
|
|
@@ -210,7 +218,7 @@ async function renderPost(state, post) {
|
|
|
210
218
|
}
|
|
211
219
|
|
|
212
220
|
async function renderPage(state, page) {
|
|
213
|
-
const currentUrl =
|
|
221
|
+
const currentUrl = page.url;
|
|
214
222
|
let html = await state.engine.render(
|
|
215
223
|
'page',
|
|
216
224
|
{
|
|
@@ -229,7 +237,7 @@ async function renderPage(state, page) {
|
|
|
229
237
|
);
|
|
230
238
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
231
239
|
html = injectCustomCssAssetLink(html, state.customCssHref);
|
|
232
|
-
await writeOutput(state.writer, state.summaries,
|
|
240
|
+
await writeOutput(state.writer, state.summaries, routePathToOutputPath(page.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
233
241
|
state.emitted.pages.push({
|
|
234
242
|
url: currentUrl,
|
|
235
243
|
title: page.title,
|
|
@@ -274,6 +282,7 @@ function normalizePreviewData(previewData) {
|
|
|
274
282
|
timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
|
|
275
283
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
276
284
|
disallowComments: previewData.site.disallowComments === true,
|
|
285
|
+
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
277
286
|
};
|
|
278
287
|
|
|
279
288
|
return {
|
|
@@ -348,6 +357,21 @@ function normalizeCustomCss(customCss) {
|
|
|
348
357
|
return content ? { content } : undefined;
|
|
349
358
|
}
|
|
350
359
|
|
|
360
|
+
function normalizePermalinks(permalinks) {
|
|
361
|
+
const source = permalinks && typeof permalinks === 'object' ? permalinks : {};
|
|
362
|
+
const outputStyle = typeof source.output_style === 'string' && PERMALINK_OUTPUT_STYLES.has(source.output_style)
|
|
363
|
+
? source.output_style
|
|
364
|
+
: DEFAULT_PERMALINKS.output_style;
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
output_style: outputStyle,
|
|
368
|
+
posts: normalizeNonEmptyString(source.posts, DEFAULT_PERMALINKS.posts),
|
|
369
|
+
pages: normalizeNonEmptyString(source.pages, DEFAULT_PERMALINKS.pages),
|
|
370
|
+
categories: normalizeNonEmptyString(source.categories, DEFAULT_PERMALINKS.categories),
|
|
371
|
+
tags: normalizeNonEmptyString(source.tags, DEFAULT_PERMALINKS.tags),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
351
375
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
352
376
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
353
377
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
@@ -376,7 +400,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
376
400
|
prev: index > 0 ? buildAdjacentPostSummary(preparedPosts[index - 1]) : null,
|
|
377
401
|
next: index < preparedPosts.length - 1 ? buildAdjacentPostSummary(preparedPosts[index + 1]) : null,
|
|
378
402
|
}));
|
|
379
|
-
const pages = previewData.content.pages.map((page) => preparePage(page));
|
|
403
|
+
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
380
404
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
381
405
|
|
|
382
406
|
return {
|
|
@@ -387,6 +411,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
387
411
|
items: previewData.content.posts,
|
|
388
412
|
postsPerPage: previewData.site.postsPerPage,
|
|
389
413
|
basePath: '/',
|
|
414
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
390
415
|
}).map((entry) => ({
|
|
391
416
|
path: entry.path,
|
|
392
417
|
page: entry.page,
|
|
@@ -398,6 +423,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
398
423
|
items: previewData.content.posts,
|
|
399
424
|
postsPerPage: previewData.site.postsPerPage,
|
|
400
425
|
basePath: '/archive/',
|
|
426
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
401
427
|
}).map((entry) => ({
|
|
402
428
|
path: entry.path,
|
|
403
429
|
page: entry.page,
|
|
@@ -412,7 +438,8 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
412
438
|
postsBySlug: categoryPostsBySlug,
|
|
413
439
|
postBySlug,
|
|
414
440
|
postsPerPage: previewData.site.postsPerPage,
|
|
415
|
-
|
|
441
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
442
|
+
buildBasePath: (category) => resolvePermalink(previewData.site, 'categories', category).path,
|
|
416
443
|
renderExtras: (category) => ({
|
|
417
444
|
taxonomy: buildTaxonomyRouteData('category', category, categoryCountBySlug),
|
|
418
445
|
}),
|
|
@@ -422,7 +449,8 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
422
449
|
postsBySlug: tagPostsBySlug,
|
|
423
450
|
postBySlug,
|
|
424
451
|
postsPerPage: previewData.site.postsPerPage,
|
|
425
|
-
|
|
452
|
+
outputStyle: previewData.site.permalinks.output_style,
|
|
453
|
+
buildBasePath: (tag) => resolvePermalink(previewData.site, 'tags', tag).path,
|
|
426
454
|
renderExtras: (tag) => ({
|
|
427
455
|
taxonomy: buildTaxonomyRouteData('tag', tag, tagCountBySlug),
|
|
428
456
|
}),
|
|
@@ -521,7 +549,7 @@ function resolveCategoriesWidget(baseWidget, settings, previewData) {
|
|
|
521
549
|
.map((category) => ({
|
|
522
550
|
name: category.name,
|
|
523
551
|
slug: category.slug,
|
|
524
|
-
url:
|
|
552
|
+
url: resolvePermalink(previewData.site, 'categories', category).url,
|
|
525
553
|
count: countBySlug.get(category.slug) || 0,
|
|
526
554
|
depth: 0,
|
|
527
555
|
}))
|
|
@@ -546,7 +574,7 @@ function resolveTagsWidget(baseWidget, settings, previewData) {
|
|
|
546
574
|
.map((tag) => ({
|
|
547
575
|
name: tag.name,
|
|
548
576
|
slug: tag.slug,
|
|
549
|
-
url:
|
|
577
|
+
url: resolvePermalink(previewData.site, 'tags', tag).url,
|
|
550
578
|
count: countBySlug.get(tag.slug) || 0,
|
|
551
579
|
}))
|
|
552
580
|
.filter((tag) => tag.count > 0)
|
|
@@ -569,7 +597,7 @@ function resolveArchivesWidget(baseWidget, settings, previewData) {
|
|
|
569
597
|
.slice(0, limit)
|
|
570
598
|
.map((entry) => ({
|
|
571
599
|
label: entry.label,
|
|
572
|
-
url: '/archive/',
|
|
600
|
+
url: routePathToPublicUrl('/archive/', previewData.site.permalinks.output_style),
|
|
573
601
|
count: entry.count,
|
|
574
602
|
year: entry.year,
|
|
575
603
|
month: entry.month,
|
|
@@ -657,27 +685,32 @@ function resolveProfileWidget(baseWidget, settings) {
|
|
|
657
685
|
};
|
|
658
686
|
}
|
|
659
687
|
|
|
660
|
-
function preparePage(page) {
|
|
688
|
+
function preparePage(page, site) {
|
|
661
689
|
const documentType = normalizeDocumentType(page.document_type);
|
|
690
|
+
const renderedDocument = renderDocument(page.content, documentType);
|
|
691
|
+
const permalink = resolvePagePermalink(site, page);
|
|
662
692
|
|
|
663
693
|
return {
|
|
664
694
|
...page,
|
|
695
|
+
url: permalink.url,
|
|
665
696
|
document_type: documentType,
|
|
666
|
-
html:
|
|
697
|
+
html: renderedDocument.html,
|
|
698
|
+
toc: renderedDocument.toc,
|
|
667
699
|
};
|
|
668
700
|
}
|
|
669
701
|
|
|
670
702
|
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments) {
|
|
671
703
|
const documentType = normalizeDocumentType(post.document_type);
|
|
672
|
-
const
|
|
704
|
+
const renderedDocument = renderDocument(post.content, documentType);
|
|
673
705
|
const author = authorsById.get(post.author_id);
|
|
706
|
+
const permalink = resolvePermalink(site, 'posts', post);
|
|
674
707
|
const categories = post.category_slugs
|
|
675
708
|
.map((slug) => categoriesBySlug.get(slug))
|
|
676
709
|
.filter(Boolean)
|
|
677
710
|
.map((category) => ({
|
|
678
711
|
name: category.name,
|
|
679
712
|
slug: category.slug,
|
|
680
|
-
url:
|
|
713
|
+
url: resolvePermalink(site, 'categories', category).url,
|
|
681
714
|
}));
|
|
682
715
|
const tags = post.tag_slugs
|
|
683
716
|
.map((slug) => tagsBySlug.get(slug))
|
|
@@ -685,14 +718,14 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
685
718
|
.map((tag) => ({
|
|
686
719
|
name: tag.name,
|
|
687
720
|
slug: tag.slug,
|
|
688
|
-
url:
|
|
721
|
+
url: resolvePermalink(site, 'tags', tag).url,
|
|
689
722
|
}));
|
|
690
723
|
|
|
691
724
|
return {
|
|
692
725
|
public_id: post.public_id,
|
|
693
726
|
title: post.title,
|
|
694
727
|
slug: post.slug,
|
|
695
|
-
url:
|
|
728
|
+
url: permalink.url,
|
|
696
729
|
content: post.content,
|
|
697
730
|
document_type: documentType,
|
|
698
731
|
excerpt: post.excerpt,
|
|
@@ -700,6 +733,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
700
733
|
updated_at_iso: post.updated_at_iso,
|
|
701
734
|
author_id: post.author_id,
|
|
702
735
|
featured_image: post.featured_image,
|
|
736
|
+
meta: post.meta,
|
|
703
737
|
status: post.status,
|
|
704
738
|
allow_comments: post.allow_comments,
|
|
705
739
|
category_slugs: post.category_slugs,
|
|
@@ -711,10 +745,11 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
711
745
|
},
|
|
712
746
|
categories,
|
|
713
747
|
tags,
|
|
714
|
-
html,
|
|
748
|
+
html: renderedDocument.html,
|
|
749
|
+
toc: renderedDocument.toc,
|
|
715
750
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
716
751
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
717
|
-
reading_time: calculateReadingTime(html),
|
|
752
|
+
reading_time: calculateReadingTime(renderedDocument.html),
|
|
718
753
|
comments_enabled: themeSupportsComments && site.disallowComments !== true && post.allow_comments === true,
|
|
719
754
|
};
|
|
720
755
|
}
|
|
@@ -744,6 +779,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
744
779
|
items: matchedPosts,
|
|
745
780
|
postsPerPage: options.postsPerPage,
|
|
746
781
|
basePath: options.buildBasePath(item),
|
|
782
|
+
outputStyle: options.outputStyle,
|
|
747
783
|
});
|
|
748
784
|
|
|
749
785
|
for (const entry of paginated) {
|
|
@@ -764,6 +800,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
764
800
|
|
|
765
801
|
function buildPaginatedCollection(options) {
|
|
766
802
|
const basePath = normalizePaginationBasePath(options.basePath);
|
|
803
|
+
const outputStyle = PERMALINK_OUTPUT_STYLES.has(options.outputStyle) ? options.outputStyle : DEFAULT_PERMALINKS.output_style;
|
|
767
804
|
const postsPerPage = Number.isInteger(options.postsPerPage) && options.postsPerPage > 0 ? options.postsPerPage : DEFAULT_POSTS_PER_PAGE;
|
|
768
805
|
const totalPosts = options.items.length;
|
|
769
806
|
const totalPages = Math.max(1, Math.ceil(totalPosts / postsPerPage));
|
|
@@ -778,7 +815,7 @@ function buildPaginatedCollection(options) {
|
|
|
778
815
|
page,
|
|
779
816
|
totalPages,
|
|
780
817
|
items: options.items.slice(start, end),
|
|
781
|
-
paginationData: buildPaginationData(page, totalPages, totalPosts, basePath),
|
|
818
|
+
paginationData: buildPaginationData(page, totalPages, totalPosts, basePath, outputStyle),
|
|
782
819
|
});
|
|
783
820
|
}
|
|
784
821
|
|
|
@@ -811,20 +848,22 @@ function normalizePaginationBasePath(basePath) {
|
|
|
811
848
|
return `/${normalized}/`;
|
|
812
849
|
}
|
|
813
850
|
|
|
814
|
-
function buildPaginationData(currentPage, totalPages, totalPosts, basePath) {
|
|
851
|
+
function buildPaginationData(currentPage, totalPages, totalPosts, basePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
852
|
+
const buildPageUrl = (page) => routePathToPublicUrl(buildPaginatedPath(basePath, page), outputStyle);
|
|
853
|
+
|
|
815
854
|
return {
|
|
816
855
|
currentPage,
|
|
817
856
|
totalPages,
|
|
818
857
|
totalPosts,
|
|
819
858
|
hasNext: currentPage < totalPages,
|
|
820
859
|
hasPrev: currentPage > 1,
|
|
821
|
-
nextUrl: currentPage < totalPages ?
|
|
822
|
-
prevUrl: currentPage > 1 ?
|
|
860
|
+
nextUrl: currentPage < totalPages ? buildPageUrl(currentPage + 1) : undefined,
|
|
861
|
+
prevUrl: currentPage > 1 ? buildPageUrl(currentPage - 1) : undefined,
|
|
823
862
|
pages: Array.from({ length: totalPages }, (_, index) => {
|
|
824
863
|
const page = index + 1;
|
|
825
864
|
return {
|
|
826
865
|
number: page,
|
|
827
|
-
url:
|
|
866
|
+
url: buildPageUrl(page),
|
|
828
867
|
current: page === currentPage,
|
|
829
868
|
};
|
|
830
869
|
}),
|
|
@@ -1433,14 +1472,28 @@ async function writeOutput(writer, summaries, path, content, contentType) {
|
|
|
1433
1472
|
});
|
|
1434
1473
|
}
|
|
1435
1474
|
|
|
1436
|
-
function routePathToOutputPath(routePath) {
|
|
1475
|
+
function routePathToOutputPath(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
1437
1476
|
const normalizedPath = normalizeRoutePath(routePath);
|
|
1438
1477
|
if (normalizedPath === '/') {
|
|
1439
1478
|
return 'index.html';
|
|
1440
1479
|
}
|
|
1480
|
+
if (outputStyle === 'html-extension') {
|
|
1481
|
+
return `${normalizedPath.replace(/^\/+|\/+$/g, '')}.html`;
|
|
1482
|
+
}
|
|
1441
1483
|
return `${normalizedPath.replace(/^\//, '')}index.html`;
|
|
1442
1484
|
}
|
|
1443
1485
|
|
|
1486
|
+
function routePathToPublicUrl(routePath, outputStyle = DEFAULT_PERMALINKS.output_style) {
|
|
1487
|
+
const normalizedPath = normalizeRoutePath(routePath);
|
|
1488
|
+
if (normalizedPath === '/') {
|
|
1489
|
+
return '/';
|
|
1490
|
+
}
|
|
1491
|
+
if (outputStyle === 'html-extension') {
|
|
1492
|
+
return normalizedPath.replace(/\/$/, '');
|
|
1493
|
+
}
|
|
1494
|
+
return normalizedPath;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1444
1497
|
function normalizeRoutePath(routePath) {
|
|
1445
1498
|
if (!routePath || routePath === '/') {
|
|
1446
1499
|
return '/';
|
|
@@ -1449,34 +1502,124 @@ function normalizeRoutePath(routePath) {
|
|
|
1449
1502
|
return `/${normalized}/`;
|
|
1450
1503
|
}
|
|
1451
1504
|
|
|
1505
|
+
function resolvePagePermalink(site, page) {
|
|
1506
|
+
if (normalizeOptionalString(page.path)) {
|
|
1507
|
+
return buildRouteInfo(page.path, site.permalinks.output_style);
|
|
1508
|
+
}
|
|
1509
|
+
return resolvePermalink(site, 'pages', page);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function resolvePermalink(site, kind, item) {
|
|
1513
|
+
const pattern = normalizeNonEmptyString(site.permalinks?.[kind], DEFAULT_PERMALINKS[kind]);
|
|
1514
|
+
return buildRouteInfo(applyPermalinkPattern(pattern, kind, item, site), site.permalinks.output_style);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function buildRouteInfo(routePath, outputStyle) {
|
|
1518
|
+
const path = normalizeRoutePath(routePath);
|
|
1519
|
+
return {
|
|
1520
|
+
path,
|
|
1521
|
+
url: routePathToPublicUrl(path, outputStyle),
|
|
1522
|
+
outputPath: routePathToOutputPath(path, outputStyle),
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function applyPermalinkPattern(pattern, kind, item, site) {
|
|
1527
|
+
const tokenValues = buildPermalinkTokenValues(kind, item, site);
|
|
1528
|
+
const body = String(pattern || '').replace(/^\/+|\/+$/g, '');
|
|
1529
|
+
const segments = body.split('/').filter(Boolean).map((segment) => {
|
|
1530
|
+
if (segment.startsWith(':')) {
|
|
1531
|
+
return tokenValues[segment.slice(1)] || '';
|
|
1532
|
+
}
|
|
1533
|
+
return segment;
|
|
1534
|
+
});
|
|
1535
|
+
return `/${segments.join('/')}/`;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function buildPermalinkTokenValues(kind, item, site) {
|
|
1539
|
+
const values = {
|
|
1540
|
+
slug: encodeSlugSegment(item.slug),
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
if (kind === 'posts') {
|
|
1544
|
+
const parts = getPermalinkDateParts(item.published_at_iso, site);
|
|
1545
|
+
values.public_id = String(item.public_id);
|
|
1546
|
+
values.year = parts.year;
|
|
1547
|
+
values.month = parts.month;
|
|
1548
|
+
values.day = parts.day;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return values;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function getPermalinkDateParts(value, site) {
|
|
1555
|
+
const date = toDate(value);
|
|
1556
|
+
const timeZone = normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE);
|
|
1557
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
1558
|
+
timeZone,
|
|
1559
|
+
year: 'numeric',
|
|
1560
|
+
month: '2-digit',
|
|
1561
|
+
day: '2-digit',
|
|
1562
|
+
}).formatToParts(date);
|
|
1563
|
+
|
|
1564
|
+
return {
|
|
1565
|
+
year: parts.find((part) => part.type === 'year')?.value || String(date.getUTCFullYear()),
|
|
1566
|
+
month: parts.find((part) => part.type === 'month')?.value || String(date.getUTCMonth() + 1).padStart(2, '0'),
|
|
1567
|
+
day: parts.find((part) => part.type === 'day')?.value || String(date.getUTCDate()).padStart(2, '0'),
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1452
1571
|
function normalizeOutputPath(filePath) {
|
|
1453
1572
|
return String(filePath || '').replace(/^\/+/, '');
|
|
1454
1573
|
}
|
|
1455
1574
|
|
|
1456
1575
|
function assertPlannedOutputPathsSafe(state) {
|
|
1576
|
+
const outputStyle = state.previewData.site.permalinks.output_style;
|
|
1457
1577
|
for (const post of state.renderData.posts) {
|
|
1458
|
-
assertSafeSlugDerivedOutputPath(post.slug,
|
|
1578
|
+
assertSafeSlugDerivedOutputPath(post.slug, routePathToOutputPath(post.url, outputStyle));
|
|
1459
1579
|
}
|
|
1460
1580
|
|
|
1461
1581
|
for (const page of state.renderData.pages) {
|
|
1462
|
-
assertSafeSlugDerivedOutputPath(page.slug,
|
|
1582
|
+
assertSafeSlugDerivedOutputPath(page.slug, routePathToOutputPath(page.url, outputStyle));
|
|
1463
1583
|
}
|
|
1464
1584
|
|
|
1465
1585
|
for (const category of state.previewData.content.categories) {
|
|
1466
|
-
assertSafeSlugDerivedOutputPath(category.slug,
|
|
1586
|
+
assertSafeSlugDerivedOutputPath(category.slug, resolvePermalink(state.previewData.site, 'categories', category).outputPath);
|
|
1467
1587
|
}
|
|
1468
1588
|
|
|
1469
1589
|
for (const tag of state.previewData.content.tags) {
|
|
1470
|
-
assertSafeSlugDerivedOutputPath(tag.slug,
|
|
1590
|
+
assertSafeSlugDerivedOutputPath(tag.slug, resolvePermalink(state.previewData.site, 'tags', tag).outputPath);
|
|
1471
1591
|
}
|
|
1472
1592
|
|
|
1593
|
+
const routeEntries = [
|
|
1594
|
+
...state.renderData.indexRoutes.map((route) => ({
|
|
1595
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1596
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1597
|
+
})),
|
|
1598
|
+
...state.renderData.archiveRoutes.map((route) => ({
|
|
1599
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1600
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1601
|
+
})),
|
|
1602
|
+
...state.renderData.categoryRoutes.map((route) => ({
|
|
1603
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1604
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1605
|
+
})),
|
|
1606
|
+
...state.renderData.tagRoutes.map((route) => ({
|
|
1607
|
+
url: routePathToPublicUrl(route.path, outputStyle),
|
|
1608
|
+
outputPath: routePathToOutputPath(route.path, outputStyle),
|
|
1609
|
+
})),
|
|
1610
|
+
...state.renderData.posts.map((post) => ({
|
|
1611
|
+
url: post.url,
|
|
1612
|
+
outputPath: routePathToOutputPath(post.url, outputStyle),
|
|
1613
|
+
})),
|
|
1614
|
+
...state.renderData.pages.map((page) => ({
|
|
1615
|
+
url: page.url,
|
|
1616
|
+
outputPath: routePathToOutputPath(page.url, outputStyle),
|
|
1617
|
+
})),
|
|
1618
|
+
];
|
|
1619
|
+
assertUniqueRoutes(routeEntries);
|
|
1620
|
+
|
|
1473
1621
|
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`),
|
|
1622
|
+
...routeEntries.map((entry) => entry.outputPath),
|
|
1480
1623
|
...state.assetOutputs.map((assetOutput) => assetOutput.path),
|
|
1481
1624
|
COMMENT_POLICY_OUTPUT_PATH,
|
|
1482
1625
|
];
|
|
@@ -1493,6 +1636,34 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
1493
1636
|
const normalizedPath = normalizeOutputPath(rawPath);
|
|
1494
1637
|
assertSafeRelativeOutputPath(rawPath, normalizedPath);
|
|
1495
1638
|
}
|
|
1639
|
+
|
|
1640
|
+
assertUniqueOutputPaths(plannedPaths);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function assertUniqueRoutes(routeEntries) {
|
|
1644
|
+
const seenUrls = new Map();
|
|
1645
|
+
for (const entry of routeEntries) {
|
|
1646
|
+
const normalizedUrl = normalizeRouteCollisionKey(entry.url);
|
|
1647
|
+
if (seenUrls.has(normalizedUrl)) {
|
|
1648
|
+
throw new Error(`Duplicate public URL detected: ${entry.url}`);
|
|
1649
|
+
}
|
|
1650
|
+
seenUrls.set(normalizedUrl, entry);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function assertUniqueOutputPaths(plannedPaths) {
|
|
1655
|
+
const seenPaths = new Set();
|
|
1656
|
+
for (const plannedPath of plannedPaths) {
|
|
1657
|
+
const normalizedPath = normalizeOutputPath(plannedPath);
|
|
1658
|
+
if (seenPaths.has(normalizedPath)) {
|
|
1659
|
+
throw new Error(`Duplicate output path detected: ${plannedPath}`);
|
|
1660
|
+
}
|
|
1661
|
+
seenPaths.add(normalizedPath);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function normalizeRouteCollisionKey(url) {
|
|
1666
|
+
return String(url || '').replace(/\/+$/, '') || '/';
|
|
1496
1667
|
}
|
|
1497
1668
|
|
|
1498
1669
|
function assertSafeSlugDerivedOutputPath(rawSlug, outputPath) {
|
|
@@ -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) {
|