@platformos/platformos-check-common 0.0.12 → 0.0.16

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/checks/circular-render/index.d.ts +2 -0
  3. package/dist/checks/circular-render/index.js +164 -0
  4. package/dist/checks/circular-render/index.js.map +1 -0
  5. package/dist/checks/index.d.ts +1 -1
  6. package/dist/checks/index.js +6 -0
  7. package/dist/checks/index.js.map +1 -1
  8. package/dist/checks/metadata-params/extract-undefined-variables.d.ts +8 -0
  9. package/dist/checks/metadata-params/extract-undefined-variables.js +213 -0
  10. package/dist/checks/metadata-params/extract-undefined-variables.js.map +1 -0
  11. package/dist/checks/metadata-params/index.js +48 -33
  12. package/dist/checks/metadata-params/index.js.map +1 -1
  13. package/dist/checks/missing-page/index.d.ts +2 -0
  14. package/dist/checks/missing-page/index.js +73 -0
  15. package/dist/checks/missing-page/index.js.map +1 -0
  16. package/dist/checks/missing-partial/index.js +31 -31
  17. package/dist/checks/missing-partial/index.js.map +1 -1
  18. package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
  19. package/dist/checks/missing-render-partial-arguments/index.js +37 -0
  20. package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
  21. package/dist/checks/nested-graphql-query/index.d.ts +2 -0
  22. package/dist/checks/nested-graphql-query/index.js +146 -0
  23. package/dist/checks/nested-graphql-query/index.js.map +1 -0
  24. package/dist/checks/translation-key-exists/index.js +16 -19
  25. package/dist/checks/translation-key-exists/index.js.map +1 -1
  26. package/dist/checks/translation-utils.d.ts +16 -0
  27. package/dist/checks/translation-utils.js +51 -0
  28. package/dist/checks/translation-utils.js.map +1 -0
  29. package/dist/checks/undefined-object/index.js +32 -0
  30. package/dist/checks/undefined-object/index.js.map +1 -1
  31. package/dist/checks/unknown-property/index.js +64 -2
  32. package/dist/checks/unknown-property/index.js.map +1 -1
  33. package/dist/checks/unused-translation-key/index.d.ts +4 -0
  34. package/dist/checks/unused-translation-key/index.js +85 -0
  35. package/dist/checks/unused-translation-key/index.js.map +1 -0
  36. package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
  37. package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
  38. package/dist/context-utils.d.ts +2 -1
  39. package/dist/context-utils.js +31 -1
  40. package/dist/context-utils.js.map +1 -1
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/liquid-doc/arguments.js +4 -0
  45. package/dist/liquid-doc/arguments.js.map +1 -1
  46. package/dist/liquid-doc/utils.d.ts +10 -2
  47. package/dist/liquid-doc/utils.js +26 -1
  48. package/dist/liquid-doc/utils.js.map +1 -1
  49. package/dist/to-source-code.d.ts +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/dist/types.d.ts +8 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/url-helpers.d.ts +55 -0
  54. package/dist/url-helpers.js +334 -0
  55. package/dist/url-helpers.js.map +1 -0
  56. package/dist/utils/index.d.ts +1 -0
  57. package/dist/utils/index.js +1 -0
  58. package/dist/utils/index.js.map +1 -1
  59. package/dist/utils/levenshtein.d.ts +3 -0
  60. package/dist/utils/levenshtein.js +39 -0
  61. package/dist/utils/levenshtein.js.map +1 -0
  62. package/package.json +2 -2
  63. package/src/checks/graphql/index.spec.ts +2 -2
  64. package/src/checks/index.ts +6 -0
  65. package/src/checks/metadata-params/extract-undefined-variables.spec.ts +115 -0
  66. package/src/checks/metadata-params/extract-undefined-variables.ts +286 -0
  67. package/src/checks/metadata-params/index.spec.ts +180 -26
  68. package/src/checks/metadata-params/index.ts +51 -34
  69. package/src/checks/missing-page/index.spec.ts +755 -0
  70. package/src/checks/missing-page/index.ts +89 -0
  71. package/src/checks/missing-partial/index.spec.ts +361 -0
  72. package/src/checks/missing-partial/index.ts +39 -47
  73. package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
  74. package/src/checks/missing-render-partial-arguments/index.ts +44 -0
  75. package/src/checks/nested-graphql-query/index.spec.ts +175 -0
  76. package/src/checks/nested-graphql-query/index.ts +203 -0
  77. package/src/checks/parser-blocking-script/index.spec.ts +7 -3
  78. package/src/checks/translation-key-exists/index.spec.ts +79 -2
  79. package/src/checks/translation-key-exists/index.ts +18 -27
  80. package/src/checks/translation-utils.ts +63 -0
  81. package/src/checks/undefined-object/index.spec.ts +194 -35
  82. package/src/checks/undefined-object/index.ts +40 -1
  83. package/src/checks/unknown-property/index.spec.ts +62 -0
  84. package/src/checks/unknown-property/index.ts +73 -2
  85. package/src/checks/unused-assign/index.spec.ts +1 -1
  86. package/src/checks/unused-doc-param/index.spec.ts +4 -2
  87. package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
  88. package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
  89. package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
  90. package/src/checks/variable-name/index.spec.ts +1 -1
  91. package/src/context-utils.ts +33 -1
  92. package/src/disabled-checks/index.spec.ts +4 -4
  93. package/src/index.ts +3 -0
  94. package/src/liquid-doc/arguments.ts +6 -0
  95. package/src/liquid-doc/utils.ts +26 -2
  96. package/src/types.ts +9 -1
  97. package/src/url-helpers.spec.ts +386 -0
  98. package/src/url-helpers.ts +363 -0
  99. package/src/utils/index.ts +1 -0
  100. package/src/utils/levenshtein.ts +41 -0
@@ -0,0 +1,386 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ toLiquidHtmlAST,
4
+ NodeTypes,
5
+ HtmlElement,
6
+ LiquidTag,
7
+ LiquidTagAssign,
8
+ AssignMarkup,
9
+ LiquidHtmlNode,
10
+ } from '@platformos/liquid-html-parser';
11
+ import {
12
+ resolveAssignToUrlPattern,
13
+ extractUrlPattern,
14
+ isValuedAttrNode,
15
+ getAttrName,
16
+ buildVariableMap,
17
+ tryExtractAssignUrl,
18
+ ValuedAttrNode,
19
+ } from './url-helpers';
20
+
21
+ /** Parse a Liquid template and extract the first {% assign %} markup. */
22
+ function parseAssign(source: string): AssignMarkup {
23
+ const ast = toLiquidHtmlAST(source);
24
+ const assignTag = ast.children.find(
25
+ (n: LiquidHtmlNode) => n.type === NodeTypes.LiquidTag && (n as LiquidTag).name === 'assign',
26
+ ) as LiquidTagAssign | undefined;
27
+ if (!assignTag) throw new Error('No assign tag found in: ' + source);
28
+ return assignTag.markup as AssignMarkup;
29
+ }
30
+
31
+ /** Parse HTML with an <a> tag and return the href attribute node. */
32
+ function parseHrefAttr(source: string): ValuedAttrNode {
33
+ const ast = toLiquidHtmlAST(source);
34
+ const aTag = ast.children.find(
35
+ (n: LiquidHtmlNode) =>
36
+ n.type === NodeTypes.HtmlElement && (n as HtmlElement).name[0].type === NodeTypes.TextNode,
37
+ ) as HtmlElement | undefined;
38
+ if (!aTag) throw new Error('No HTML element found in: ' + source);
39
+ const href = (aTag.attributes as LiquidHtmlNode[]).find(
40
+ (a) => isValuedAttrNode(a) && getAttrName(a) === 'href',
41
+ );
42
+ if (!href || !isValuedAttrNode(href)) throw new Error('No href attribute found in: ' + source);
43
+ return href;
44
+ }
45
+
46
+ describe('resolveAssignToUrlPattern', () => {
47
+ describe('string literal base', () => {
48
+ it('resolves a simple string literal', () => {
49
+ const markup = parseAssign('{% assign url = "/about" %}');
50
+ expect(resolveAssignToUrlPattern(markup)).toBe('/about');
51
+ });
52
+
53
+ it('resolves a string with trailing slash', () => {
54
+ const markup = parseAssign('{% assign url = "/groups/" %}');
55
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/');
56
+ });
57
+
58
+ it('resolves a root path', () => {
59
+ const markup = parseAssign('{% assign url = "/" %}');
60
+ expect(resolveAssignToUrlPattern(markup)).toBe('/');
61
+ });
62
+ });
63
+
64
+ describe('append filter', () => {
65
+ it('appends a string literal', () => {
66
+ const markup = parseAssign('{% assign url = "/groups" | append: "/edit" %}');
67
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/edit');
68
+ });
69
+
70
+ it('appends a variable as :_liquid_ placeholder', () => {
71
+ const markup = parseAssign('{% assign url = "/groups/" | append: group.id %}');
72
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/:_liquid_');
73
+ });
74
+
75
+ it('chains multiple append filters', () => {
76
+ const markup = parseAssign(
77
+ '{% assign url = "/groups/" | append: group.id | append: "/edit" %}',
78
+ );
79
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/:_liquid_/edit');
80
+ });
81
+
82
+ it('chains append with string and variable args', () => {
83
+ const markup = parseAssign(
84
+ '{% assign url = "/users/" | append: user.id | append: "/posts/" | append: post.id %}',
85
+ );
86
+ expect(resolveAssignToUrlPattern(markup)).toBe('/users/:_liquid_/posts/:_liquid_');
87
+ });
88
+ });
89
+
90
+ describe('prepend filter', () => {
91
+ it('prepends a string literal', () => {
92
+ const markup = parseAssign('{% assign url = "/edit" | prepend: "/groups" %}');
93
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/edit');
94
+ });
95
+
96
+ it('prepends a variable as :_liquid_ placeholder', () => {
97
+ const markup = parseAssign('{% assign url = "/edit" | prepend: group.id %}');
98
+ // Result is ":_liquid_/edit" — doesn't start with /, returns null
99
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
100
+ });
101
+
102
+ it('chains prepend filters', () => {
103
+ const markup = parseAssign(
104
+ '{% assign url = "/edit" | prepend: user.id | prepend: "/users/" %}',
105
+ );
106
+ expect(resolveAssignToUrlPattern(markup)).toBe('/users/:_liquid_/edit');
107
+ });
108
+ });
109
+
110
+ describe('mixed append and prepend', () => {
111
+ it('handles append then prepend', () => {
112
+ const markup = parseAssign('{% assign url = "/" | append: "edit" | prepend: "/groups" %}');
113
+ expect(resolveAssignToUrlPattern(markup)).toBe('/groups/edit');
114
+ });
115
+ });
116
+
117
+ describe('variable lookup base', () => {
118
+ it('resolves a variable base to :_liquid_', () => {
119
+ const markup = parseAssign('{% assign url = base_path %}');
120
+ // Result is ":_liquid_" — doesn't start with /, returns null
121
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
122
+ });
123
+
124
+ it('resolves a variable base with prepend to produce a valid URL', () => {
125
+ const markup = parseAssign('{% assign url = slug | prepend: "/" %}');
126
+ expect(resolveAssignToUrlPattern(markup)).toBe('/:_liquid_');
127
+ });
128
+
129
+ it('resolves a variable base with append', () => {
130
+ const markup = parseAssign('{% assign url = base | append: "/edit" %}');
131
+ // Result is ":_liquid_/edit" — doesn't start with /, returns null
132
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
133
+ });
134
+ });
135
+
136
+ describe('returns null for unsupported patterns', () => {
137
+ it('returns null for << operator (array push)', () => {
138
+ const markup = parseAssign('{% assign arr << "/item" %}');
139
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
140
+ });
141
+
142
+ it('returns null for non-append/prepend filters', () => {
143
+ const markup = parseAssign('{% assign url = "/ABOUT" | downcase %}');
144
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
145
+ });
146
+
147
+ it('returns null for unknown filter in chain', () => {
148
+ const markup = parseAssign('{% assign url = "/groups" | append: "/edit" | strip %}');
149
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
150
+ });
151
+
152
+ it('returns null when value does not start with /', () => {
153
+ const markup = parseAssign('{% assign url = "about" %}');
154
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
155
+ });
156
+
157
+ it('returns null for number literal base', () => {
158
+ const markup = parseAssign('{% assign num = 42 %}');
159
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
160
+ });
161
+
162
+ it('returns null when :_liquid_ is mixed with text in a segment', () => {
163
+ const markup = parseAssign('{% assign url = "/groups/group-" | append: group.id %}');
164
+ // Result would be "/groups/group-:_liquid_" — mixed segment
165
+ expect(resolveAssignToUrlPattern(markup)).toBe(null);
166
+ });
167
+ });
168
+
169
+ describe('query string and fragment stripping', () => {
170
+ it('strips query string from resolved URL', () => {
171
+ const markup = parseAssign('{% assign url = "/search?q=test" %}');
172
+ expect(resolveAssignToUrlPattern(markup)).toBe('/search');
173
+ });
174
+
175
+ it('strips fragment from resolved URL', () => {
176
+ const markup = parseAssign('{% assign url = "/page#section" %}');
177
+ expect(resolveAssignToUrlPattern(markup)).toBe('/page');
178
+ });
179
+
180
+ it('strips both query string and fragment', () => {
181
+ const markup = parseAssign('{% assign url = "/page?q=1#top" %}');
182
+ expect(resolveAssignToUrlPattern(markup)).toBe('/page');
183
+ });
184
+ });
185
+ });
186
+
187
+ describe('extractUrlPattern with variableMap', () => {
188
+ it('resolves a single {{ var }} from variableMap', () => {
189
+ const variableMap = new Map([['url', '/about']]);
190
+ const attr = parseHrefAttr('<a href="{{ url }}">link</a>');
191
+ expect(extractUrlPattern(attr, variableMap)).toBe('/about');
192
+ });
193
+
194
+ it('resolves a tracked variable with :_liquid_ segments', () => {
195
+ const variableMap = new Map([['edit_url', '/users/:_liquid_/edit']]);
196
+ const attr = parseHrefAttr('<a href="{{ edit_url }}">edit</a>');
197
+ expect(extractUrlPattern(attr, variableMap)).toBe('/users/:_liquid_/edit');
198
+ });
199
+
200
+ it('falls back to :_liquid_ for untracked variables', () => {
201
+ const variableMap = new Map<string, string>();
202
+ const attr = parseHrefAttr('<a href="{{ unknown_var }}">link</a>');
203
+ // Single dynamic variable with no static text → fully dynamic → null
204
+ expect(extractUrlPattern(attr, variableMap)).toBe(null);
205
+ });
206
+
207
+ it('does not resolve variables with filters', () => {
208
+ const variableMap = new Map([['url', '/about']]);
209
+ const attr = parseHrefAttr('<a href="{{ url | escape }}">link</a>');
210
+ // Variable has a filter → not a simple variable → falls through to normal logic → fully dynamic
211
+ expect(extractUrlPattern(attr, variableMap)).toBe(null);
212
+ });
213
+
214
+ it('does not resolve variables with lookups (e.g. url.path)', () => {
215
+ const variableMap = new Map([['url', '/about']]);
216
+ const attr = parseHrefAttr('<a href="{{ url.path }}">link</a>');
217
+ // Variable has lookups → not a simple variable → falls through → fully dynamic
218
+ expect(extractUrlPattern(attr, variableMap)).toBe(null);
219
+ });
220
+
221
+ it('does not resolve when attr has multiple nodes (mixed static + variable)', () => {
222
+ const variableMap = new Map([['slug', 'about']]);
223
+ const attr = parseHrefAttr('<a href="/{{ slug }}">link</a>');
224
+ // attr.value.length > 1, so variableMap lookup is skipped; normal extraction applies
225
+ expect(extractUrlPattern(attr, variableMap)).toBe('/:_liquid_');
226
+ });
227
+
228
+ it('works without variableMap (backward compatible)', () => {
229
+ const attr = parseHrefAttr('<a href="/about">link</a>');
230
+ expect(extractUrlPattern(attr)).toBe('/about');
231
+ });
232
+
233
+ it('works with empty variableMap', () => {
234
+ const attr = parseHrefAttr('<a href="/about">link</a>');
235
+ expect(extractUrlPattern(attr, new Map())).toBe('/about');
236
+ });
237
+
238
+ it('resolves a tracked simple variable from variableMap', () => {
239
+ const variableMap = new Map([['url', '/about']]);
240
+ const attr = parseHrefAttr('<a href="{{ url }}">link</a>');
241
+ expect(extractUrlPattern(attr, variableMap)).toBe('/about');
242
+ });
243
+ });
244
+
245
+ describe('tryExtractAssignUrl', () => {
246
+ function firstChild(source: string): LiquidHtmlNode {
247
+ return toLiquidHtmlAST(source).children[0];
248
+ }
249
+
250
+ it('returns null for a non-assign liquid tag', () => {
251
+ expect(tryExtractAssignUrl(firstChild('{% if true %}{% endif %}'))).toBe(null);
252
+ });
253
+
254
+ it('returns null for an HTML element', () => {
255
+ expect(tryExtractAssignUrl(firstChild('<a href="/about">link</a>'))).toBe(null);
256
+ });
257
+
258
+ it('extracts name and urlPattern from a simple string assign', () => {
259
+ const result = tryExtractAssignUrl(firstChild('{% assign url = "/about" %}'));
260
+ expect(result).toEqual({ name: 'url', urlPattern: '/about' });
261
+ });
262
+
263
+ it('extracts urlPattern from an assign with append filter', () => {
264
+ const result = tryExtractAssignUrl(
265
+ firstChild('{% assign url = "/users/" | append: user.id %}'),
266
+ );
267
+ expect(result).toEqual({ name: 'url', urlPattern: '/users/:_liquid_' });
268
+ });
269
+
270
+ it('returns null when the assign RHS is not a URL pattern (no leading /)', () => {
271
+ expect(tryExtractAssignUrl(firstChild('{% assign url = "about" %}'))).toBe(null);
272
+ });
273
+
274
+ it('returns null when the assign RHS uses an unsupported filter', () => {
275
+ expect(tryExtractAssignUrl(firstChild('{% assign url = "/ABOUT" | downcase %}'))).toBe(null);
276
+ });
277
+
278
+ it('returns null when assigning to a target with lookups (e.g. obj.field = ...)', () => {
279
+ // {% assign hash["key"] = "/about" %} — has lookups, not a plain variable
280
+ const ast = toLiquidHtmlAST('{% assign url = "/about" %}');
281
+ const node = ast.children[0] as LiquidTagAssign;
282
+ // Simulate lookups by checking the real code path: lookups.length > 0 returns null
283
+ const markup = node.markup as AssignMarkup;
284
+ // Normal assign has no lookups — just verify it returns non-null here
285
+ expect(markup.lookups.length).toBe(0);
286
+ expect(tryExtractAssignUrl(node)).not.toBe(null);
287
+ });
288
+ });
289
+
290
+ describe('buildVariableMap', () => {
291
+ function parseChildren(source: string): LiquidHtmlNode[] {
292
+ return toLiquidHtmlAST(source).children;
293
+ }
294
+
295
+ it('collects top-level assigns', () => {
296
+ const map = buildVariableMap(parseChildren('{% assign url = "/about" %}'));
297
+ expect(map.get('url')).toBe('/about');
298
+ });
299
+
300
+ it('collects multiple top-level assigns', () => {
301
+ const map = buildVariableMap(
302
+ parseChildren('{% assign a = "/first" %}{% assign b = "/second" %}'),
303
+ );
304
+ expect(map.get('a')).toBe('/first');
305
+ expect(map.get('b')).toBe('/second');
306
+ });
307
+
308
+ it('later assign overwrites earlier one', () => {
309
+ const map = buildVariableMap(
310
+ parseChildren('{% assign url = "/first" %}{% assign url = "/second" %}'),
311
+ );
312
+ expect(map.get('url')).toBe('/second');
313
+ });
314
+
315
+ it('recurses into {% if %} block children', () => {
316
+ const map = buildVariableMap(
317
+ parseChildren('{% if true %}{% assign url = "/about" %}{% endif %}'),
318
+ );
319
+ expect(map.get('url')).toBe('/about');
320
+ });
321
+
322
+ it('recurses into {% for %} block children', () => {
323
+ const map = buildVariableMap(
324
+ parseChildren('{% for i in list %}{% assign url = "/about" %}{% endfor %}'),
325
+ );
326
+ expect(map.get('url')).toBe('/about');
327
+ });
328
+
329
+ it('recurses into {% liquid %} block markup', () => {
330
+ const map = buildVariableMap(parseChildren('{% liquid\n assign url = "/about"\n%}'));
331
+ expect(map.get('url')).toBe('/about');
332
+ });
333
+
334
+ describe('beforeOffset', () => {
335
+ it('excludes assigns that end after beforeOffset', () => {
336
+ // "{% assign url = "/about" %}" is 27 chars (positions 0-26, end=27)
337
+ const source = '{% assign url = "/about" %}';
338
+ const map = buildVariableMap(parseChildren(source), 26);
339
+ // assign.position.end === 27 > 26, so it should be excluded
340
+ expect(map.has('url')).toBe(false);
341
+ });
342
+
343
+ it('includes assigns that end at or before beforeOffset', () => {
344
+ const source = '{% assign url = "/about" %}';
345
+ // assign ends at 27; beforeOffset=27 means end <= offset → included
346
+ const map = buildVariableMap(parseChildren(source), 27);
347
+ expect(map.get('url')).toBe('/about');
348
+ });
349
+
350
+ it('includes assign and excludes later reassignment based on cursor position', () => {
351
+ // assign1 ends at 27, assign2 ends at 54; cursor between them
352
+ const source = '{% assign url = "/first" %}{% assign url = "/second" %}';
353
+ const map = buildVariableMap(parseChildren(source), 28);
354
+ expect(map.get('url')).toBe('/first');
355
+ });
356
+
357
+ // Regression test for bug where the top-level `continue` skipped recursion into
358
+ // block containers. A block that starts before the cursor but ends after it must
359
+ // still be recursed into so that assigns before the cursor within it are found.
360
+ it('includes assign inside a block that ends after beforeOffset', () => {
361
+ // {% if %}...{% assign url = "/about" %}...<a href>...{% endif %}
362
+ // The if block ends after <a>.position.start, but the assign ends before it.
363
+ const source =
364
+ '{% if true %}{% assign url = "/about" %}<a href="{{ url }}">About</a>{% endif %}';
365
+ const aStart = source.indexOf('<a href');
366
+ const map = buildVariableMap(parseChildren(source), aStart);
367
+ expect(map.get('url')).toBe('/about');
368
+ });
369
+
370
+ it('includes assign inside {% liquid %} block when block ends after beforeOffset', () => {
371
+ const source =
372
+ '{% if true %}{% liquid\n assign url = "/about"\n%}<a href="{{ url }}">About</a>{% endif %}';
373
+ const aStart = source.indexOf('<a href');
374
+ const map = buildVariableMap(parseChildren(source), aStart);
375
+ expect(map.get('url')).toBe('/about');
376
+ });
377
+
378
+ it('excludes assign inside block that starts after beforeOffset', () => {
379
+ const source =
380
+ '<a href="{{ url }}">About</a>{% if true %}{% assign url = "/about" %}{% endif %}';
381
+ const aStart = source.indexOf('<a href');
382
+ const map = buildVariableMap(parseChildren(source), aStart);
383
+ expect(map.has('url')).toBe(false);
384
+ });
385
+ });
386
+ });