zemdomu 1.3.14 → 1.3.15

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.
@@ -1,40 +1,6 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.default = enforceListNesting;
37
- const t = __importStar(require("@babel/types"));
38
4
  const utils_1 = require("./utils");
39
5
  function enforceListNesting() {
40
6
  const stack = [];
@@ -59,19 +25,15 @@ function enforceListNesting() {
59
25
  return [];
60
26
  },
61
27
  enterJsx(path) {
62
- var _a, _b, _c, _d, _e, _f;
28
+ var _a, _b, _c, _d;
63
29
  const tag = (0, utils_1.getTag)(path);
64
30
  if (tag === 'li') {
65
- const parentNode = (_b = (_a = path.parentPath) === null || _a === void 0 ? void 0 : _a.parentPath) === null || _b === void 0 ? void 0 : _b.node;
66
- let inList = false;
67
- if (parentNode && t.isJSXElement(parentNode)) {
68
- const open = parentNode.openingElement;
69
- const pTag = t.isJSXIdentifier(open.name) ? open.name.name.toLowerCase() : '';
70
- inList = ['ul', 'ol'].includes(pTag);
71
- }
31
+ const parentElement = path.findParent((p) => p.isJSXElement());
32
+ const parentTag = parentElement ? (0, utils_1.getTag)(parentElement) : '';
33
+ const inList = parentTag === 'ul' || parentTag === 'ol';
72
34
  if (!inList) {
73
- const line = ((_d = (_c = path.node.loc) === null || _c === void 0 ? void 0 : _c.start.line) !== null && _d !== void 0 ? _d : 1) - 1;
74
- const column = (_f = (_e = path.node.loc) === null || _e === void 0 ? void 0 : _e.start.column) !== null && _f !== void 0 ? _f : 0;
35
+ const line = ((_b = (_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : 1) - 1;
36
+ const column = (_d = (_c = path.node.loc) === null || _c === void 0 ? void 0 : _c.start.column) !== null && _d !== void 0 ? _d : 0;
75
37
  return [{ line, column, message: '<li> must be inside a <ul> or <ol>', rule: 'enforceListNesting' }];
76
38
  }
77
39
  }
@@ -35,21 +35,24 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.default = requireAltText;
37
37
  const t = __importStar(require("@babel/types"));
38
+ const utils_1 = require("./utils");
38
39
  function requireAltText() {
39
40
  return {
40
41
  name: 'requireAltText',
41
42
  enterHtml(node) {
42
- if (node.type === 'element' &&
43
- node.tagName === 'img' &&
44
- (!('alt' in node.attrs) || !node.attrs.alt.trim())) {
45
- return [
46
- {
47
- line: 0, // line/column handling omitted for brevity
48
- column: 0,
49
- message: '<img> tag missing alt attribute',
50
- rule: 'requireAltText',
51
- },
52
- ];
43
+ var _a, _b;
44
+ if (node.type === 'element' && node.tagName === 'img') {
45
+ const alt = (_b = (_a = node.attrs.alt) !== null && _a !== void 0 ? _a : node.attrs[':alt']) !== null && _b !== void 0 ? _b : node.attrs['v-bind:alt'];
46
+ if (alt === undefined || !String(alt).trim()) {
47
+ return [
48
+ {
49
+ line: 0, // line/column handling omitted for brevity
50
+ column: 0,
51
+ message: '<img> tag missing or empty alt attribute',
52
+ rule: 'requireAltText',
53
+ },
54
+ ];
55
+ }
53
56
  }
54
57
  return [];
55
58
  },
@@ -58,16 +61,17 @@ function requireAltText() {
58
61
  const name = t.isJSXIdentifier(opening.name) ? opening.name.name : '';
59
62
  if (name !== 'img')
60
63
  return [];
61
- const altAttr = opening.attributes.find((a) => t.isJSXAttribute(a) &&
62
- t.isJSXIdentifier(a.name) &&
63
- a.name.name === 'alt');
64
- if (!altAttr || !t.isStringLiteral(altAttr.value) || altAttr.value.value.trim() === '') {
64
+ const altState = (0, utils_1.getJsxAttributeState)(opening, 'alt', true);
65
+ if (altState === 'missing' || altState === 'empty' || altState === 'possiblyEmpty') {
65
66
  const loc = opening.loc.start;
67
+ const message = altState === 'possiblyEmpty'
68
+ ? '<img> alt is possibly empty or undefined'
69
+ : '<img> tag missing or empty alt attribute';
66
70
  return [
67
71
  {
68
72
  line: loc.line - 1,
69
73
  column: loc.column,
70
- message: '<img> tag missing alt attribute',
74
+ message,
71
75
  rule: 'requireAltText',
72
76
  },
73
77
  ];
@@ -40,10 +40,18 @@ function requireHrefOnAnchors() {
40
40
  return {
41
41
  name: 'requireHrefOnAnchors',
42
42
  enterHtml(node) {
43
+ var _a, _b;
43
44
  if (node.type === 'element' && node.tagName === 'a') {
44
- const href = node.attrs.href;
45
+ const href = (_b = (_a = node.attrs.href) !== null && _a !== void 0 ? _a : node.attrs[':href']) !== null && _b !== void 0 ? _b : node.attrs['v-bind:href'];
45
46
  if (!href || !href.trim()) {
46
- return [{ line: 0, column: 0, message: '<a> tag missing non-empty href attribute', rule: 'requireHrefOnAnchors' }];
47
+ return [
48
+ {
49
+ line: 0,
50
+ column: 0,
51
+ message: '<a> tag missing non-empty href attribute',
52
+ rule: 'requireHrefOnAnchors',
53
+ },
54
+ ];
47
55
  }
48
56
  }
49
57
  return [];
@@ -53,11 +61,14 @@ function requireHrefOnAnchors() {
53
61
  const opening = path.node.openingElement;
54
62
  const tag = t.isJSXIdentifier(opening.name) ? opening.name.name.toLowerCase() : '';
55
63
  if (tag === 'a') {
56
- const href = (0, utils_1.getJsxAttr)(opening, 'href');
57
- if (!href || !href.trim()) {
64
+ const hrefState = (0, utils_1.getJsxAttributeState)(opening, 'href', true);
65
+ if (hrefState === 'missing' || hrefState === 'empty' || hrefState === 'possiblyEmpty') {
58
66
  const line = ((_b = (_a = opening.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : 1) - 1;
59
67
  const column = (_d = (_c = opening.loc) === null || _c === void 0 ? void 0 : _c.start.column) !== null && _d !== void 0 ? _d : 0;
60
- return [{ line, column, message: '<a> tag missing non-empty href attribute', rule: 'requireHrefOnAnchors' }];
68
+ const message = hrefState === 'possiblyEmpty'
69
+ ? '<a> href is possibly empty or undefined'
70
+ : '<a> tag missing non-empty href attribute';
71
+ return [{ line, column, message, rule: 'requireHrefOnAnchors' }];
61
72
  }
62
73
  }
63
74
  return [];
@@ -36,6 +36,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.default = requireLinkText;
37
37
  const t = __importStar(require("@babel/types"));
38
38
  const utils_1 = require("./utils");
39
+ function mergeTextStates(states) {
40
+ if (states.some((s) => s === "present"))
41
+ return "present";
42
+ if (states.some((s) => s === "possiblyEmpty"))
43
+ return "possiblyEmpty";
44
+ return "empty";
45
+ }
46
+ function jsxChildTextState(child) {
47
+ if (t.isJSXText(child))
48
+ return child.value.trim().length > 0 ? "present" : "empty";
49
+ if (t.isJSXExpressionContainer(child)) {
50
+ const expr = child.expression;
51
+ if (t.isJSXElement(expr))
52
+ return jsxElementTextState(expr);
53
+ if (t.isJSXFragment(expr)) {
54
+ return mergeTextStates(expr.children.map(jsxChildTextState));
55
+ }
56
+ return (0, utils_1.getJsxExpressionState)(expr, true);
57
+ }
58
+ if (t.isJSXElement(child))
59
+ return jsxElementTextState(child);
60
+ if (t.isJSXFragment(child)) {
61
+ return mergeTextStates(child.children.map(jsxChildTextState));
62
+ }
63
+ if (t.isJSXSpreadChild(child))
64
+ return "present";
65
+ return "empty";
66
+ }
67
+ function jsxElementTextState(node) {
68
+ return mergeTextStates(node.children.map(jsxChildTextState));
69
+ }
39
70
  function requireLinkText() {
40
71
  const stack = [];
41
72
  return {
@@ -65,31 +96,25 @@ function requireLinkText() {
65
96
  }
66
97
  return [];
67
98
  },
68
- enterJsx(path) {
69
- const tag = (0, utils_1.getTag)(path);
70
- if (tag === "a")
71
- stack.push({ found: false });
99
+ enterJsx(_) {
72
100
  return [];
73
101
  },
74
102
  exitJsx(path) {
75
- var _a, _b, _c, _d, _e;
103
+ var _a, _b, _c, _d;
76
104
  const tag = (0, utils_1.getTag)(path);
77
105
  if (tag === "a") {
78
- const entry = stack.pop();
79
- let hasText = false;
80
- const parentNode = (_a = path.parentPath) === null || _a === void 0 ? void 0 : _a.node;
81
- if (t.isJSXElement(parentNode) && Array.isArray(parentNode.children)) {
82
- hasText = parentNode.children.some((c) => (t.isJSXText(c) && c.value.trim()) ||
83
- t.isJSXExpressionContainer(c));
84
- }
85
- if (entry && !(entry.found || hasText)) {
86
- const line = ((_c = (_b = path.node.loc) === null || _b === void 0 ? void 0 : _b.start.line) !== null && _c !== void 0 ? _c : 1) - 1;
87
- const column = (_e = (_d = path.node.loc) === null || _d === void 0 ? void 0 : _d.start.column) !== null && _e !== void 0 ? _e : 0;
106
+ const textState = jsxElementTextState(path.node);
107
+ if (textState !== "present") {
108
+ const line = ((_b = (_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : 1) - 1;
109
+ const column = (_d = (_c = path.node.loc) === null || _c === void 0 ? void 0 : _c.start.column) !== null && _d !== void 0 ? _d : 0;
110
+ const message = textState === "possiblyEmpty"
111
+ ? "<a> link text is possibly empty or undefined"
112
+ : "<a> tag missing link text";
88
113
  return [
89
114
  {
90
115
  line,
91
116
  column,
92
- message: "<a> tag missing link text",
117
+ message,
93
118
  rule: "requireLinkText",
94
119
  },
95
120
  ];
@@ -3,4 +3,10 @@ import { NodePath } from '@babel/traverse';
3
3
  import { ElementNode } from '../simpleHtmlParser';
4
4
  export declare function getAttr(node: ElementNode, name: string): string | undefined;
5
5
  export declare function getJsxAttr(opening: t.JSXOpeningElement, name: string): string | undefined;
6
+ export declare function getJsxAttribute(opening: t.JSXOpeningElement, name: string): t.JSXAttribute | undefined;
7
+ export type JsxValueState = 'missing' | 'empty' | 'possiblyEmpty' | 'present';
8
+ export declare function getJsxExpressionState(expression: t.Expression | t.TSType | t.JSXEmptyExpression, trimText: boolean): JsxValueState;
9
+ export declare function isJsxExpressionPossiblyEmpty(expression: t.Expression | t.JSXEmptyExpression, trimText: boolean): boolean;
10
+ export declare function isJsxAttrValueEmpty(value: t.JSXAttribute['value'], trimText: boolean): boolean;
11
+ export declare function getJsxAttributeState(opening: t.JSXOpeningElement, name: string, trimText: boolean): JsxValueState;
6
12
  export declare function getTag(path: NodePath<t.JSXElement>): string;
@@ -35,6 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getAttr = getAttr;
37
37
  exports.getJsxAttr = getJsxAttr;
38
+ exports.getJsxAttribute = getJsxAttribute;
39
+ exports.getJsxExpressionState = getJsxExpressionState;
40
+ exports.isJsxExpressionPossiblyEmpty = isJsxExpressionPossiblyEmpty;
41
+ exports.isJsxAttrValueEmpty = isJsxAttrValueEmpty;
42
+ exports.getJsxAttributeState = getJsxAttributeState;
38
43
  exports.getTag = getTag;
39
44
  const t = __importStar(require("@babel/types"));
40
45
  function getAttr(node, name) {
@@ -44,6 +49,115 @@ function getJsxAttr(opening, name) {
44
49
  const attr = opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === name);
45
50
  return attr && t.isStringLiteral(attr.value) ? attr.value.value : undefined;
46
51
  }
52
+ function getJsxAttribute(opening, name) {
53
+ return opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === name);
54
+ }
55
+ function isEmptyString(value, trimText) {
56
+ return trimText ? value.trim().length === 0 : value.length === 0;
57
+ }
58
+ function mergeConditionalStates(a, b) {
59
+ if (a === 'present' && b === 'present')
60
+ return 'present';
61
+ if (a === 'empty' && b === 'empty')
62
+ return 'empty';
63
+ return 'possiblyEmpty';
64
+ }
65
+ function mergeTemplateStates(states) {
66
+ if (states.some((s) => s === 'present'))
67
+ return 'present';
68
+ if (states.some((s) => s === 'possiblyEmpty'))
69
+ return 'possiblyEmpty';
70
+ return 'empty';
71
+ }
72
+ function getJsxExpressionState(expression, trimText) {
73
+ if (t.isTSType(expression))
74
+ return 'present';
75
+ if (t.isJSXEmptyExpression(expression))
76
+ return 'empty';
77
+ if (t.isNullLiteral(expression))
78
+ return 'empty';
79
+ if (t.isIdentifier(expression, { name: 'undefined' }))
80
+ return 'empty';
81
+ if (t.isBooleanLiteral(expression))
82
+ return 'empty';
83
+ if (t.isOptionalMemberExpression(expression) || t.isOptionalCallExpression(expression)) {
84
+ return 'possiblyEmpty';
85
+ }
86
+ if (t.isStringLiteral(expression)) {
87
+ return isEmptyString(expression.value, trimText) ? 'empty' : 'present';
88
+ }
89
+ if (t.isTemplateLiteral(expression)) {
90
+ if (expression.expressions.length === 0) {
91
+ const raw = expression.quasis
92
+ .map((q) => { var _a; return (_a = q.value.cooked) !== null && _a !== void 0 ? _a : q.value.raw; })
93
+ .join('');
94
+ return isEmptyString(raw, trimText) ? 'empty' : 'present';
95
+ }
96
+ const staticText = expression.quasis
97
+ .map((q) => { var _a; return (_a = q.value.cooked) !== null && _a !== void 0 ? _a : q.value.raw; })
98
+ .join('');
99
+ if (staticText.trim().length > 0)
100
+ return 'present';
101
+ const exprStates = expression.expressions.map((expr) => getJsxExpressionState(expr, trimText));
102
+ return mergeTemplateStates(exprStates);
103
+ }
104
+ if (t.isConditionalExpression(expression)) {
105
+ return mergeConditionalStates(getJsxExpressionState(expression.consequent, trimText), getJsxExpressionState(expression.alternate, trimText));
106
+ }
107
+ if (t.isLogicalExpression(expression)) {
108
+ const left = getJsxExpressionState(expression.left, trimText);
109
+ if (expression.operator === '&&') {
110
+ if (left === 'empty')
111
+ return 'empty';
112
+ if (left === 'present') {
113
+ return getJsxExpressionState(expression.right, trimText);
114
+ }
115
+ return 'possiblyEmpty';
116
+ }
117
+ if (expression.operator === '||' || expression.operator === '??') {
118
+ if (left === 'present')
119
+ return 'present';
120
+ if (left === 'empty') {
121
+ return getJsxExpressionState(expression.right, trimText);
122
+ }
123
+ const right = getJsxExpressionState(expression.right, trimText);
124
+ if (right === 'present')
125
+ return 'present';
126
+ return 'possiblyEmpty';
127
+ }
128
+ }
129
+ if (t.isUnaryExpression(expression) && expression.operator === 'void')
130
+ return 'empty';
131
+ return 'present';
132
+ }
133
+ function isJsxExpressionPossiblyEmpty(expression, trimText) {
134
+ return getJsxExpressionState(expression, trimText) !== 'present';
135
+ }
136
+ function isJsxAttrValueEmpty(value, trimText) {
137
+ if (!value)
138
+ return true;
139
+ if (t.isStringLiteral(value)) {
140
+ return isEmptyString(value.value, trimText);
141
+ }
142
+ if (t.isJSXExpressionContainer(value)) {
143
+ return isJsxExpressionPossiblyEmpty(value.expression, trimText);
144
+ }
145
+ return false;
146
+ }
147
+ function getJsxAttributeState(opening, name, trimText) {
148
+ const attr = getJsxAttribute(opening, name);
149
+ if (!attr)
150
+ return 'missing';
151
+ if (!attr.value)
152
+ return 'empty';
153
+ if (t.isStringLiteral(attr.value)) {
154
+ return isEmptyString(attr.value.value, trimText) ? 'empty' : 'present';
155
+ }
156
+ if (t.isJSXExpressionContainer(attr.value)) {
157
+ return getJsxExpressionState(attr.value.expression, trimText);
158
+ }
159
+ return 'present';
160
+ }
47
161
  function getTag(path) {
48
162
  const opening = path.node.openingElement;
49
163
  return t.isJSXIdentifier(opening.name) ? opening.name.name.toLowerCase() : '';
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const assert_1 = require("assert");
4
+ const linter_1 = require("../src/linter");
5
+ describe("img alt text with dynamic values", () => {
6
+ it("allows JSX expressions for alt when not explicitly empty", () => {
7
+ const jsx = `
8
+ const avatarUrl = "/avatar.png";
9
+ const displayLabel = "Avatar";
10
+ export default function Avatar() {
11
+ return <img src={avatarUrl} alt={displayLabel} />;
12
+ }
13
+ `;
14
+ const results = (0, linter_1.lint)(jsx);
15
+ assert_1.strict.ok(!results.some((r) => r.rule === "requireAltText"), "Did not expect alt warning for dynamic alt value");
16
+ });
17
+ it("flags explicitly empty JSX alt values", () => {
18
+ const jsx = `export default () => <img src="/avatar.png" alt={""} />;`;
19
+ const results = (0, linter_1.lint)(jsx);
20
+ assert_1.strict.ok(results.some((r) => r.rule === "requireAltText"), "Expected alt warning for empty JSX alt");
21
+ });
22
+ it("uses possible-empty messaging for conditional alt", () => {
23
+ const jsx = `
24
+ export default function Avatar({ user }) {
25
+ return <img src="/avatar.png" alt={user ? user.label : ""} />;
26
+ }
27
+ `;
28
+ const results = (0, linter_1.lint)(jsx);
29
+ const warning = results.find((r) => r.rule === "requireAltText");
30
+ assert_1.strict.ok(warning, "Expected alt warning for possibly empty alt");
31
+ assert_1.strict.ok(warning === null || warning === void 0 ? void 0 : warning.message.includes("possibly empty or undefined"), "Expected possible-empty alt message");
32
+ });
33
+ it("treats Vue-style bound alt as present in HTML mode unless empty", () => {
34
+ const html = `<img src="/avatar.png" :alt="displayLabel"><img src="/avatar.png" :alt="">`;
35
+ const results = (0, linter_1.lint)(html, { forceHtml: true });
36
+ const warnings = results.filter((r) => r.rule === "requireAltText");
37
+ assert_1.strict.strictEqual(warnings.length, 1, "Expected only the empty bound alt to warn");
38
+ });
39
+ });
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const assert_1 = require("assert");
4
+ const linter_1 = require("../src/linter");
5
+ describe("link href and text with dynamic values", () => {
6
+ it("allows JSX expressions for href and text when not explicitly empty", () => {
7
+ const jsx = `
8
+ const link = { url: "/docs" };
9
+ const text = "Docs";
10
+ export default function Nav() {
11
+ return <a href={link.url}>{text}</a>;
12
+ }
13
+ `;
14
+ const results = (0, linter_1.lint)(jsx);
15
+ assert_1.strict.ok(!results.some((r) => r.rule === "requireHrefOnAnchors"), "Did not expect href warning for dynamic href value");
16
+ assert_1.strict.ok(!results.some((r) => r.rule === "requireLinkText"), "Did not expect link text warning for dynamic text value");
17
+ });
18
+ it("flags explicitly empty JSX href values", () => {
19
+ const jsx = `export default () => <a href={""}>Text</a>;`;
20
+ const results = (0, linter_1.lint)(jsx);
21
+ assert_1.strict.ok(results.some((r) => r.rule === "requireHrefOnAnchors"), "Expected href warning for empty JSX href");
22
+ });
23
+ it("flags explicitly empty JSX text values", () => {
24
+ const jsx = `export default () => <a href="/docs">{undefined}</a>;`;
25
+ const results = (0, linter_1.lint)(jsx);
26
+ assert_1.strict.ok(results.some((r) => r.rule === "requireLinkText"), "Expected link text warning for undefined JSX text");
27
+ });
28
+ it("uses possible-empty messaging for conditional href/text", () => {
29
+ const jsx = `
30
+ export default function Nav({ ready, link }) {
31
+ return (
32
+ <a href={ready ? link.url : ""}>
33
+ {ready ? "Docs" : ""}
34
+ </a>
35
+ );
36
+ }
37
+ `;
38
+ const results = (0, linter_1.lint)(jsx);
39
+ const hrefWarning = results.find((r) => r.rule === "requireHrefOnAnchors");
40
+ const textWarning = results.find((r) => r.rule === "requireLinkText");
41
+ assert_1.strict.ok(hrefWarning, "Expected href warning for possibly empty href");
42
+ assert_1.strict.ok(textWarning, "Expected link text warning for possibly empty text");
43
+ assert_1.strict.ok(hrefWarning === null || hrefWarning === void 0 ? void 0 : hrefWarning.message.includes("possibly empty or undefined"), "Expected possible-empty href message");
44
+ assert_1.strict.ok(textWarning === null || textWarning === void 0 ? void 0 : textWarning.message.includes("possibly empty or undefined"), "Expected possible-empty text message");
45
+ });
46
+ it("treats Vue-style bound href and mustache text as present in HTML mode", () => {
47
+ const html = `<a :href="link.url">{{ text }}</a>`;
48
+ const results = (0, linter_1.lint)(html, { forceHtml: true });
49
+ assert_1.strict.ok(!results.some((r) => r.rule === "requireHrefOnAnchors"), "Did not expect href warning for bound Vue href");
50
+ assert_1.strict.ok(!results.some((r) => r.rule === "requireLinkText"), "Did not expect link text warning for mustache text");
51
+ });
52
+ });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const assert_1 = require("assert");
4
+ const linter_1 = require("../src/linter");
5
+ describe("list nesting with JSX expressions", () => {
6
+ it("allows <li> returned from map inside <ul>", () => {
7
+ const jsx = `
8
+ const holdings = [{ id: 1, asset: { id: "btc" } }];
9
+ export default function Holdings() {
10
+ return (
11
+ <ul className="mt-4 space-y-3">
12
+ {holdings.map((holding) => (
13
+ <li key={holding.asset.id}>{holding.asset.id}</li>
14
+ ))}
15
+ </ul>
16
+ );
17
+ }
18
+ `;
19
+ const results = (0, linter_1.lint)(jsx);
20
+ assert_1.strict.ok(!results.some((r) => r.rule === "enforceListNesting"), "Did not expect list nesting warning for mapped <li> inside <ul>");
21
+ });
22
+ it("still warns when <li> is not inside <ul> or <ol>", () => {
23
+ const jsx = `export default () => <li>Orphan</li>;`;
24
+ const results = (0, linter_1.lint)(jsx);
25
+ assert_1.strict.ok(results.some((r) => r.rule === "enforceListNesting"), "Expected list nesting warning for orphan <li>");
26
+ });
27
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zemdomu",
3
- "version": "1.3.14",
3
+ "version": "1.3.15",
4
4
  "description": "Semantic HTML linter for HTML, JSX, TSX, and Vue templates. Detects accessibility, SEO, and structure issues before deployment.",
5
5
  "main": "./out/index.js",
6
6
  "types": "./out/index.d.ts",