@zeropress/build-core 0.1.16 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.1.16",
3
+ "version": "0.2.1",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@zeropress/slug-policy": "^0.1.0",
37
37
  "@zeropress/preview-data-validator": "^0.4.4",
38
- "@zeropress/theme-validator": "^0.4.0",
38
+ "@zeropress/theme-validator": "^0.5.0",
39
39
  "highlight.js": "^11.11.1",
40
40
  "markdown-it": "^14.1.0",
41
41
  "markdown-it-anchor": "^9.2.0"
@@ -846,9 +846,71 @@ function buildStructuredPagination(paginationData) {
846
846
  url: page.url,
847
847
  current: page.current,
848
848
  })),
849
+ window: buildPaginationWindow(paginationData),
849
850
  };
850
851
  }
851
852
 
853
+ function buildPaginationWindow(paginationData) {
854
+ const totalPages = paginationData.totalPages;
855
+ if (!Number.isInteger(totalPages) || totalPages <= 0) {
856
+ return [];
857
+ }
858
+
859
+ const currentPage = paginationData.currentPage;
860
+ const pageMap = new Map(
861
+ paginationData.pages.map((page) => [page.number, {
862
+ kind: 'page',
863
+ number: page.number,
864
+ url: page.url,
865
+ current: page.current,
866
+ }]),
867
+ );
868
+
869
+ if (totalPages <= 7) {
870
+ return paginationData.pages.map((page) => ({
871
+ kind: 'page',
872
+ number: page.number,
873
+ url: page.url,
874
+ current: page.current,
875
+ }));
876
+ }
877
+
878
+ const pageNumbers = new Set([1, totalPages, currentPage - 1, currentPage, currentPage + 1]);
879
+
880
+ if (currentPage <= 4) {
881
+ for (let number = 1; number <= 5; number += 1) {
882
+ pageNumbers.add(number);
883
+ }
884
+ }
885
+
886
+ if (currentPage >= totalPages - 3) {
887
+ for (let number = totalPages - 4; number <= totalPages; number += 1) {
888
+ pageNumbers.add(number);
889
+ }
890
+ }
891
+
892
+ const orderedNumbers = Array.from(pageNumbers)
893
+ .filter((number) => Number.isInteger(number) && number >= 1 && number <= totalPages)
894
+ .sort((left, right) => left - right);
895
+
896
+ const windowItems = [];
897
+ let previousNumber = null;
898
+
899
+ for (const number of orderedNumbers) {
900
+ if (previousNumber != null && number - previousNumber > 1) {
901
+ windowItems.push({ kind: 'gap' });
902
+ }
903
+
904
+ const pageItem = pageMap.get(number);
905
+ if (pageItem) {
906
+ windowItems.push(pageItem);
907
+ }
908
+ previousNumber = number;
909
+ }
910
+
911
+ return windowItems;
912
+ }
913
+
852
914
  function buildStructuredPostItems(posts, postBySlug) {
853
915
  return posts
854
916
  .map((post) => postBySlug.get(post.slug))
@@ -1,6 +1,8 @@
1
- function isWhitespace(value) {
2
- return /^[\s\r\n\t]*$/.test(value);
3
- }
1
+ import { parsePartialToken } from './partial-resolver.js';
2
+
3
+ const PATH_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
4
+ const FOR_EXPRESSION_REGEX = /^([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)$/;
5
+ const IF_EQ_EXPRESSION_REGEX = /^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+("(?:[^"\\]|\\.)*")$/;
4
6
 
5
7
  export class ControlFlowRenderer {
6
8
  constructor(options = {}) {
@@ -18,6 +20,13 @@ export class ControlFlowRenderer {
18
20
  }
19
21
 
20
22
  render(template, data, renderOptions = {}) {
23
+ return this.renderTemplate(template, data, {
24
+ ...renderOptions,
25
+ partialStack: Array.isArray(renderOptions.partialStack) ? renderOptions.partialStack : [],
26
+ });
27
+ }
28
+
29
+ renderTemplate(template, data, renderOptions) {
21
30
  const nodes = this.parse(template);
22
31
  return this.renderNodes(nodes, data, renderOptions);
23
32
  }
@@ -94,58 +103,46 @@ export class ControlFlowRenderer {
94
103
  return { nodes, nextIndex: tokenEnd + 2, stopTag: 'else' };
95
104
  }
96
105
 
97
- if (token.startsWith('#if_eq ')) {
98
- const expression = token.slice('#if_eq '.length).trim();
99
- const match = /^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+("(?:[^"\\]|\\.)*")$/.exec(expression);
100
- if (!match) {
101
- throw new Error(`Invalid if_eq expression: ${expression}`);
106
+ if (token.startsWith('#else_if_eq ')) {
107
+ if (!stopTags.has('else_if_eq')) {
108
+ throw new Error('Unexpected else_if_eq tag');
102
109
  }
103
- const [, path, literalSource] = match;
104
- const literal = JSON.parse(literalSource);
105
- const ifResult = this.parseNodes(source, tokenEnd + 2, new Set(['else', 'if_eq']));
106
- let elseNodes = [];
107
- let nextIndex = ifResult.nextIndex;
108
- if (ifResult.stopTag === 'else') {
109
- const elseResult = this.parseNodes(source, ifResult.nextIndex, new Set(['if_eq']));
110
- if (elseResult.stopTag !== 'if_eq') {
111
- throw new Error('Unclosed if_eq block after else');
112
- }
113
- elseNodes = elseResult.nodes;
114
- nextIndex = elseResult.nextIndex;
115
- } else if (ifResult.stopTag !== 'if_eq') {
116
- throw new Error('Unclosed if_eq block');
110
+ return { nodes, nextIndex: tokenEnd + 2, stopTag: token };
111
+ }
112
+
113
+ if (token.startsWith('#else_if ')) {
114
+ if (!stopTags.has('else_if')) {
115
+ throw new Error('Unexpected else_if tag');
117
116
  }
118
- nodes.push({ type: 'if_eq', path, literal, consequent: ifResult.nodes, alternate: elseNodes });
119
- index = nextIndex;
117
+ return { nodes, nextIndex: tokenEnd + 2, stopTag: token };
118
+ }
119
+
120
+ if (token.startsWith('partial:')) {
121
+ const { name, args } = parsePartialToken(token);
122
+ nodes.push({ type: 'partial', name, args });
123
+ index = tokenEnd + 2;
124
+ continue;
125
+ }
126
+
127
+ if (token.startsWith('#if_eq ')) {
128
+ const expression = token.slice('#if_eq '.length).trim();
129
+ const block = this.parseIfEqBlock(source, tokenEnd + 2, expression);
130
+ nodes.push(block.node);
131
+ index = block.nextIndex;
120
132
  continue;
121
133
  }
122
134
 
123
135
  if (token.startsWith('#if ')) {
124
- const path = token.slice('#if '.length).trim();
125
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(path)) {
126
- throw new Error(`Invalid if expression: ${path}`);
127
- }
128
- const ifResult = this.parseNodes(source, tokenEnd + 2, new Set(['else', 'if']));
129
- let elseNodes = [];
130
- let nextIndex = ifResult.nextIndex;
131
- if (ifResult.stopTag === 'else') {
132
- const elseResult = this.parseNodes(source, ifResult.nextIndex, new Set(['if']));
133
- if (elseResult.stopTag !== 'if') {
134
- throw new Error('Unclosed if block after else');
135
- }
136
- elseNodes = elseResult.nodes;
137
- nextIndex = elseResult.nextIndex;
138
- } else if (ifResult.stopTag !== 'if') {
139
- throw new Error('Unclosed if block');
140
- }
141
- nodes.push({ type: 'if', path, consequent: ifResult.nodes, alternate: elseNodes });
142
- index = nextIndex;
136
+ const expression = token.slice('#if '.length).trim();
137
+ const block = this.parseIfBlock(source, tokenEnd + 2, expression);
138
+ nodes.push(block.node);
139
+ index = block.nextIndex;
143
140
  continue;
144
141
  }
145
142
 
146
143
  if (token.startsWith('#for ')) {
147
144
  const expression = token.slice('#for '.length).trim();
148
- const match = /^([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)$/.exec(expression);
145
+ const match = FOR_EXPRESSION_REGEX.exec(expression);
149
146
  if (!match) {
150
147
  throw new Error(`Invalid for expression: ${expression}`);
151
148
  }
@@ -164,13 +161,132 @@ export class ControlFlowRenderer {
164
161
  }
165
162
 
166
163
  if (stopTags.size > 0) {
167
- const expected = Array.from(stopTags).filter((entry) => entry !== 'else').join(', ');
164
+ const expected = Array.from(stopTags).join(', ');
168
165
  throw new Error(`Missing closing tag for ${expected}`);
169
166
  }
170
167
 
171
168
  return { nodes, nextIndex: source.length, stopTag: null };
172
169
  }
173
170
 
171
+ parseIfBlock(source, startIndex, initialPath) {
172
+ if (!PATH_REGEX.test(initialPath)) {
173
+ throw new Error(`Invalid if expression: ${initialPath}`);
174
+ }
175
+
176
+ const branches = [];
177
+ let alternate = [];
178
+ let nextIndex = startIndex;
179
+ let currentPath = initialPath;
180
+
181
+ while (true) {
182
+ const branchResult = this.parseNodes(source, nextIndex, new Set(['else', 'else_if', 'if']));
183
+ branches.push({
184
+ path: currentPath,
185
+ consequent: branchResult.nodes,
186
+ });
187
+
188
+ if (branchResult.stopTag === 'else') {
189
+ const elseResult = this.parseNodes(source, branchResult.nextIndex, new Set(['if']));
190
+ if (elseResult.stopTag !== 'if') {
191
+ throw new Error('Unclosed if block after else');
192
+ }
193
+ alternate = elseResult.nodes;
194
+ nextIndex = elseResult.nextIndex;
195
+ break;
196
+ }
197
+
198
+ if (typeof branchResult.stopTag === 'string' && branchResult.stopTag.startsWith('#else_if ')) {
199
+ currentPath = branchResult.stopTag.slice('#else_if '.length).trim();
200
+ if (!PATH_REGEX.test(currentPath)) {
201
+ throw new Error(`Invalid else_if expression: ${currentPath}`);
202
+ }
203
+ nextIndex = branchResult.nextIndex;
204
+ continue;
205
+ }
206
+
207
+ if (branchResult.stopTag === 'if') {
208
+ nextIndex = branchResult.nextIndex;
209
+ break;
210
+ }
211
+
212
+ throw new Error('Unclosed if block');
213
+ }
214
+
215
+ return {
216
+ node: {
217
+ type: 'if',
218
+ branches,
219
+ alternate,
220
+ },
221
+ nextIndex,
222
+ };
223
+ }
224
+
225
+ parseIfEqBlock(source, startIndex, expression) {
226
+ const initialBranch = this.parseIfEqBranchExpression(expression, 'if_eq');
227
+ const branches = [];
228
+ let alternate = [];
229
+ let nextIndex = startIndex;
230
+ let currentBranch = initialBranch;
231
+
232
+ while (true) {
233
+ const branchResult = this.parseNodes(source, nextIndex, new Set(['else', 'else_if_eq', 'if_eq']));
234
+ branches.push({
235
+ path: currentBranch.path,
236
+ literal: currentBranch.literal,
237
+ consequent: branchResult.nodes,
238
+ });
239
+
240
+ if (branchResult.stopTag === 'else') {
241
+ const elseResult = this.parseNodes(source, branchResult.nextIndex, new Set(['if_eq']));
242
+ if (elseResult.stopTag !== 'if_eq') {
243
+ throw new Error('Unclosed if_eq block after else');
244
+ }
245
+ alternate = elseResult.nodes;
246
+ nextIndex = elseResult.nextIndex;
247
+ break;
248
+ }
249
+
250
+ if (typeof branchResult.stopTag === 'string' && branchResult.stopTag.startsWith('#else_if_eq ')) {
251
+ currentBranch = this.parseIfEqBranchExpression(
252
+ branchResult.stopTag.slice('#else_if_eq '.length).trim(),
253
+ 'else_if_eq',
254
+ );
255
+ nextIndex = branchResult.nextIndex;
256
+ continue;
257
+ }
258
+
259
+ if (branchResult.stopTag === 'if_eq') {
260
+ nextIndex = branchResult.nextIndex;
261
+ break;
262
+ }
263
+
264
+ throw new Error('Unclosed if_eq block');
265
+ }
266
+
267
+ return {
268
+ node: {
269
+ type: 'if_eq',
270
+ branches,
271
+ alternate,
272
+ },
273
+ nextIndex,
274
+ };
275
+ }
276
+
277
+ parseIfEqBranchExpression(expression, tagName) {
278
+ const match = IF_EQ_EXPRESSION_REGEX.exec(expression);
279
+ if (!match) {
280
+ throw new Error(`Invalid ${tagName} expression: ${expression}`);
281
+ }
282
+
283
+ const [, path, literalSource] = match;
284
+ return {
285
+ path,
286
+ literal: JSON.parse(literalSource),
287
+ };
288
+ }
289
+
174
290
  renderNodes(nodes, data, renderOptions) {
175
291
  return nodes.map((node) => this.renderNode(node, data, renderOptions)).join('');
176
292
  }
@@ -180,15 +296,15 @@ export class ControlFlowRenderer {
180
296
  case 'text':
181
297
  return this.renderText(node.value, data, renderOptions);
182
298
  case 'if': {
183
- const value = this.resolvePath(data, node.path);
184
- return this.isTruthy(value)
185
- ? this.renderNodes(node.consequent, data, renderOptions)
299
+ const branch = node.branches.find((entry) => this.isTruthy(this.resolvePath(data, entry.path)));
300
+ return branch
301
+ ? this.renderNodes(branch.consequent, data, renderOptions)
186
302
  : this.renderNodes(node.alternate, data, renderOptions);
187
303
  }
188
304
  case 'if_eq': {
189
- const value = this.resolvePath(data, node.path);
190
- return value === node.literal
191
- ? this.renderNodes(node.consequent, data, renderOptions)
305
+ const branch = node.branches.find((entry) => this.resolvePath(data, entry.path) === entry.literal);
306
+ return branch
307
+ ? this.renderNodes(branch.consequent, data, renderOptions)
192
308
  : this.renderNodes(node.alternate, data, renderOptions);
193
309
  }
194
310
  case 'for': {
@@ -200,15 +316,45 @@ export class ControlFlowRenderer {
200
316
  .map((item, index) => this.renderNodes(node.body, {
201
317
  ...data,
202
318
  [node.itemName]: item,
203
- $index: index,
319
+ loop: {
320
+ first: index === 0,
321
+ last: index === items.length - 1,
322
+ index,
323
+ },
204
324
  }, renderOptions))
205
325
  .join('');
206
326
  }
327
+ case 'partial':
328
+ return this.renderPartial(node, data, renderOptions);
207
329
  default:
208
330
  return '';
209
331
  }
210
332
  }
211
333
 
334
+ renderPartial(node, data, renderOptions) {
335
+ const partials = renderOptions?.partials;
336
+ if (!partials || !partials.has(node.name)) {
337
+ throw new Error(`Partial "${node.name}" not found`);
338
+ }
339
+
340
+ const activeStack = Array.isArray(renderOptions?.partialStack) ? renderOptions.partialStack : [];
341
+ if (activeStack.includes(node.name)) {
342
+ const cycle = [...activeStack, node.name].join(' -> ');
343
+ throw new Error(`Circular partial reference detected: ${cycle}`);
344
+ }
345
+
346
+ const partialTemplate = String(partials.get(node.name) || '');
347
+ const partialData = {
348
+ ...data,
349
+ partial: { ...node.args },
350
+ };
351
+
352
+ return this.renderTemplate(partialTemplate, partialData, {
353
+ ...renderOptions,
354
+ partialStack: [...activeStack, node.name],
355
+ });
356
+ }
357
+
212
358
  isTruthy(value) {
213
359
  if (Array.isArray(value)) {
214
360
  return value.length > 0;
@@ -1,23 +1,114 @@
1
- export class PartialResolver {
2
- static PARTIAL_PATTERN = /\{\{partial:([a-zA-Z_][a-zA-Z0-9_-]*(?:\/[a-zA-Z_][a-zA-Z0-9_-]*)*)\}\}/g;
1
+ const PARTIAL_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\/[a-zA-Z_][a-zA-Z0-9_-]*)*$/;
2
+ const IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3
3
 
4
- resolve(template, partials) {
5
- return this.resolveTemplate(String(template || ''), partials, []);
4
+ export function parsePartialToken(token) {
5
+ const source = String(token || '').trim();
6
+ if (!source.startsWith('partial:')) {
7
+ throw new Error(`Invalid partial token: ${source}`);
6
8
  }
7
9
 
8
- resolveTemplate(template, partials, activeStack) {
9
- return template.replace(PartialResolver.PARTIAL_PATTERN, (_, partialName) => {
10
- if (!partials.has(partialName)) {
11
- throw new Error(`Partial "${partialName}" not found`);
10
+ const expression = source.slice('partial:'.length).trim();
11
+ const nameMatch = /^([a-zA-Z_][a-zA-Z0-9_-]*(?:\/[a-zA-Z_][a-zA-Z0-9_-]*)*)(?:\s+|$)/.exec(expression);
12
+ if (!nameMatch) {
13
+ throw new Error(`Invalid partial reference: ${source}`);
14
+ }
15
+
16
+ const name = nameMatch[1];
17
+ if (!PARTIAL_NAME_REGEX.test(name)) {
18
+ throw new Error(`Invalid partial name: ${name}`);
19
+ }
20
+
21
+ const argsSource = expression.slice(name.length).trim();
22
+ const args = parsePartialArgs(argsSource);
23
+
24
+ return { name, args };
25
+ }
26
+
27
+ function parsePartialArgs(source) {
28
+ if (!source) {
29
+ return {};
30
+ }
31
+
32
+ const args = {};
33
+ let index = 0;
34
+
35
+ while (index < source.length) {
36
+ while (index < source.length && /\s/.test(source[index])) {
37
+ index += 1;
38
+ }
39
+
40
+ if (index >= source.length) {
41
+ break;
42
+ }
43
+
44
+ const keyMatch = /^[a-zA-Z_][a-zA-Z0-9_]*/.exec(source.slice(index));
45
+ if (!keyMatch) {
46
+ throw new Error(`Invalid partial argument syntax near "${source.slice(index)}"`);
47
+ }
48
+
49
+ const key = keyMatch[0];
50
+ if (!IDENTIFIER_REGEX.test(key)) {
51
+ throw new Error(`Invalid partial argument key: ${key}`);
52
+ }
53
+ if (Object.prototype.hasOwnProperty.call(args, key)) {
54
+ throw new Error(`Duplicate partial argument: ${key}`);
55
+ }
56
+
57
+ index += key.length;
58
+ if (source[index] !== '=') {
59
+ throw new Error(`Expected "=" after partial argument "${key}"`);
60
+ }
61
+ index += 1;
62
+
63
+ if (source[index] === '"') {
64
+ let cursor = index + 1;
65
+ let escaped = false;
66
+
67
+ while (cursor < source.length) {
68
+ const char = source[cursor];
69
+ if (escaped) {
70
+ escaped = false;
71
+ cursor += 1;
72
+ continue;
73
+ }
74
+ if (char === '\\') {
75
+ escaped = true;
76
+ cursor += 1;
77
+ continue;
78
+ }
79
+ if (char === '"') {
80
+ break;
81
+ }
82
+ cursor += 1;
12
83
  }
13
84
 
14
- if (activeStack.includes(partialName)) {
15
- const cycle = [...activeStack, partialName].join(' -> ');
16
- throw new Error(`Circular partial reference detected: ${cycle}`);
85
+ if (cursor >= source.length || source[cursor] !== '"') {
86
+ throw new Error(`Unclosed string literal for partial argument "${key}"`);
17
87
  }
18
88
 
19
- const partialContent = partials.get(partialName);
20
- return this.resolveTemplate(String(partialContent || ''), partials, [...activeStack, partialName]);
21
- });
89
+ args[key] = JSON.parse(source.slice(index, cursor + 1));
90
+ index = cursor + 1;
91
+ continue;
92
+ }
93
+
94
+ if (source.startsWith('true', index) && isValueBoundary(source, index + 4)) {
95
+ args[key] = true;
96
+ index += 4;
97
+ continue;
98
+ }
99
+
100
+ if (source.startsWith('false', index) && isValueBoundary(source, index + 5)) {
101
+ args[key] = false;
102
+ index += 5;
103
+ continue;
104
+ }
105
+
106
+ throw new Error(`Unsupported partial argument value for "${key}"`);
22
107
  }
108
+
109
+ return args;
110
+ }
111
+
112
+ function isValueBoundary(source, index) {
113
+ return index >= source.length || /\s/.test(source[index]);
23
114
  }
@@ -1,12 +1,12 @@
1
1
  import { SlotResolver } from './slot-resolver.js';
2
2
  import { VariableResolver } from './variable-resolver.js';
3
3
  import { ControlFlowRenderer } from './control-flow-renderer.js';
4
- import { PartialResolver } from './partial-resolver.js';
4
+
5
+ const CONTENT_SLOT_PLACEHOLDER = '__ZEROPRESS_CONTENT_SLOT_4F30E5A44C1D__';
5
6
 
6
7
  export class ZeroPressEngine {
7
8
  constructor() {
8
9
  this.slotResolver = new SlotResolver();
9
- this.partialResolver = new PartialResolver();
10
10
  this.variableResolver = new VariableResolver();
11
11
  this.controlFlowRenderer = new ControlFlowRenderer({
12
12
  resolvePath: (data, path) => this.variableResolver.resolvePath(data, path),
@@ -35,9 +35,9 @@ export class ZeroPressEngine {
35
35
  }
36
36
 
37
37
  const renderData = this.combineRenderData(data, context);
38
- const renderedContent = this.renderTemplate(this.expandPartials(template), renderData);
39
- const layoutWithSlots = this.slotResolver.resolve(layout, this.themePackage.partials, renderedContent);
40
- return this.renderTemplate(this.expandPartials(layoutWithSlots), renderData);
38
+ const renderedContent = this.renderTemplate(template, renderData);
39
+ const layoutWithSlots = this.slotResolver.resolve(layout, this.themePackage.partials, CONTENT_SLOT_PLACEHOLDER);
40
+ return this.renderTemplate(layoutWithSlots, renderData).replaceAll(CONTENT_SLOT_PLACEHOLDER, renderedContent);
41
41
  }
42
42
 
43
43
  combineRenderData(data, context) {
@@ -50,22 +50,15 @@ export class ZeroPressEngine {
50
50
  }
51
51
 
52
52
  renderTemplate(template, data) {
53
- if (this.themePackage?.metadata?.runtime === '0.4') {
54
- return this.controlFlowRenderer.render(template, data, {
55
- escapeValues: true,
56
- rawPaths: new Set(['meta.head_tags']),
57
- rawPathPrefixes: new Set(['meta']),
58
- });
59
- }
60
-
61
- return this.variableResolver.resolve(template, data);
62
- }
63
-
64
- expandPartials(template) {
65
- if (this.themePackage?.metadata?.runtime !== '0.4') {
66
- return template;
53
+ if (this.themePackage?.metadata?.runtime !== '0.5') {
54
+ throw new Error(`Unsupported theme runtime: ${this.themePackage?.metadata?.runtime || 'unknown'}`);
67
55
  }
68
56
 
69
- return this.partialResolver.resolve(template, this.themePackage.partials);
57
+ return this.controlFlowRenderer.render(template, data, {
58
+ escapeValues: true,
59
+ rawPaths: new Set(['meta.head_tags']),
60
+ rawPathPrefixes: new Set(['meta']),
61
+ partials: this.themePackage.partials,
62
+ });
70
63
  }
71
64
  }