td-ai-tools 1.2.8 → 1.2.9
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
CHANGED
|
@@ -33,9 +33,11 @@ Rendered storefront copy should come from a section setting, translation key,
|
|
|
33
33
|
metafield, or metaobject. The check reports text nodes, direct quoted output
|
|
34
34
|
with `{{ }}` or `{% echo %}`, quoted `default` filter fallbacks, hard-coded
|
|
35
35
|
`alt`, `title`, `placeholder`, and ARIA text attributes, and labels in the
|
|
36
|
-
`value` attribute of button-like inputs.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
`value` attribute of button-like inputs. Text inside inline SVG elements and
|
|
37
|
+
string literals passed as named `render` or `include` arguments are also
|
|
38
|
+
checked. It ignores technical attributes such as classes and IDs, Liquid logic
|
|
39
|
+
strings, comments, punctuation-only fragments, and text made entirely of HTML
|
|
40
|
+
character references.
|
|
39
41
|
|
|
40
42
|
Content inside `{% stylesheet %}`, `{% javascript %}`, and `{% schema %}` is
|
|
41
43
|
ignored. A quoted translation key passed through `t` or `translate` is also
|
|
@@ -74,6 +74,48 @@ describe('HardcodedText', () => {
|
|
|
74
74
|
expect(offenses).toHaveLength(2);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it('reports hard-coded text inside inline SVG elements', async () => {
|
|
78
|
+
const source = `
|
|
79
|
+
<svg aria-hidden="true">
|
|
80
|
+
<title>Search icon</title>
|
|
81
|
+
<desc>Opens the search dialog</desc>
|
|
82
|
+
<text x="10" y="20">Search</text>
|
|
83
|
+
<path d="M10 10 H 90 V 90 H 10 Z" />
|
|
84
|
+
</svg>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const offenses = await runLiquidCheck(HardcodedText, source);
|
|
88
|
+
|
|
89
|
+
expect(offenses).toHaveLength(3);
|
|
90
|
+
expect(
|
|
91
|
+
offenses.map(({ start, end }) =>
|
|
92
|
+
source.slice(start.index, end.index),
|
|
93
|
+
),
|
|
94
|
+
).toEqual([
|
|
95
|
+
'Search icon',
|
|
96
|
+
'Opens the search dialog',
|
|
97
|
+
'Search',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('reports string literals passed as render and include arguments', async () => {
|
|
102
|
+
const source = `
|
|
103
|
+
{% render 'button', label: 'Add to cart' %}
|
|
104
|
+
{% include 'icon', accessible_name: "Search icon" %}
|
|
105
|
+
{% render 'button', label: section.settings.button_label %}
|
|
106
|
+
{% render 'icon' %}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const offenses = await runLiquidCheck(HardcodedText, source);
|
|
110
|
+
|
|
111
|
+
expect(offenses).toHaveLength(2);
|
|
112
|
+
expect(
|
|
113
|
+
offenses.map(({ start, end }) =>
|
|
114
|
+
source.slice(start.index, end.index),
|
|
115
|
+
),
|
|
116
|
+
).toEqual(["'Add to cart'", '"Search icon"']);
|
|
117
|
+
});
|
|
118
|
+
|
|
77
119
|
it('reports hard-coded default filter fallbacks', async () => {
|
|
78
120
|
const source = `
|
|
79
121
|
{{ product.title | default: 'Free shipping on all orders' }}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NodeTypes,
|
|
3
|
+
toLiquidHtmlAST,
|
|
3
4
|
} from '@shopify/liquid-html-parser';
|
|
4
5
|
import type {
|
|
5
6
|
AttributeNode,
|
|
@@ -8,12 +9,14 @@ import type {
|
|
|
8
9
|
LiquidRawTag,
|
|
9
10
|
LiquidString,
|
|
10
11
|
LiquidVariable,
|
|
12
|
+
RawMarkup,
|
|
11
13
|
TextNode,
|
|
12
14
|
} from '@shopify/liquid-html-parser';
|
|
13
15
|
import {
|
|
14
16
|
LiquidCheckDefinition,
|
|
15
17
|
Severity,
|
|
16
18
|
SourceCodeType,
|
|
19
|
+
visit,
|
|
17
20
|
} from '@shopify/theme-check-common';
|
|
18
21
|
|
|
19
22
|
const EXEMPT_RAW_TAGS = new Set([
|
|
@@ -111,6 +114,14 @@ function rawTagAncestor(
|
|
|
111
114
|
return undefined;
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
function isInlineSvgMarkup(ancestors: LiquidHtmlNode[]): boolean {
|
|
118
|
+
const parent = ancestors.at(-1);
|
|
119
|
+
return (
|
|
120
|
+
parent?.type === NodeTypes.HtmlRawNode &&
|
|
121
|
+
parent.name.toLowerCase() === 'svg'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
function hasMeaningfulText(value: string): boolean {
|
|
115
126
|
return /[\p{L}\p{N}]/u.test(
|
|
116
127
|
value.replace(HTML_CHARACTER_REFERENCE, ''),
|
|
@@ -189,44 +200,80 @@ export const HardcodedText: LiquidCheckDefinition = {
|
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
202
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
function reportTextNode(
|
|
204
|
+
node: TextNode,
|
|
205
|
+
ancestors: LiquidHtmlNode[],
|
|
206
|
+
positionOffset = 0,
|
|
207
|
+
) {
|
|
208
|
+
if (!hasMeaningfulText(node.value)) return;
|
|
209
|
+
|
|
210
|
+
const rawTag = rawTagAncestor(ancestors);
|
|
211
|
+
if (rawTag) {
|
|
212
|
+
if (EXEMPT_RAW_TAGS.has(rawTag.name.toLowerCase())) return;
|
|
213
|
+
report(
|
|
214
|
+
positionOffset + node.position.start,
|
|
215
|
+
positionOffset + node.position.end,
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
195
219
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
const parent = ancestors.at(-1);
|
|
221
|
+
if (!parent || isHtmlElementName(node, parent)) return;
|
|
222
|
+
|
|
223
|
+
if (isValuedAttribute(parent)) {
|
|
224
|
+
if (!parent.value.some((part) => part === node)) return;
|
|
225
|
+
|
|
226
|
+
const attributeName = staticAttributeName(parent);
|
|
227
|
+
if (
|
|
228
|
+
(attributeName && TEXT_ATTRIBUTES.has(attributeName)) ||
|
|
229
|
+
(attributeName === 'value' &&
|
|
230
|
+
isInputButtonValue(ancestors))
|
|
231
|
+
) {
|
|
232
|
+
report(
|
|
233
|
+
positionOffset + node.position.start,
|
|
234
|
+
positionOffset + node.position.end,
|
|
235
|
+
);
|
|
201
236
|
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
202
239
|
|
|
203
|
-
|
|
204
|
-
if (!parent || isHtmlElementName(node, parent)) return;
|
|
240
|
+
if (parent.type === NodeTypes.RawMarkup) return;
|
|
205
241
|
|
|
206
|
-
|
|
207
|
-
|
|
242
|
+
if (
|
|
243
|
+
parent.type === NodeTypes.Document ||
|
|
244
|
+
parent.type === NodeTypes.HtmlElement ||
|
|
245
|
+
parent.type === NodeTypes.LiquidBranch ||
|
|
246
|
+
parent.type === NodeTypes.LiquidTag
|
|
247
|
+
) {
|
|
248
|
+
report(
|
|
249
|
+
positionOffset + node.position.start,
|
|
250
|
+
positionOffset + node.position.end,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
208
254
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
255
|
+
function reportInlineSvgText(node: RawMarkup) {
|
|
256
|
+
let fragment;
|
|
257
|
+
try {
|
|
258
|
+
fragment = toLiquidHtmlAST(node.value);
|
|
259
|
+
} catch {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
219
262
|
|
|
220
|
-
|
|
263
|
+
visit<SourceCodeType.LiquidHtml, void>(fragment, {
|
|
264
|
+
TextNode(textNode, ancestors) {
|
|
265
|
+
reportTextNode(
|
|
266
|
+
textNode,
|
|
267
|
+
ancestors,
|
|
268
|
+
node.position.start,
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
221
273
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
parent.type === NodeTypes.LiquidBranch ||
|
|
226
|
-
parent.type === NodeTypes.LiquidTag
|
|
227
|
-
) {
|
|
228
|
-
report(node.position.start, node.position.end);
|
|
229
|
-
}
|
|
274
|
+
return {
|
|
275
|
+
async TextNode(node, ancestors) {
|
|
276
|
+
reportTextNode(node, ancestors);
|
|
230
277
|
},
|
|
231
278
|
|
|
232
279
|
async LiquidVariableOutput(node) {
|
|
@@ -238,6 +285,31 @@ export const HardcodedText: LiquidCheckDefinition = {
|
|
|
238
285
|
if (node.name !== 'echo' || typeof node.markup === 'string') return;
|
|
239
286
|
reportLiteralOutput(node.markup);
|
|
240
287
|
},
|
|
288
|
+
|
|
289
|
+
async RawMarkup(node, ancestors) {
|
|
290
|
+
if (!isInlineSvgMarkup(ancestors)) return;
|
|
291
|
+
|
|
292
|
+
const rawTag = rawTagAncestor(ancestors);
|
|
293
|
+
if (
|
|
294
|
+
rawTag &&
|
|
295
|
+
EXEMPT_RAW_TAGS.has(rawTag.name.toLowerCase())
|
|
296
|
+
) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
reportInlineSvgText(node);
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async RenderMarkup(node) {
|
|
304
|
+
for (const argument of node.args) {
|
|
305
|
+
if (
|
|
306
|
+
argument.type === NodeTypes.NamedArgument &&
|
|
307
|
+
argument.value.type === NodeTypes.String
|
|
308
|
+
) {
|
|
309
|
+
reportString(argument.value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
241
313
|
};
|
|
242
314
|
},
|
|
243
315
|
};
|