@zeropress/build-core 0.7.3 → 0.7.4
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
|
@@ -121,6 +121,9 @@ Loads a theme directory from disk and renders it using the same core pipeline.
|
|
|
121
121
|
|
|
122
122
|
This is useful for local tooling that wants filesystem theme loading but still uses the same deterministic core renderer.
|
|
123
123
|
|
|
124
|
+
Theme validation failures from either build entry point carry `error.code === 'THEME_VALIDATION_FAILED'`.
|
|
125
|
+
Their messages preserve report and hint line breaks while escaping terminal control characters in diagnostic values such as paths, messages, and source snippets. CLI consumers can preserve the layout of these reports while escaping arbitrary errors as single-line text.
|
|
126
|
+
|
|
124
127
|
## Writers
|
|
125
128
|
|
|
126
129
|
### `MemoryWriter`
|
|
@@ -171,7 +174,7 @@ Theme CSS and JavaScript assets are emitted byte-for-byte as provided. `custom_c
|
|
|
171
174
|
|
|
172
175
|
User-defined scalar metadata under `site.meta`, `post.meta`, `page.meta`, and item `meta` is always HTML-escaped during template interpolation. Names ending in `_html` or `_url` do not opt metadata into raw rendering; use the explicit content and custom HTML contracts for trusted raw markup.
|
|
173
176
|
|
|
174
|
-
Post/Page HTML, Markdown raw HTML, and text-widget HTML pass through a parser-based explicit allowlist. Safe structural, media, table, task-list, and alert markup is retained; event handlers and unsafe URL schemes are removed. Links permit relative URLs, HTTP(S), `mailto:`, and `tel:`. Media and iframe URLs permit relative URLs or HTTP(S), and protocol-relative URLs are rejected. Each `srcset` candidate is checked independently. This sanitizer boundary does not include trusted `custom_html` slots or `standalone_html`, which remain intentionally unchanged.
|
|
177
|
+
Post/Page HTML, Markdown raw HTML, and text-widget HTML pass through a parser-based explicit allowlist. Safe structural, media, table, task-list, and alert markup is retained; event handlers and unsafe URL schemes are removed. Inline presentation is limited to `text-align: left | center | right | justify` on paragraphs/headings and bounded CSS `color` values on spans; every other inline style is removed. Links permit relative URLs, HTTP(S), `mailto:`, and `tel:`. Media and iframe URLs permit relative URLs or HTTP(S), and protocol-relative URLs are rejected. Each `srcset` candidate is checked independently. This sanitizer boundary does not include trusted `custom_html` slots or `standalone_html`, which remain intentionally unchanged.
|
|
175
178
|
|
|
176
179
|
Build-core derives:
|
|
177
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Shared ZeroPress build core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"node": ">=22.22.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zeropress/slug-policy": "0.2.
|
|
36
|
+
"@zeropress/slug-policy": "0.2.2",
|
|
37
37
|
"@zeropress/preview-data-validator": "0.7.3",
|
|
38
38
|
"@zeropress/theme-validator": "0.7.3",
|
|
39
39
|
"highlight.js": "11.12.0",
|
|
40
|
-
"markdown-it": "15.0.
|
|
41
|
-
"markdown-it-anchor": "
|
|
40
|
+
"markdown-it": "15.0.2",
|
|
41
|
+
"markdown-it-anchor": "10.0.0",
|
|
42
42
|
"sanitize-html": "2.17.7"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/api/build-site.js
CHANGED
|
@@ -5,7 +5,7 @@ import { validateThemeFiles } from '@zeropress/theme-validator';
|
|
|
5
5
|
import sanitizeHtml from 'sanitize-html';
|
|
6
6
|
import { AssetProcessor } from '../assets/asset-processor.js';
|
|
7
7
|
import { renderDocument, renderDocumentContent } from '../render/content-renderer.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createThemeValidationError } from '../theme/format-theme-validation.js';
|
|
9
9
|
import { ZeroPressEngine } from '../render/zeropress-engine.js';
|
|
10
10
|
|
|
11
11
|
const DEFAULT_OPTIONS = {
|
|
@@ -2248,7 +2248,7 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
2248
2248
|
|
|
2249
2249
|
const validation = await validateThemeFiles(fileMap);
|
|
2250
2250
|
if (!validation.ok) {
|
|
2251
|
-
throw
|
|
2251
|
+
throw createThemeValidationError(validation);
|
|
2252
2252
|
}
|
|
2253
2253
|
|
|
2254
2254
|
if (!validation.manifest) {
|
|
@@ -34,6 +34,14 @@ const CONTENT_ALLOWED_TAGS = [
|
|
|
34
34
|
'iframe', 'input',
|
|
35
35
|
];
|
|
36
36
|
const CONTENT_ALLOWED_ATTRIBUTES = {
|
|
37
|
+
h1: ['style'],
|
|
38
|
+
h2: ['style'],
|
|
39
|
+
h3: ['style'],
|
|
40
|
+
h4: ['style'],
|
|
41
|
+
h5: ['style'],
|
|
42
|
+
h6: ['style'],
|
|
43
|
+
p: ['style'],
|
|
44
|
+
span: ['style'],
|
|
37
45
|
a: ['href', 'title', 'class', 'id', 'target', 'rel'],
|
|
38
46
|
aside: ['role', 'class', 'id'],
|
|
39
47
|
img: ['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding'],
|
|
@@ -47,6 +55,8 @@ const CONTENT_ALLOWED_ATTRIBUTES = {
|
|
|
47
55
|
track: ['src', 'kind', 'srclang', 'label', 'default', 'class', 'id'],
|
|
48
56
|
'*': ['class', 'id'],
|
|
49
57
|
};
|
|
58
|
+
const CONTENT_COLOR_STYLE_PATTERN = /^(?:#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|[a-z]{1,32}|(?:rgb|rgba|hsl|hsla)\((?:[-+.\d%,/\s]|deg|rad|grad|turn)+\))$/i;
|
|
59
|
+
const CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN = /^(?:left|center|right|justify)$/i;
|
|
50
60
|
const CONTENT_SANITIZER_OPTIONS = {
|
|
51
61
|
allowedTags: CONTENT_ALLOWED_TAGS,
|
|
52
62
|
allowedAttributes: CONTENT_ALLOWED_ATTRIBUTES,
|
|
@@ -57,7 +67,17 @@ const CONTENT_SANITIZER_OPTIONS = {
|
|
|
57
67
|
allowedSchemesAppliedToAttributes: ['href', 'src', 'poster'],
|
|
58
68
|
allowProtocolRelative: false,
|
|
59
69
|
allowIframeRelativeUrls: true,
|
|
60
|
-
|
|
70
|
+
allowedStyles: {
|
|
71
|
+
h1: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
72
|
+
h2: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
73
|
+
h3: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
74
|
+
h4: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
75
|
+
h5: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
76
|
+
h6: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
77
|
+
p: { 'text-align': [CONTENT_TEXT_ALIGNMENT_STYLE_PATTERN] },
|
|
78
|
+
span: { color: [CONTENT_COLOR_STYLE_PATTERN] },
|
|
79
|
+
},
|
|
80
|
+
parseStyleAttributes: true,
|
|
61
81
|
selfClosing: ['img', 'br', 'hr', 'input', 'source', 'track'],
|
|
62
82
|
nonTextTags: ['script', 'style', 'template', 'noscript', 'textarea', 'option', 'xmp'],
|
|
63
83
|
transformTags: {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
const UNSAFE_TERMINAL_CHARACTER_REGEX = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu;
|
|
2
|
+
|
|
3
|
+
export function createThemeValidationError(validation) {
|
|
4
|
+
return Object.assign(new Error(formatThemeValidationFailure(validation)), {
|
|
5
|
+
code: 'THEME_VALIDATION_FAILED',
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
export function formatThemeValidationFailure(validation) {
|
|
2
10
|
const blocks = [
|
|
3
11
|
[
|
|
4
12
|
'Theme validation failed',
|
|
5
13
|
`Errors: ${validation.errors.length}`,
|
|
6
|
-
`Checked files: ${validation.checkedFiles}`,
|
|
14
|
+
`Checked files: ${toTerminalSafeText(validation.checkedFiles)}`,
|
|
7
15
|
].join('\n'),
|
|
8
16
|
...validation.errors.map((issue) => formatThemeValidationIssue(issue)),
|
|
9
17
|
];
|
|
@@ -15,32 +23,38 @@ function formatThemeValidationIssue(issue) {
|
|
|
15
23
|
return 'Reason: Unknown error';
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
const lines = [`ERROR ${issue.code || 'THEME_VALIDATION_ERROR'}`];
|
|
26
|
+
const lines = [`ERROR ${toTerminalSafeText(issue.code || 'THEME_VALIDATION_ERROR')}`];
|
|
19
27
|
const location = splitIssuePath(issue.path);
|
|
20
28
|
if (location.file) {
|
|
21
|
-
lines.push(`File: ${location.file}`);
|
|
29
|
+
lines.push(`File: ${toTerminalSafeText(location.file)}`);
|
|
22
30
|
}
|
|
23
31
|
if (location.path) {
|
|
24
|
-
lines.push(`Path: ${location.path}`);
|
|
32
|
+
lines.push(`Path: ${toTerminalSafeText(location.path)}`);
|
|
25
33
|
}
|
|
26
34
|
if (Number.isInteger(issue.line) && Number.isInteger(issue.column)) {
|
|
27
35
|
lines.push(`Line: ${issue.line}, Column: ${issue.column}`);
|
|
28
36
|
}
|
|
29
37
|
if (issue.category) {
|
|
30
|
-
lines.push(`Category: ${issue.category}`);
|
|
38
|
+
lines.push(`Category: ${toTerminalSafeText(issue.category)}`);
|
|
31
39
|
}
|
|
32
|
-
lines.push(`Reason: ${issue.message || 'Unknown error'}`);
|
|
40
|
+
lines.push(`Reason: ${toTerminalSafeText(issue.message || 'Unknown error')}`);
|
|
33
41
|
if (issue.snippet) {
|
|
34
42
|
const lineLabel = Number.isInteger(issue.line) ? String(issue.line) : '';
|
|
35
|
-
lines.push('', `${lineLabel} | ${issue.snippet.line}`, `${' '.repeat(lineLabel.length)} | ${issue.snippet.pointer}`);
|
|
43
|
+
lines.push('', `${lineLabel} | ${toTerminalSafeText(issue.snippet.line)}`, `${' '.repeat(lineLabel.length)} | ${toTerminalSafeText(issue.snippet.pointer)}`);
|
|
36
44
|
}
|
|
37
45
|
if (issue.hint) {
|
|
38
|
-
lines.push('', 'Hint:', issue.hint);
|
|
46
|
+
lines.push('', 'Hint:', String(issue.hint).split('\n').map(toTerminalSafeText).join('\n'));
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
return lines.join('\n');
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
function toTerminalSafeText(value) {
|
|
53
|
+
return String(value ?? '').replace(UNSAFE_TERMINAL_CHARACTER_REGEX, (character) => (
|
|
54
|
+
`\\u${character.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
function splitIssuePath(issuePath) {
|
|
45
59
|
const normalizedPath = String(issuePath || '');
|
|
46
60
|
if (normalizedPath.startsWith('theme.json.')) {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
validateThemeFiles,
|
|
3
3
|
validateThemePackageLimits,
|
|
4
4
|
} from '@zeropress/theme-validator';
|
|
5
|
-
import {
|
|
5
|
+
import { createThemeValidationError } from './format-theme-validation.js';
|
|
6
6
|
|
|
7
7
|
const TEXT_FILE_EXTENSIONS = new Set(['.html', '.json', '.css', '.js', '.txt', '.svg', '.xml']);
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ export async function loadThemePackageFromDir(themeDir) {
|
|
|
47
47
|
pathEntries: packageState.pathEntries,
|
|
48
48
|
});
|
|
49
49
|
if (!validation.ok) {
|
|
50
|
-
throw
|
|
50
|
+
throw createThemeValidationError(validation);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const rawThemeJson = String(fileMap.get('theme.json'));
|
|
@@ -174,7 +174,7 @@ async function readThemeDir(fs, fsConstants, path, rootDir, currentDir, fileMap,
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
function createThemePathError(code, filePath, message, checkedFiles) {
|
|
177
|
-
return
|
|
177
|
+
return createThemeValidationError({
|
|
178
178
|
errors: [{
|
|
179
179
|
code,
|
|
180
180
|
path: filePath,
|
|
@@ -183,7 +183,7 @@ function createThemePathError(code, filePath, message, checkedFiles) {
|
|
|
183
183
|
category: 'theme_package_paths',
|
|
184
184
|
}],
|
|
185
185
|
checkedFiles,
|
|
186
|
-
})
|
|
186
|
+
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function assertThemePackageLimits(packageState) {
|
|
@@ -194,10 +194,10 @@ function assertThemePackageLimits(packageState) {
|
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
throw
|
|
197
|
+
throw createThemeValidationError({
|
|
198
198
|
errors,
|
|
199
199
|
checkedFiles: packageState.fileSizes.size,
|
|
200
|
-
})
|
|
200
|
+
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function toUint8Array(value) {
|