@strav/view 0.2.7 → 0.2.8
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 +1 -1
- package/src/cache.ts +1 -0
- package/src/compiler.ts +40 -2
- package/src/engine.ts +69 -7
- package/src/tokenizer.ts +3 -0
package/package.json
CHANGED
package/src/cache.ts
CHANGED
package/src/compiler.ts
CHANGED
|
@@ -7,9 +7,10 @@ export interface CompilationResult {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface StackEntry {
|
|
10
|
-
type: 'if' | 'each' | 'section'
|
|
10
|
+
type: 'if' | 'each' | 'section' | 'push' | 'prepend'
|
|
11
11
|
line: number
|
|
12
12
|
blockName?: string
|
|
13
|
+
stackName?: string
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function escapeJs(str: string): string {
|
|
@@ -94,6 +95,7 @@ export function compile(tokens: Token[]): CompilationResult {
|
|
|
94
95
|
|
|
95
96
|
lines.push('let __out = "";')
|
|
96
97
|
lines.push('const __blocks = {};')
|
|
98
|
+
lines.push('const __stacks = {};')
|
|
97
99
|
|
|
98
100
|
for (const token of tokens) {
|
|
99
101
|
switch (token.type) {
|
|
@@ -147,7 +149,7 @@ export function compile(tokens: Token[]): CompilationResult {
|
|
|
147
149
|
throw new TemplateError(`Unclosed @${unclosed.type} block (opened at line ${unclosed.line})`)
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
lines.push('return { output: __out, blocks: __blocks };')
|
|
152
|
+
lines.push('return { output: __out, blocks: __blocks, stacks: __stacks };')
|
|
151
153
|
|
|
152
154
|
return { code: lines.join('\n'), layout }
|
|
153
155
|
}
|
|
@@ -274,6 +276,40 @@ function compileDirective(
|
|
|
274
276
|
)
|
|
275
277
|
break
|
|
276
278
|
|
|
279
|
+
case 'push': {
|
|
280
|
+
if (!token.args) throw new TemplateError(`@push requires a name at line ${token.line}`)
|
|
281
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
282
|
+
const nameStr = JSON.stringify(name)
|
|
283
|
+
// Initialize stack array if it doesn't exist, then capture content and push
|
|
284
|
+
lines.push(`if (!__stacks[${nameStr}]) __stacks[${nameStr}] = [];`)
|
|
285
|
+
lines.push(`__stacks[${nameStr}].push((function() { let __out = "";`)
|
|
286
|
+
stack.push({ type: 'push', line: token.line, stackName: name })
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'prepend': {
|
|
291
|
+
if (!token.args) throw new TemplateError(`@prepend requires a name at line ${token.line}`)
|
|
292
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
293
|
+
const nameStr = JSON.stringify(name)
|
|
294
|
+
// Initialize stack array if it doesn't exist, then capture content and unshift
|
|
295
|
+
lines.push(`if (!__stacks[${nameStr}]) __stacks[${nameStr}] = [];`)
|
|
296
|
+
lines.push(`__stacks[${nameStr}].unshift((function() { let __out = "";`)
|
|
297
|
+
stack.push({ type: 'prepend', line: token.line, stackName: name })
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case 'stack': {
|
|
302
|
+
if (!token.args) throw new TemplateError(`@stack requires a name at line ${token.line}`)
|
|
303
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
304
|
+
const nameStr = JSON.stringify(name)
|
|
305
|
+
// Output joined stack content - merge local and passed stacks
|
|
306
|
+
lines.push(`{`)
|
|
307
|
+
lines.push(` const __mergedStack = [...((__data.__stacks && __data.__stacks[${nameStr}]) || []), ...(__stacks[${nameStr}] || [])];`)
|
|
308
|
+
lines.push(` __out += __mergedStack.join('');`)
|
|
309
|
+
lines.push(`}`)
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
case 'end': {
|
|
278
314
|
if (!stack.length) {
|
|
279
315
|
throw new TemplateError(`Unexpected @end at line ${token.line} — no open block`)
|
|
@@ -281,6 +317,8 @@ function compileDirective(
|
|
|
281
317
|
const top = stack.pop()!
|
|
282
318
|
if (top.type === 'section') {
|
|
283
319
|
lines.push(` return __out; })();`)
|
|
320
|
+
} else if (top.type === 'push' || top.type === 'prepend') {
|
|
321
|
+
lines.push(` return __out; })());`)
|
|
284
322
|
} else if (top.type === 'each') {
|
|
285
323
|
lines.push(` }`) // close for loop
|
|
286
324
|
lines.push(`}`) // close block scope
|
package/src/engine.ts
CHANGED
|
@@ -48,7 +48,8 @@ export default class ViewEngine {
|
|
|
48
48
|
private async renderWithDepth(
|
|
49
49
|
name: string,
|
|
50
50
|
data: Record<string, unknown>,
|
|
51
|
-
depth: number
|
|
51
|
+
depth: number,
|
|
52
|
+
parentStacks: Record<string, string[]> = {}
|
|
52
53
|
): Promise<string> {
|
|
53
54
|
if (depth > MAX_INCLUDE_DEPTH) {
|
|
54
55
|
throw new TemplateError(
|
|
@@ -58,21 +59,82 @@ export default class ViewEngine {
|
|
|
58
59
|
|
|
59
60
|
const entry = await this.resolve(name)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Create an include function that merges stacks from includes
|
|
63
|
+
const includeFn: IncludeFn = async (includeName, includeData) => {
|
|
64
|
+
const includeResult = await this.renderWithDepthInternal(
|
|
65
|
+
includeName,
|
|
66
|
+
{ ...data, ...includeData },
|
|
67
|
+
depth + 1,
|
|
68
|
+
{}
|
|
69
|
+
)
|
|
70
|
+
// Merge include's stacks into parent stacks
|
|
71
|
+
this.mergeStacks(parentStacks, includeResult.stacks)
|
|
72
|
+
return includeResult.output
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
// Pass parent stacks to the template
|
|
76
|
+
const dataWithStacks = { ...data, __stacks: { ...parentStacks } }
|
|
77
|
+
const result = await entry.fn(dataWithStacks, includeFn)
|
|
78
|
+
|
|
79
|
+
// Merge current template's stacks with parent stacks
|
|
80
|
+
this.mergeStacks(parentStacks, result.stacks)
|
|
66
81
|
|
|
67
|
-
// Layout inheritance: render child first, then render layout with blocks merged
|
|
82
|
+
// Layout inheritance: render child first, then render layout with blocks and stacks merged
|
|
68
83
|
if (entry.layout) {
|
|
69
|
-
const layoutData = { ...data, ...result.blocks }
|
|
70
|
-
return this.renderWithDepth(entry.layout, layoutData, depth + 1)
|
|
84
|
+
const layoutData = { ...data, ...result.blocks, __stacks: parentStacks }
|
|
85
|
+
return this.renderWithDepth(entry.layout, layoutData, depth + 1, parentStacks)
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
return result.output
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
private async renderWithDepthInternal(
|
|
92
|
+
name: string,
|
|
93
|
+
data: Record<string, unknown>,
|
|
94
|
+
depth: number,
|
|
95
|
+
parentStacks: Record<string, string[]> = {}
|
|
96
|
+
): Promise<{ output: string; stacks: Record<string, string[]> }> {
|
|
97
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
98
|
+
throw new TemplateError(
|
|
99
|
+
`Maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded — possible circular include`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry = await this.resolve(name)
|
|
104
|
+
|
|
105
|
+
const includeFn: IncludeFn = async (includeName, includeData) => {
|
|
106
|
+
const includeResult = await this.renderWithDepthInternal(
|
|
107
|
+
includeName,
|
|
108
|
+
{ ...data, ...includeData },
|
|
109
|
+
depth + 1,
|
|
110
|
+
{}
|
|
111
|
+
)
|
|
112
|
+
this.mergeStacks(parentStacks, includeResult.stacks)
|
|
113
|
+
return includeResult.output
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dataWithStacks = { ...data, __stacks: { ...parentStacks } }
|
|
117
|
+
const result = await entry.fn(dataWithStacks, includeFn)
|
|
118
|
+
this.mergeStacks(parentStacks, result.stacks)
|
|
119
|
+
|
|
120
|
+
if (entry.layout) {
|
|
121
|
+
const layoutData = { ...data, ...result.blocks, __stacks: parentStacks }
|
|
122
|
+
const layoutOutput = await this.renderWithDepth(entry.layout, layoutData, depth + 1, parentStacks)
|
|
123
|
+
return { output: layoutOutput, stacks: parentStacks }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { output: result.output, stacks: parentStacks }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private mergeStacks(target: Record<string, string[]>, source: Record<string, string[]>): void {
|
|
130
|
+
for (const [key, values] of Object.entries(source)) {
|
|
131
|
+
if (!target[key]) {
|
|
132
|
+
target[key] = []
|
|
133
|
+
}
|
|
134
|
+
target[key].push(...values)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
76
138
|
private async resolve(name: string): Promise<CacheEntry> {
|
|
77
139
|
const cached = this.cache.get(name)
|
|
78
140
|
|