@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 +2 -2
- package/src/api/build-site.js +62 -0
- package/src/render/control-flow-renderer.js +198 -52
- package/src/render/partial-resolver.js +105 -14
- package/src/render/zeropress-engine.js +13 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.1
|
|
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.
|
|
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"
|
package/src/api/build-site.js
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
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('#
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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 =
|
|
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).
|
|
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
|
|
184
|
-
return
|
|
185
|
-
? this.renderNodes(
|
|
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
|
|
190
|
-
return
|
|
191
|
-
? this.renderNodes(
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 (
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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(
|
|
39
|
-
const layoutWithSlots = this.slotResolver.resolve(layout, this.themePackage.partials,
|
|
40
|
-
return this.renderTemplate(
|
|
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
|
|
54
|
-
|
|
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.
|
|
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
|
}
|