mikser-io-render-liquid 4.2.3 → 5.0.0
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/index.js +41 -30
- package/package.json +1 -1
- package/test/references.test.js +58 -13
package/index.js
CHANGED
|
@@ -148,13 +148,31 @@ export function parseReferences(source) {
|
|
|
148
148
|
// as wrong.
|
|
149
149
|
const FALLBACK_FILTERS = new Set(['default'])
|
|
150
150
|
const hasFallback = (value) => (value?.filters ?? []).some(f => FALLBACK_FILTERS.has(f.name))
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
151
|
+
|
|
152
|
+
// Any filter makes the value DERIVED, and a derived value is not an alias.
|
|
153
|
+
//
|
|
154
|
+
// `{% assign rows = specs | split: '|' %}` turns a string into a list, so a
|
|
155
|
+
// read of `rows[0].k` says nothing about `specs` having members. The source
|
|
156
|
+
// is still recorded — that is the real dependency — only the paths beneath
|
|
157
|
+
// it are dropped.
|
|
158
|
+
//
|
|
159
|
+
// Deliberately blunt. Classifying all 87 of liquid's filters by what they do
|
|
160
|
+
// to a value's shape was tried and removed: it is a table that has to track
|
|
161
|
+
// an engine's evolution, it is wrong in the direction that INVENTS required
|
|
162
|
+
// keys, and it exists per engine. What it bought was a few extra paths in a
|
|
163
|
+
// list that is now advisory, which is not worth the fragility. Being
|
|
164
|
+
// conservative here costs an occasional missing entry in a weak list;
|
|
165
|
+
// being clever cost a working page being reported as broken, twice.
|
|
166
|
+
const isDerived = (value) => !!(value?.filters ?? []).length
|
|
167
|
+
|
|
168
|
+
// Keyed by partial name and MERGED across call sites: one partial rendered
|
|
169
|
+
// eight times with different labels is one entry holding the union of what
|
|
170
|
+
// it is ever passed.
|
|
154
171
|
const partials = new Map()
|
|
155
172
|
const iterations = []
|
|
156
173
|
const assigns = []
|
|
157
174
|
|
|
175
|
+
|
|
158
176
|
// The path a tag ARGUMENT refers to, or null if it is a literal.
|
|
159
177
|
//
|
|
160
178
|
// Type, not text: LIQUID_IDENT_PATH is unanchored, so run over the source
|
|
@@ -278,16 +296,6 @@ export function parseReferences(source) {
|
|
|
278
296
|
// r.more %}` makes this template depend on `r.more`,
|
|
279
297
|
// and nothing recorded that — so a contract built from
|
|
280
298
|
// one file could not see a key consumed one file down.
|
|
281
|
-
// Whether a given argument carries a fallback filter,
|
|
282
|
-
// read from the RAW tag text: liquidjs parses a hash
|
|
283
|
-
// value down to a bare path token and drops the filter
|
|
284
|
-
// from the structured form, so there is nothing else to
|
|
285
|
-
// look at. Best-effort by necessity, and wrong only in
|
|
286
|
-
// the direction of calling something optional.
|
|
287
|
-
const rawArgs = String(node.token?.args ?? node.token?.content ?? '')
|
|
288
|
-
const argHasFallback = (name) => new RegExp(
|
|
289
|
-
`\\b${name}\\s*:\\s*[A-Za-z_$][\\w$.]*\\s*\\|\\s*(?:${[...FALLBACK_FILTERS].join('|')})\\b`,
|
|
290
|
-
).test(rawArgs)
|
|
291
299
|
for (const [name, token] of Object.entries(node.hash?.hash ?? {})) {
|
|
292
300
|
const path = pathOfToken(token)
|
|
293
301
|
if (!path) continue
|
|
@@ -295,7 +303,7 @@ export function parseReferences(source) {
|
|
|
295
303
|
// before the partial ever runs — so a partial
|
|
296
304
|
// rendered inside a loop reports what the loop
|
|
297
305
|
// hands it, not the loop variable's local name.
|
|
298
|
-
entry.args[name] = record(path, scope, guarded
|
|
306
|
+
entry.args[name] = record(path, scope, guarded)
|
|
299
307
|
}
|
|
300
308
|
// `{% render 'x' with item as t %}` — the same binding
|
|
301
309
|
// written positionally.
|
|
@@ -319,22 +327,16 @@ export function parseReferences(source) {
|
|
|
319
327
|
const raw = [...found][0] ?? null
|
|
320
328
|
const from = raw ? record(raw, scope, guarded) : null
|
|
321
329
|
if (node.key) {
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
330
|
+
// Bound according to what the filters did to the
|
|
331
|
+
// shape: unchanged keeps the source path, an
|
|
332
|
+
// element-yielding filter binds to `source[]`, and
|
|
333
|
+
// anything else is derived and binds nothing. The source
|
|
334
|
+
// is recorded above either way — that is the real
|
|
335
|
+
// dependency. `derived` travels with the assign because
|
|
336
|
+
// the closure walker applies these as bindings too and
|
|
337
|
+
// has no other way to know.
|
|
338
|
+
const derived = isDerived(node.value)
|
|
326
339
|
assigns.push({ key: node.key, from, ...(derived ? { derived } : {}) })
|
|
327
|
-
// Bound for the REST of this template, which is what
|
|
328
|
-
// `assign` means — everything after it sees the alias.
|
|
329
|
-
//
|
|
330
|
-
// NOT bound when the value passed through a filter: the
|
|
331
|
-
// result is DERIVED, so a read on it says nothing about
|
|
332
|
-
// the source. `{% assign rows = c.specs | split: '|' %}`
|
|
333
|
-
// turns a string into a list, and binding it would let a
|
|
334
|
-
// loop over `rows` report `specs[]` — a key the document
|
|
335
|
-
// does not have and cannot be given, since specs is the
|
|
336
|
-
// string being split. The source itself is still
|
|
337
|
-
// recorded above, which is the true dependency.
|
|
338
340
|
if (from && !derived) scope[node.key] = from
|
|
339
341
|
}
|
|
340
342
|
break
|
|
@@ -351,7 +353,16 @@ export function parseReferences(source) {
|
|
|
351
353
|
iterations.push({ item, collection })
|
|
352
354
|
const path = extractPath(collection)
|
|
353
355
|
const resolved = path ? record(path, scope, guarded) : null
|
|
354
|
-
|
|
356
|
+
// A filtered collection is derived for the same reason.
|
|
357
|
+
// liquidjs parses it down to a bare path token, so the
|
|
358
|
+
// pipe is looked for in the raw tag text — with quoted
|
|
359
|
+
// segments removed first, since liquid's own
|
|
360
|
+
// `split: '|'` would otherwise read as one.
|
|
361
|
+
const raw = String(node.token?.args ?? node.token?.content ?? '')
|
|
362
|
+
.replace(/'[^']*'|"[^"]*"/g, "''")
|
|
363
|
+
if (resolved && node.variable && !raw.includes('|')) {
|
|
364
|
+
inner[node.variable] = `${resolved}[]`
|
|
365
|
+
}
|
|
355
366
|
}
|
|
356
367
|
if (Array.isArray(node.templates)) walk(node.templates, inner, guarded)
|
|
357
368
|
if (Array.isArray(node.elseTemplates)) walk(node.elseTemplates, inner, guarded)
|
package/package.json
CHANGED
package/test/references.test.js
CHANGED
|
@@ -195,23 +195,39 @@ describe('liquid parseReferences: default: as a guard', () => {
|
|
|
195
195
|
assert.deepEqual(r.optional, [])
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
-
it('
|
|
198
|
+
it('does NOT see a default: on a partial argument, and that is deliberate', () => {
|
|
199
199
|
// liquidjs parses a hash value down to a bare path token and drops the
|
|
200
|
-
// filter, so
|
|
201
|
-
//
|
|
200
|
+
// filter, so the only place it survives is the raw tag text. Reading it
|
|
201
|
+
// from there worked, and was removed: a regex over tag source is the
|
|
202
|
+
// kind of engine-shaped machinery that has to track the engine, and it
|
|
203
|
+
// bought optionality for one filter in one position.
|
|
204
|
+
//
|
|
205
|
+
// The cost is honest and small — such a key lands in `missing` instead
|
|
206
|
+
// of `missingOptional`, both of which are inferred and labelled as such.
|
|
202
207
|
const r = parseReferences(
|
|
203
|
-
"{% render 'ui/tag', label: data.meta.after | default: 'x'
|
|
204
|
-
assert.ok(r.
|
|
205
|
-
assert.ok(!r.optional.includes('data.meta.
|
|
208
|
+
"{% render 'ui/tag', label: data.meta.after | default: 'x' %}")
|
|
209
|
+
assert.ok(r.variables.includes('data.meta.after'), 'still recorded as consumed')
|
|
210
|
+
assert.ok(!r.optional.includes('data.meta.after'))
|
|
206
211
|
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Aliases nested more than one level deep.
|
|
215
|
+
//
|
|
216
|
+
// A read is only useful as provenance if it names the SOURCE path. Each of
|
|
217
|
+
// these rebinds a value to a new name, twice, and the recorded path has to
|
|
218
|
+
// survive both hops — a contract naming `field.label` describes nothing a
|
|
219
|
+
// document author can act on.
|
|
220
|
+
describe('liquid parseReferences: aliases through nesting', () => {
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// A fallback filter is a guard.
|
|
224
|
+
//
|
|
225
|
+
// `{{ hero.title | default: meta.title }}` renders correctly for a document
|
|
226
|
+
// that omits hero.title — the layout was written to work without it, exactly as
|
|
227
|
+
// with `{% if %}`. Calling it required reports a working page as wrong, and
|
|
228
|
+
// `missing` is the one list that must only ever mean "probably wrong".
|
|
229
|
+
describe('liquid parseReferences: default: as a guard', () => {
|
|
207
230
|
|
|
208
|
-
it('resolves the defaulted argument through scope, like any other', () => {
|
|
209
|
-
const r = parseReferences(
|
|
210
|
-
'{% assign r = data.meta.results %}'
|
|
211
|
-
+ "{% render 'ui/tag', label: r.afterLabel | default: 'x' %}")
|
|
212
|
-
assert.ok(r.optional.includes('data.meta.results.afterLabel'),
|
|
213
|
-
`optional: ${r.optional.join(', ')}`)
|
|
214
|
-
})
|
|
215
231
|
})
|
|
216
232
|
|
|
217
233
|
// A filtered value is DERIVED, not an alias.
|
|
@@ -244,6 +260,35 @@ describe('liquid parseReferences: derived values are not aliases', () => {
|
|
|
244
260
|
assert.ok(!r.assigns.find(a => a.key === 'hero')?.derived)
|
|
245
261
|
})
|
|
246
262
|
|
|
263
|
+
it('drops paths under ANY filtered value, not just type-changing ones', () => {
|
|
264
|
+
// Blunt on purpose. Classifying liquid's 87 filters by what they do to a
|
|
265
|
+
// value's shape was tried and removed: the table has to track the
|
|
266
|
+
// engine, it is wrong in the direction that INVENTS required keys, and
|
|
267
|
+
// it exists per engine. `sort` really does preserve element type, and
|
|
268
|
+
// the key it would recover lands in an advisory list — not worth it.
|
|
269
|
+
const r = parseReferences(
|
|
270
|
+
'{% assign rows = data.meta.items | sort %}'
|
|
271
|
+
+ '{% for row in rows %}{{ row.name }}{% endfor %}')
|
|
272
|
+
assert.ok(r.variables.includes('data.meta.items'), 'the dependency itself is still recorded')
|
|
273
|
+
assert.ok(!r.variables.some(v => v.startsWith('data.meta.items[')),
|
|
274
|
+
`extended a path through a filter: ${r.variables.join(', ')}`)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('treats a filtered FOR collection the same way', () => {
|
|
278
|
+
// liquidjs parses the collection down to a bare path token, so the pipe
|
|
279
|
+
// is looked for in the raw text — with quotes stripped, since liquid's
|
|
280
|
+
// own `split: '|'` would otherwise read as one.
|
|
281
|
+
const r = parseReferences("{% for t in data.meta.tags | split: '|' %}{{ t.n }}{% endfor %}")
|
|
282
|
+
assert.ok(r.variables.includes('data.meta.tags'))
|
|
283
|
+
assert.ok(!r.variables.some(v => v.startsWith('data.meta.tags[')),
|
|
284
|
+
`fabricated a member of a string: ${r.variables.join(', ')}`)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('leaves an unfiltered collection alone', () => {
|
|
288
|
+
const r = parseReferences('{% for t in data.meta.tags %}{{ t.n }}{% endfor %}')
|
|
289
|
+
assert.ok(r.variables.includes('data.meta.tags[].n'))
|
|
290
|
+
})
|
|
291
|
+
|
|
247
292
|
it('handles the real shape: split, loop, split again', () => {
|
|
248
293
|
// Verbatim from the project that surfaced this.
|
|
249
294
|
const r = parseReferences(
|