@zeropress/build-core 0.1.9 → 0.1.10
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 +6 -3
- package/src/api/build-site.js +34 -5
- package/src/index.d.ts +4 -4
- package/src/render/content-renderer.js +156 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Shared ZeroPress build and partial-render core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"node": ">=18.18.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zeropress/preview-data-validator": "^0.
|
|
37
|
-
"@zeropress/theme-validator": "^0.2.4"
|
|
36
|
+
"@zeropress/preview-data-validator": "^0.4.0",
|
|
37
|
+
"@zeropress/theme-validator": "^0.2.4",
|
|
38
|
+
"highlight.js": "^11.11.1",
|
|
39
|
+
"markdown-it": "^14.1.0",
|
|
40
|
+
"markdown-it-anchor": "^9.2.0"
|
|
38
41
|
}
|
|
39
42
|
}
|
package/src/api/build-site.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { assertPreviewData } from '@zeropress/preview-data-validator';
|
|
3
3
|
import { validateThemeFiles } from '@zeropress/theme-validator';
|
|
4
4
|
import { AssetProcessor } from '../assets/asset-processor.js';
|
|
5
|
+
import { renderDocumentContent } from '../render/content-renderer.js';
|
|
5
6
|
import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_OPTIONS = {
|
|
@@ -28,7 +29,7 @@ export async function buildSite(input) {
|
|
|
28
29
|
await renderPost(state, post);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
for (const page of state.
|
|
32
|
+
for (const page of state.renderData.pages) {
|
|
32
33
|
await renderPage(state, page);
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -324,12 +325,15 @@ function normalizePreviewData(previewData) {
|
|
|
324
325
|
site: normalizedSite,
|
|
325
326
|
content: {
|
|
326
327
|
...previewData.content,
|
|
328
|
+
authors: previewData.content.authors.map((author) => ({
|
|
329
|
+
...author,
|
|
330
|
+
avatar: normalizeMediaField(author.avatar, normalizedSite.mediaBaseUrl),
|
|
331
|
+
})),
|
|
327
332
|
posts: previewData.content.posts
|
|
328
333
|
.map((post) => ({
|
|
329
334
|
...post,
|
|
330
335
|
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
331
336
|
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
332
|
-
author_avatar: normalizeMediaField(post.author_avatar, normalizedSite.mediaBaseUrl),
|
|
333
337
|
featured_image: normalizeMediaField(post.featured_image, normalizedSite.mediaBaseUrl),
|
|
334
338
|
}))
|
|
335
339
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
@@ -344,6 +348,7 @@ function normalizePreviewData(previewData) {
|
|
|
344
348
|
}
|
|
345
349
|
|
|
346
350
|
function createRenderData(previewData) {
|
|
351
|
+
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
347
352
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
348
353
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
349
354
|
const categoryPostsBySlug = new Map();
|
|
@@ -363,13 +368,15 @@ function createRenderData(previewData) {
|
|
|
363
368
|
}
|
|
364
369
|
}
|
|
365
370
|
|
|
366
|
-
const posts = previewData.content.posts.map((post) => preparePost(post, previewData.site, categoriesBySlug, tagsBySlug));
|
|
371
|
+
const posts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug));
|
|
372
|
+
const pages = previewData.content.pages.map((page) => preparePage(page));
|
|
367
373
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
368
374
|
const categoryLinks = renderCategoryLinks(previewData.content.categories, categoryCountBySlug);
|
|
369
375
|
const tagLinks = renderTagLinks(previewData.content.tags, tagCountBySlug);
|
|
370
376
|
|
|
371
377
|
return {
|
|
372
378
|
posts,
|
|
379
|
+
pages,
|
|
373
380
|
postBySlug,
|
|
374
381
|
indexRoutes: buildPaginatedCollection({
|
|
375
382
|
items: previewData.content.posts,
|
|
@@ -414,12 +421,30 @@ function createRenderData(previewData) {
|
|
|
414
421
|
};
|
|
415
422
|
}
|
|
416
423
|
|
|
417
|
-
function
|
|
424
|
+
function preparePage(page) {
|
|
425
|
+
const documentType = normalizeDocumentType(page.document_type);
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
...page,
|
|
429
|
+
document_type: documentType,
|
|
430
|
+
html: renderDocumentContent(page.content, documentType),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug) {
|
|
435
|
+
const documentType = normalizeDocumentType(post.document_type);
|
|
436
|
+
const html = renderDocumentContent(post.content, documentType);
|
|
437
|
+
const author = authorsById.get(post.author_id);
|
|
438
|
+
|
|
418
439
|
return {
|
|
419
440
|
...post,
|
|
441
|
+
document_type: documentType,
|
|
442
|
+
html,
|
|
443
|
+
author_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
444
|
+
author_avatar: author?.avatar,
|
|
420
445
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
421
446
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
422
|
-
reading_time: calculateReadingTime(
|
|
447
|
+
reading_time: calculateReadingTime(html),
|
|
423
448
|
categories_html: renderInlineTaxonomyLinks(post.category_slugs, categoriesBySlug, 'category'),
|
|
424
449
|
tags_html: renderInlineTaxonomyLinks(post.tag_slugs, tagsBySlug, 'tag'),
|
|
425
450
|
comments_html: renderCommentsContainer(site, post),
|
|
@@ -776,6 +801,10 @@ function normalizeLocale(value) {
|
|
|
776
801
|
}).join('-');
|
|
777
802
|
}
|
|
778
803
|
|
|
804
|
+
function normalizeDocumentType(value) {
|
|
805
|
+
return value === 'plaintext' || value === 'html' ? value : 'markdown';
|
|
806
|
+
}
|
|
807
|
+
|
|
779
808
|
function hasTemplate(state, templateName) {
|
|
780
809
|
return state.engine.themePackage?.templates?.has(templateName) === true;
|
|
781
810
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PreviewDataV05 } from '@zeropress/preview-data-validator';
|
|
2
2
|
|
|
3
3
|
export interface ThemePackage {
|
|
4
4
|
metadata: {
|
|
@@ -62,14 +62,14 @@ export interface BuildSiteResult {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function buildSite(input: {
|
|
65
|
-
previewData:
|
|
65
|
+
previewData: PreviewDataV05;
|
|
66
66
|
themePackage: ThemePackage;
|
|
67
67
|
writer: BuildWriter;
|
|
68
68
|
options?: BuildOptions;
|
|
69
69
|
}): Promise<BuildSiteResult>;
|
|
70
70
|
|
|
71
71
|
export function buildSelectedRoutes(input: {
|
|
72
|
-
previewData:
|
|
72
|
+
previewData: PreviewDataV05;
|
|
73
73
|
themePackage: ThemePackage;
|
|
74
74
|
writer: BuildWriter;
|
|
75
75
|
selection: BuildSelection;
|
|
@@ -77,7 +77,7 @@ export function buildSelectedRoutes(input: {
|
|
|
77
77
|
}): Promise<BuildSiteResult>;
|
|
78
78
|
|
|
79
79
|
export function buildSiteFromThemeDir(input: {
|
|
80
|
-
previewData:
|
|
80
|
+
previewData: PreviewDataV05;
|
|
81
81
|
themeDir: string;
|
|
82
82
|
writer: BuildWriter;
|
|
83
83
|
options?: BuildOptions;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
import hljs from 'highlight.js';
|
|
3
|
+
import anchor from 'markdown-it-anchor';
|
|
4
|
+
|
|
5
|
+
const markdown = new MarkdownIt({
|
|
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
|
+
});
|
|
31
|
+
|
|
32
|
+
export function renderDocumentContent(content, documentType = 'markdown') {
|
|
33
|
+
const normalizedContent = typeof content === 'string' ? content : '';
|
|
34
|
+
const normalizedType = normalizeDocumentType(documentType);
|
|
35
|
+
|
|
36
|
+
if (normalizedType === 'plaintext') {
|
|
37
|
+
return transformPlaintext(normalizedContent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (normalizedType === 'html') {
|
|
41
|
+
return sanitizeHtml(normalizedContent);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return sanitizeHtml(markdown.render(normalizedContent));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeDocumentType(value) {
|
|
48
|
+
return value === 'plaintext' || value === 'html' ? value : 'markdown';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function transformPlaintext(content) {
|
|
52
|
+
const escaped = content
|
|
53
|
+
.replace(/&/g, '&')
|
|
54
|
+
.replace(/</g, '<')
|
|
55
|
+
.replace(/>/g, '>')
|
|
56
|
+
.replace(/"/g, '"')
|
|
57
|
+
.replace(/'/g, ''');
|
|
58
|
+
|
|
59
|
+
const paragraphs = escaped
|
|
60
|
+
.split(/\n\s*\n/)
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
|
|
64
|
+
if (paragraphs.length === 0) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return paragraphs.map((entry) => `<p>${entry}</p>`).join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sanitizeHtml(html) {
|
|
72
|
+
const allowedTags = new Set([
|
|
73
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
74
|
+
'p', 'br', 'hr',
|
|
75
|
+
'strong', 'em', 'u', 's', 'code', 'pre',
|
|
76
|
+
'a', 'img',
|
|
77
|
+
'ul', 'ol', 'li',
|
|
78
|
+
'blockquote',
|
|
79
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
80
|
+
'div', 'span', 'nav',
|
|
81
|
+
'iframe',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const allowedAttributes = {
|
|
85
|
+
a: new Set(['href', 'title', 'class', 'id']),
|
|
86
|
+
img: new Set(['src', 'alt', 'title', 'class', 'id', 'width', 'height']),
|
|
87
|
+
iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class']),
|
|
88
|
+
'*': new Set(['class', 'id']),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const safeUriPattern = /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
92
|
+
|
|
93
|
+
const stripped = html
|
|
94
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
95
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
96
|
+
|
|
97
|
+
const sanitized = stripped.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)?\/?>/g, (match, tag, attributeString) => {
|
|
98
|
+
const normalizedTag = tag.toLowerCase();
|
|
99
|
+
|
|
100
|
+
if (match.startsWith('</')) {
|
|
101
|
+
return allowedTags.has(normalizedTag) ? `</${normalizedTag}>` : '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!allowedTags.has(normalizedTag)) {
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tagAllowedAttributes = allowedAttributes[normalizedTag] || new Set();
|
|
109
|
+
const globalAllowedAttributes = allowedAttributes['*'];
|
|
110
|
+
const filteredAttributes = [];
|
|
111
|
+
|
|
112
|
+
if (attributeString) {
|
|
113
|
+
const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
114
|
+
let attributeMatch;
|
|
115
|
+
|
|
116
|
+
while ((attributeMatch = attributePattern.exec(attributeString)) !== null) {
|
|
117
|
+
const attributeName = attributeMatch[1].toLowerCase();
|
|
118
|
+
const attributeValue = attributeMatch[2] ?? attributeMatch[3] ?? attributeMatch[4] ?? '';
|
|
119
|
+
|
|
120
|
+
if (!tagAllowedAttributes.has(attributeName) && !globalAllowedAttributes.has(attributeName)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ((attributeName === 'href' || attributeName === 'src') && !safeUriPattern.test(attributeValue)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
filteredAttributes.push(`${attributeName}="${attributeValue}"`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img';
|
|
133
|
+
const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
|
|
134
|
+
return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return sanitized
|
|
138
|
+
.split(/(<[^>]+>)/g)
|
|
139
|
+
.map((part) => {
|
|
140
|
+
if (part.startsWith('<') && part.endsWith('>')) {
|
|
141
|
+
return part;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return part.replace(/&(?!(?:[a-zA-Z]+|#\d+|#x[0-9a-fA-F]+);)/g, '&');
|
|
145
|
+
})
|
|
146
|
+
.join('');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function slugify(value) {
|
|
150
|
+
return String(value || '')
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.trim()
|
|
153
|
+
.replace(/[^\w\s-]/g, '')
|
|
154
|
+
.replace(/[\s_-]+/g, '-')
|
|
155
|
+
.replace(/^-+|-+$/g, '');
|
|
156
|
+
}
|