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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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. It ignores technical attributes such
37
- as classes and IDs, Liquid logic strings, comments, punctuation-only fragments,
38
- and text made entirely of HTML character references.
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
- return {
193
- async TextNode(node, ancestors) {
194
- if (!hasMeaningfulText(node.value)) return;
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
- const rawTag = rawTagAncestor(ancestors);
197
- if (rawTag) {
198
- if (EXEMPT_RAW_TAGS.has(rawTag.name.toLowerCase())) return;
199
- report(node.position.start, node.position.end);
200
- return;
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
- const parent = ancestors.at(-1);
204
- if (!parent || isHtmlElementName(node, parent)) return;
240
+ if (parent.type === NodeTypes.RawMarkup) return;
205
241
 
206
- if (isValuedAttribute(parent)) {
207
- if (!parent.value.some((part) => part === node)) return;
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
- const attributeName = staticAttributeName(parent);
210
- if (
211
- (attributeName && TEXT_ATTRIBUTES.has(attributeName)) ||
212
- (attributeName === 'value' &&
213
- isInputButtonValue(ancestors))
214
- ) {
215
- report(node.position.start, node.position.end);
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
- if (parent.type === NodeTypes.RawMarkup) return;
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
- if (
223
- parent.type === NodeTypes.Document ||
224
- parent.type === NodeTypes.HtmlElement ||
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
  };