@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.
- package/CHANGELOG.md +59 -0
- package/dist/checks/circular-render/index.d.ts +2 -0
- package/dist/checks/circular-render/index.js +164 -0
- package/dist/checks/circular-render/index.js.map +1 -0
- package/dist/checks/index.d.ts +1 -1
- package/dist/checks/index.js +6 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/metadata-params/extract-undefined-variables.d.ts +8 -0
- package/dist/checks/metadata-params/extract-undefined-variables.js +213 -0
- package/dist/checks/metadata-params/extract-undefined-variables.js.map +1 -0
- package/dist/checks/metadata-params/index.js +48 -33
- package/dist/checks/metadata-params/index.js.map +1 -1
- package/dist/checks/missing-page/index.d.ts +2 -0
- package/dist/checks/missing-page/index.js +73 -0
- package/dist/checks/missing-page/index.js.map +1 -0
- package/dist/checks/missing-partial/index.js +31 -31
- package/dist/checks/missing-partial/index.js.map +1 -1
- package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
- package/dist/checks/missing-render-partial-arguments/index.js +37 -0
- package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
- package/dist/checks/nested-graphql-query/index.d.ts +2 -0
- package/dist/checks/nested-graphql-query/index.js +146 -0
- package/dist/checks/nested-graphql-query/index.js.map +1 -0
- package/dist/checks/translation-key-exists/index.js +16 -19
- package/dist/checks/translation-key-exists/index.js.map +1 -1
- package/dist/checks/translation-utils.d.ts +16 -0
- package/dist/checks/translation-utils.js +51 -0
- package/dist/checks/translation-utils.js.map +1 -0
- package/dist/checks/undefined-object/index.js +32 -0
- package/dist/checks/undefined-object/index.js.map +1 -1
- package/dist/checks/unknown-property/index.js +64 -2
- package/dist/checks/unknown-property/index.js.map +1 -1
- package/dist/checks/unused-translation-key/index.d.ts +4 -0
- package/dist/checks/unused-translation-key/index.js +85 -0
- package/dist/checks/unused-translation-key/index.js.map +1 -0
- package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
- package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
- package/dist/context-utils.d.ts +2 -1
- package/dist/context-utils.js +31 -1
- package/dist/context-utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/liquid-doc/arguments.js +4 -0
- package/dist/liquid-doc/arguments.js.map +1 -1
- package/dist/liquid-doc/utils.d.ts +10 -2
- package/dist/liquid-doc/utils.js +26 -1
- package/dist/liquid-doc/utils.js.map +1 -1
- package/dist/to-source-code.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js.map +1 -1
- package/dist/url-helpers.d.ts +55 -0
- package/dist/url-helpers.js +334 -0
- package/dist/url-helpers.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/levenshtein.d.ts +3 -0
- package/dist/utils/levenshtein.js +39 -0
- package/dist/utils/levenshtein.js.map +1 -0
- package/package.json +2 -2
- package/src/checks/graphql/index.spec.ts +2 -2
- package/src/checks/index.ts +6 -0
- package/src/checks/metadata-params/extract-undefined-variables.spec.ts +115 -0
- package/src/checks/metadata-params/extract-undefined-variables.ts +286 -0
- package/src/checks/metadata-params/index.spec.ts +180 -26
- package/src/checks/metadata-params/index.ts +51 -34
- package/src/checks/missing-page/index.spec.ts +755 -0
- package/src/checks/missing-page/index.ts +89 -0
- package/src/checks/missing-partial/index.spec.ts +361 -0
- package/src/checks/missing-partial/index.ts +39 -47
- package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
- package/src/checks/missing-render-partial-arguments/index.ts +44 -0
- package/src/checks/nested-graphql-query/index.spec.ts +175 -0
- package/src/checks/nested-graphql-query/index.ts +203 -0
- package/src/checks/parser-blocking-script/index.spec.ts +7 -3
- package/src/checks/translation-key-exists/index.spec.ts +79 -2
- package/src/checks/translation-key-exists/index.ts +18 -27
- package/src/checks/translation-utils.ts +63 -0
- package/src/checks/undefined-object/index.spec.ts +194 -35
- package/src/checks/undefined-object/index.ts +40 -1
- package/src/checks/unknown-property/index.spec.ts +62 -0
- package/src/checks/unknown-property/index.ts +73 -2
- package/src/checks/unused-assign/index.spec.ts +1 -1
- package/src/checks/unused-doc-param/index.spec.ts +4 -2
- package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
- package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
- package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
- package/src/checks/variable-name/index.spec.ts +1 -1
- package/src/context-utils.ts +33 -1
- package/src/disabled-checks/index.spec.ts +4 -4
- package/src/index.ts +3 -0
- package/src/liquid-doc/arguments.ts +6 -0
- package/src/liquid-doc/utils.ts +26 -2
- package/src/types.ts +9 -1
- package/src/url-helpers.spec.ts +386 -0
- package/src/url-helpers.ts +363 -0
- package/src/utils/index.ts +1 -0
- 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
|
+
});
|