@zeropress/build-core 0.6.13 → 0.7.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 +145 -43
- package/package.json +8 -7
- package/src/api/build-site.js +969 -405
- package/src/assets/asset-processor.js +0 -13
- package/src/index.d.ts +43 -11
- package/src/render/content-renderer.js +98 -168
- package/src/render/control-flow-renderer.js +5 -2
- package/src/render/variable-resolver.js +6 -1
- package/src/render/zeropress-engine.js +3 -1
- package/src/theme/format-theme-validation.js +54 -0
- package/src/theme/load-theme-dir.js +134 -68
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
export class AssetProcessor {
|
|
4
|
-
async processCSS(css) {
|
|
5
|
-
return css
|
|
6
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
7
|
-
.replace(/\s+/g, ' ')
|
|
8
|
-
.replace(/\s*([{}:;,>+~])\s*/g, '$1')
|
|
9
|
-
.replace(/;}/g, '}')
|
|
10
|
-
.trim();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async processJavaScript(js) {
|
|
14
|
-
return js;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
4
|
generateAssetHash(content) {
|
|
18
5
|
const hash = createHash('sha256');
|
|
19
6
|
hash.update(content);
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PreviewDataV07 } from '@zeropress/preview-data-validator';
|
|
2
2
|
import type { ThemeManifest } from '@zeropress/theme-validator';
|
|
3
3
|
|
|
4
4
|
export interface ThemePackage {
|
|
@@ -24,17 +24,55 @@ export interface BuildOptions {
|
|
|
24
24
|
assetHashing?: boolean;
|
|
25
25
|
favicon?: {
|
|
26
26
|
icon?: string;
|
|
27
|
+
icon_dark?: string;
|
|
27
28
|
svg?: string;
|
|
28
29
|
png?: string;
|
|
29
30
|
apple_touch_icon?: string;
|
|
30
31
|
};
|
|
31
32
|
sitemapStylesheetHref?: string;
|
|
32
|
-
generateSpecialFiles?: boolean;
|
|
33
33
|
generateFeed?: boolean;
|
|
34
34
|
generateRobotsTxt?: boolean;
|
|
35
|
-
|
|
35
|
+
reservedOutputPaths?: readonly string[];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface DisabledThemeCommentsContext {
|
|
39
|
+
enabled: false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ThemeCommentsThreadingContext {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
max_depth: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ZeroPressThemeCommentsContext {
|
|
48
|
+
enabled: true;
|
|
49
|
+
target_type: 'post' | 'page';
|
|
50
|
+
target_public_id: number;
|
|
51
|
+
provider: 'zeropress';
|
|
52
|
+
api_base_url: string;
|
|
53
|
+
per_page: number;
|
|
54
|
+
order: 'asc' | 'desc';
|
|
55
|
+
threading: ThemeCommentsThreadingContext;
|
|
56
|
+
request_token: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WordPressThemeCommentsContext {
|
|
60
|
+
enabled: true;
|
|
61
|
+
target_type: 'post' | 'page';
|
|
62
|
+
target_public_id: number;
|
|
63
|
+
provider: 'wordpress';
|
|
64
|
+
api_base_url: string;
|
|
65
|
+
per_page: number;
|
|
66
|
+
order: 'asc' | 'desc';
|
|
67
|
+
threading: ThemeCommentsThreadingContext;
|
|
68
|
+
request_token?: never;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ThemeCommentsContext =
|
|
72
|
+
| DisabledThemeCommentsContext
|
|
73
|
+
| ZeroPressThemeCommentsContext
|
|
74
|
+
| WordPressThemeCommentsContext;
|
|
75
|
+
|
|
38
76
|
export interface BuildSummaryFile {
|
|
39
77
|
path: string;
|
|
40
78
|
contentType: string;
|
|
@@ -42,25 +80,19 @@ export interface BuildSummaryFile {
|
|
|
42
80
|
sha256: string;
|
|
43
81
|
}
|
|
44
82
|
|
|
45
|
-
export interface BuildManifest {
|
|
46
|
-
generatedAt: string;
|
|
47
|
-
files: BuildSummaryFile[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
83
|
export interface BuildSiteResult {
|
|
51
84
|
files: BuildSummaryFile[];
|
|
52
|
-
manifest?: BuildManifest;
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
export function buildSite(input: {
|
|
56
|
-
previewData:
|
|
88
|
+
previewData: PreviewDataV07;
|
|
57
89
|
themePackage: ThemePackage;
|
|
58
90
|
writer: BuildWriter;
|
|
59
91
|
options?: BuildOptions;
|
|
60
92
|
}): Promise<BuildSiteResult>;
|
|
61
93
|
|
|
62
94
|
export function buildSiteFromThemeDir(input: {
|
|
63
|
-
previewData:
|
|
95
|
+
previewData: PreviewDataV07;
|
|
64
96
|
themeDir: string;
|
|
65
97
|
writer: BuildWriter;
|
|
66
98
|
options?: BuildOptions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import hljs from 'highlight.js';
|
|
3
3
|
import anchor from 'markdown-it-anchor';
|
|
4
|
+
import sanitizeHtmlParser from 'sanitize-html';
|
|
4
5
|
|
|
5
6
|
const TOC_LEVELS = new Set([2, 3, 4]);
|
|
6
7
|
const ALERT_TYPES = new Map([
|
|
@@ -19,6 +20,57 @@ const ADMONITION_CONTAINER_TYPES = new Map([
|
|
|
19
20
|
['CAUTION', { className: 'caution', title: 'Caution' }],
|
|
20
21
|
['DANGER', { className: 'caution', title: 'Danger' }],
|
|
21
22
|
]);
|
|
23
|
+
const CONTENT_ALLOWED_TAGS = [
|
|
24
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
25
|
+
'p', 'br', 'hr',
|
|
26
|
+
'strong', 'b', 'em', 'u', 's', 'sup', 'sub', 'code', 'pre',
|
|
27
|
+
'a', 'img',
|
|
28
|
+
'ul', 'ol', 'li',
|
|
29
|
+
'blockquote', 'aside',
|
|
30
|
+
'figure', 'figcaption', 'picture', 'source',
|
|
31
|
+
'video', 'audio', 'track',
|
|
32
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
33
|
+
'div', 'span', 'nav',
|
|
34
|
+
'iframe', 'input',
|
|
35
|
+
];
|
|
36
|
+
const CONTENT_ALLOWED_ATTRIBUTES = {
|
|
37
|
+
a: ['href', 'title', 'class', 'id', 'target', 'rel'],
|
|
38
|
+
aside: ['role', 'class', 'id'],
|
|
39
|
+
img: ['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding'],
|
|
40
|
+
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class', 'title'],
|
|
41
|
+
input: ['type', 'checked', 'disabled', 'class', 'id', 'aria-label'],
|
|
42
|
+
source: ['src', 'srcset', 'sizes', 'type', 'media', 'width', 'height', 'class', 'id'],
|
|
43
|
+
th: ['rowspan', 'colspan', 'align', 'class', 'id'],
|
|
44
|
+
td: ['rowspan', 'colspan', 'align', 'class', 'id'],
|
|
45
|
+
video: ['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title'],
|
|
46
|
+
audio: ['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title'],
|
|
47
|
+
track: ['src', 'kind', 'srclang', 'label', 'default', 'class', 'id'],
|
|
48
|
+
'*': ['class', 'id'],
|
|
49
|
+
};
|
|
50
|
+
const CONTENT_SANITIZER_OPTIONS = {
|
|
51
|
+
allowedTags: CONTENT_ALLOWED_TAGS,
|
|
52
|
+
allowedAttributes: CONTENT_ALLOWED_ATTRIBUTES,
|
|
53
|
+
allowedSchemes: ['http', 'https'],
|
|
54
|
+
allowedSchemesByTag: {
|
|
55
|
+
a: ['http', 'https', 'mailto', 'tel'],
|
|
56
|
+
},
|
|
57
|
+
allowedSchemesAppliedToAttributes: ['href', 'src', 'poster'],
|
|
58
|
+
allowProtocolRelative: false,
|
|
59
|
+
allowIframeRelativeUrls: true,
|
|
60
|
+
parseStyleAttributes: false,
|
|
61
|
+
selfClosing: ['img', 'br', 'hr', 'input', 'source', 'track'],
|
|
62
|
+
nonTextTags: ['script', 'style', 'textarea', 'option', 'xmp'],
|
|
63
|
+
transformTags: {
|
|
64
|
+
a: transformAnchor,
|
|
65
|
+
th: transformTableCell,
|
|
66
|
+
td: transformTableCell,
|
|
67
|
+
video: transformNativeMedia,
|
|
68
|
+
audio: transformNativeMedia,
|
|
69
|
+
},
|
|
70
|
+
exclusiveFilter(frame) {
|
|
71
|
+
return frame.tag === 'input' && frame.attribs.type !== 'checkbox';
|
|
72
|
+
},
|
|
73
|
+
};
|
|
22
74
|
|
|
23
75
|
export function renderDocumentContent(content, documentType = 'markdown') {
|
|
24
76
|
return renderDocument(content, documentType).html;
|
|
@@ -429,178 +481,68 @@ function escapeHtml(value) {
|
|
|
429
481
|
}
|
|
430
482
|
|
|
431
483
|
function sanitizeHtml(html) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
'p', 'br', 'hr',
|
|
435
|
-
'strong', 'b', 'em', 'u', 's', 'sup', 'sub', 'code', 'pre',
|
|
436
|
-
'a', 'img',
|
|
437
|
-
'ul', 'ol', 'li',
|
|
438
|
-
'blockquote', 'aside',
|
|
439
|
-
'figure', 'figcaption', 'picture', 'source',
|
|
440
|
-
'video', 'audio', 'track',
|
|
441
|
-
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
442
|
-
'div', 'span', 'nav',
|
|
443
|
-
'iframe', 'input',
|
|
444
|
-
]);
|
|
445
|
-
|
|
446
|
-
const allowedAttributes = {
|
|
447
|
-
a: new Set(['href', 'title', 'class', 'id', 'target', 'rel']),
|
|
448
|
-
aside: new Set(['role', 'class', 'id']),
|
|
449
|
-
img: new Set(['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding']),
|
|
450
|
-
iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class', 'title']),
|
|
451
|
-
input: new Set(['type', 'checked', 'disabled', 'class', 'id', 'aria-label']),
|
|
452
|
-
source: new Set(['src', 'srcset', 'sizes', 'type', 'media', 'width', 'height', 'class', 'id']),
|
|
453
|
-
th: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
|
|
454
|
-
td: new Set(['rowspan', 'colspan', 'align', 'class', 'id']),
|
|
455
|
-
video: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
|
|
456
|
-
audio: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title']),
|
|
457
|
-
track: new Set(['src', 'kind', 'srclang', 'label', 'default', 'class', 'id']),
|
|
458
|
-
'*': new Set(['class', 'id']),
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const safeUriPattern = /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
462
|
-
|
|
463
|
-
const stripped = html
|
|
464
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
465
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
466
|
-
|
|
467
|
-
const sanitized = stripped.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)?\/?>/g, (match, tag, attributeString) => {
|
|
468
|
-
const normalizedTag = tag.toLowerCase();
|
|
469
|
-
|
|
470
|
-
if (match.startsWith('</')) {
|
|
471
|
-
return allowedTags.has(normalizedTag) ? `</${normalizedTag}>` : '';
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (!allowedTags.has(normalizedTag)) {
|
|
475
|
-
return '';
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const tagAllowedAttributes = allowedAttributes[normalizedTag] || new Set();
|
|
479
|
-
const globalAllowedAttributes = allowedAttributes['*'];
|
|
480
|
-
const filteredAttributes = [];
|
|
481
|
-
let anchorTarget = '';
|
|
482
|
-
let anchorRel = '';
|
|
483
|
-
let tableCellAlignClass = '';
|
|
484
|
-
|
|
485
|
-
if (attributeString) {
|
|
486
|
-
const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
487
|
-
let attributeMatch;
|
|
488
|
-
|
|
489
|
-
while ((attributeMatch = attributePattern.exec(attributeString)) !== null) {
|
|
490
|
-
const attributeName = attributeMatch[1].toLowerCase();
|
|
491
|
-
const attributeValue = attributeMatch[2] ?? attributeMatch[3] ?? attributeMatch[4] ?? '';
|
|
492
|
-
|
|
493
|
-
if (!tagAllowedAttributes.has(attributeName) && !globalAllowedAttributes.has(attributeName)) {
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if ((attributeName === 'href' || attributeName === 'src' || attributeName === 'poster') && !safeUriPattern.test(attributeValue)) {
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (normalizedTag === 'a' && attributeName === 'target') {
|
|
502
|
-
const target = sanitizeAnchorTarget(attributeValue);
|
|
503
|
-
if (!target) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
anchorTarget = target;
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (normalizedTag === 'a' && attributeName === 'rel') {
|
|
511
|
-
const rel = sanitizeRelList(attributeValue);
|
|
512
|
-
if (!rel) {
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
anchorRel = rel;
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (attributeName === 'srcset' && !isSafeSrcset(attributeValue, safeUriPattern)) {
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (attributeName === 'controlslist') {
|
|
524
|
-
const controlsList = sanitizeControlsList(attributeValue);
|
|
525
|
-
if (!controlsList) {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
filteredAttributes.push(`${attributeName}="${controlsList}"`);
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
484
|
+
return sanitizeHtmlParser(html, CONTENT_SANITIZER_OPTIONS);
|
|
485
|
+
}
|
|
531
486
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
filteredAttributes.push(`${attributeName}="${span}"`);
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
487
|
+
function transformAnchor(tagName, attributes) {
|
|
488
|
+
const transformed = { ...attributes };
|
|
489
|
+
const target = sanitizeAnchorTarget(transformed.target);
|
|
490
|
+
const rel = sanitizeRelList(transformed.rel);
|
|
540
491
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
tableCellAlignClass = `zp-align-${alignment}`;
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
492
|
+
if (target) {
|
|
493
|
+
transformed.target = target;
|
|
494
|
+
} else {
|
|
495
|
+
delete transformed.target;
|
|
496
|
+
}
|
|
549
497
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
498
|
+
const safeRel = target === '_blank' ? ensureBlankTargetRel(rel) : rel;
|
|
499
|
+
if (safeRel) {
|
|
500
|
+
transformed.rel = safeRel;
|
|
501
|
+
} else {
|
|
502
|
+
delete transformed.rel;
|
|
503
|
+
}
|
|
553
504
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
505
|
+
return { tagName, attribs: transformed };
|
|
506
|
+
}
|
|
557
507
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
508
|
+
function transformTableCell(tagName, attributes) {
|
|
509
|
+
const transformed = { ...attributes };
|
|
561
510
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
if (anchorRel) {
|
|
570
|
-
filteredAttributes.push(`rel="${anchorRel}"`);
|
|
571
|
-
}
|
|
511
|
+
for (const attributeName of ['rowspan', 'colspan']) {
|
|
512
|
+
const value = sanitizePositiveIntegerAttribute(transformed[attributeName]);
|
|
513
|
+
if (value) {
|
|
514
|
+
transformed[attributeName] = value;
|
|
515
|
+
} else {
|
|
516
|
+
delete transformed[attributeName];
|
|
572
517
|
}
|
|
518
|
+
}
|
|
573
519
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return sanitized
|
|
580
|
-
.split(/(<[^>]+>)/g)
|
|
581
|
-
.map((part) => {
|
|
582
|
-
if (part.startsWith('<') && part.endsWith('>')) {
|
|
583
|
-
return part;
|
|
584
|
-
}
|
|
520
|
+
const alignment = sanitizeTableAlignment(transformed.align);
|
|
521
|
+
delete transformed.align;
|
|
522
|
+
if (alignment) {
|
|
523
|
+
transformed.class = appendClassName(transformed.class, `zp-align-${alignment}`);
|
|
524
|
+
}
|
|
585
525
|
|
|
586
|
-
|
|
587
|
-
})
|
|
588
|
-
.join('');
|
|
526
|
+
return { tagName, attribs: transformed };
|
|
589
527
|
}
|
|
590
528
|
|
|
591
|
-
function
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
529
|
+
function transformNativeMedia(tagName, attributes) {
|
|
530
|
+
const transformed = { ...attributes };
|
|
531
|
+
const controlsList = sanitizeControlsList(transformed.controlslist);
|
|
532
|
+
if (controlsList) {
|
|
533
|
+
transformed.controlslist = controlsList;
|
|
534
|
+
} else {
|
|
535
|
+
delete transformed.controlslist;
|
|
596
536
|
}
|
|
537
|
+
return { tagName, attribs: transformed };
|
|
538
|
+
}
|
|
597
539
|
|
|
598
|
-
|
|
599
|
-
const classes =
|
|
540
|
+
function appendClassName(value, className) {
|
|
541
|
+
const classes = String(value || '').split(/\s+/).filter(Boolean);
|
|
600
542
|
if (!classes.includes(className)) {
|
|
601
543
|
classes.push(className);
|
|
602
544
|
}
|
|
603
|
-
|
|
545
|
+
return classes.join(' ');
|
|
604
546
|
}
|
|
605
547
|
|
|
606
548
|
function sanitizePositiveIntegerAttribute(value) {
|
|
@@ -668,18 +610,6 @@ function sanitizeControlsList(value) {
|
|
|
668
610
|
return tokens.join(' ');
|
|
669
611
|
}
|
|
670
612
|
|
|
671
|
-
function isSafeSrcset(value, safeUriPattern) {
|
|
672
|
-
const candidates = String(value)
|
|
673
|
-
.split(',')
|
|
674
|
-
.map((candidate) => candidate.trim())
|
|
675
|
-
.filter(Boolean);
|
|
676
|
-
|
|
677
|
-
return candidates.length > 0 && candidates.every((candidate) => {
|
|
678
|
-
const [url] = candidate.split(/\s+/);
|
|
679
|
-
return Boolean(url) && safeUriPattern.test(url);
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
613
|
function slugify(value) {
|
|
684
614
|
return String(value || '')
|
|
685
615
|
.toLowerCase()
|
|
@@ -229,6 +229,9 @@ export class ControlFlowRenderer {
|
|
|
229
229
|
|
|
230
230
|
if (token.startsWith('/')) {
|
|
231
231
|
const tagName = token.slice(1).trim();
|
|
232
|
+
if (COMPARISON_BLOCK_TAGS.has(tagName)) {
|
|
233
|
+
throw new Error(`Named conditional closing tag {{/${tagName}}} is not supported; use {{/if}}`);
|
|
234
|
+
}
|
|
232
235
|
if (!stopTags.has(tagName)) {
|
|
233
236
|
throw new Error(`Unexpected closing tag /${tagName}`);
|
|
234
237
|
}
|
|
@@ -370,8 +373,8 @@ export class ControlFlowRenderer {
|
|
|
370
373
|
let alternate = [];
|
|
371
374
|
let nextIndex = startIndex;
|
|
372
375
|
let currentBranch = initialBranch;
|
|
373
|
-
const comparisonStopTags = new Set(['else', 'if',
|
|
374
|
-
const comparisonCloseTags = new Set(['if'
|
|
376
|
+
const comparisonStopTags = new Set(['else', 'if', ...COMPARISON_ELSE_IF_TAGS]);
|
|
377
|
+
const comparisonCloseTags = new Set(['if']);
|
|
375
378
|
|
|
376
379
|
while (true) {
|
|
377
380
|
const branchResult = this.parseNodes(source, nextIndex, comparisonStopTags, partialArgScope);
|
|
@@ -94,7 +94,12 @@ export class VariableResolver {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
variablePath === 'data'
|
|
99
|
+
|| variablePath.startsWith('data.')
|
|
100
|
+
|| variablePath.includes('.data.')
|
|
101
|
+
|| variablePath.includes('.meta.')
|
|
102
|
+
) {
|
|
98
103
|
return false;
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_RUNTIME } from '@zeropress/theme-validator';
|
|
1
2
|
import { SlotResolver } from './slot-resolver.js';
|
|
2
3
|
import { VariableResolver } from './variable-resolver.js';
|
|
3
4
|
import { ControlFlowRenderer } from './control-flow-renderer.js';
|
|
@@ -46,11 +47,12 @@ export class ZeroPressEngine {
|
|
|
46
47
|
site: context.site,
|
|
47
48
|
currentUrl: context.currentUrl,
|
|
48
49
|
language: context.language,
|
|
50
|
+
comments: context.comments,
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
renderTemplate(template, data) {
|
|
53
|
-
if (this.themePackage?.metadata?.runtime !==
|
|
55
|
+
if (this.themePackage?.metadata?.runtime !== DEFAULT_RUNTIME) {
|
|
54
56
|
throw new Error(`Unsupported theme runtime: ${this.themePackage?.metadata?.runtime || 'unknown'}`);
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function formatThemeValidationFailure(validation) {
|
|
2
|
+
const blocks = [
|
|
3
|
+
[
|
|
4
|
+
'Theme validation failed',
|
|
5
|
+
`Errors: ${validation.errors.length}`,
|
|
6
|
+
`Checked files: ${validation.checkedFiles}`,
|
|
7
|
+
].join('\n'),
|
|
8
|
+
...validation.errors.map((issue) => formatThemeValidationIssue(issue)),
|
|
9
|
+
];
|
|
10
|
+
return blocks.join('\n\n');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatThemeValidationIssue(issue) {
|
|
14
|
+
if (!issue) {
|
|
15
|
+
return 'Reason: Unknown error';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const lines = [`ERROR ${issue.code || 'THEME_VALIDATION_ERROR'}`];
|
|
19
|
+
const location = splitIssuePath(issue.path);
|
|
20
|
+
if (location.file) {
|
|
21
|
+
lines.push(`File: ${location.file}`);
|
|
22
|
+
}
|
|
23
|
+
if (location.path) {
|
|
24
|
+
lines.push(`Path: ${location.path}`);
|
|
25
|
+
}
|
|
26
|
+
if (Number.isInteger(issue.line) && Number.isInteger(issue.column)) {
|
|
27
|
+
lines.push(`Line: ${issue.line}, Column: ${issue.column}`);
|
|
28
|
+
}
|
|
29
|
+
if (issue.category) {
|
|
30
|
+
lines.push(`Category: ${issue.category}`);
|
|
31
|
+
}
|
|
32
|
+
lines.push(`Reason: ${issue.message || 'Unknown error'}`);
|
|
33
|
+
if (issue.snippet) {
|
|
34
|
+
const lineLabel = Number.isInteger(issue.line) ? String(issue.line) : '';
|
|
35
|
+
lines.push('', `${lineLabel} | ${issue.snippet.line}`, `${' '.repeat(lineLabel.length)} | ${issue.snippet.pointer}`);
|
|
36
|
+
}
|
|
37
|
+
if (issue.hint) {
|
|
38
|
+
lines.push('', 'Hint:', issue.hint);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function splitIssuePath(issuePath) {
|
|
45
|
+
const normalizedPath = String(issuePath || '');
|
|
46
|
+
if (normalizedPath.startsWith('theme.json.')) {
|
|
47
|
+
return {
|
|
48
|
+
file: 'theme.json',
|
|
49
|
+
path: normalizedPath.slice('theme.json.'.length),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { file: normalizedPath, path: '' };
|
|
54
|
+
}
|