@markuplint/rules 5.0.0-alpha.2 → 5.0.0-dev.5

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 (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/attr-duplication/index.js +2 -0
  3. package/lib/attr-duplication/meta.d.ts +1 -0
  4. package/lib/attr-duplication/meta.js +1 -0
  5. package/lib/attr-order/index.d.ts +14 -0
  6. package/lib/attr-order/index.js +292 -0
  7. package/lib/attr-order/meta.d.ts +5 -0
  8. package/lib/attr-order/meta.js +4 -0
  9. package/lib/attr-value-quotes/meta.d.ts +1 -0
  10. package/lib/attr-value-quotes/meta.js +1 -0
  11. package/lib/case-sensitive-attr-name/meta.d.ts +1 -0
  12. package/lib/case-sensitive-attr-name/meta.js +1 -0
  13. package/lib/case-sensitive-tag-name/meta.d.ts +1 -0
  14. package/lib/case-sensitive-tag-name/meta.js +1 -0
  15. package/lib/head-element-order/index.d.ts +8 -0
  16. package/lib/head-element-order/index.js +147 -0
  17. package/lib/head-element-order/meta.d.ts +5 -0
  18. package/lib/head-element-order/meta.js +4 -0
  19. package/lib/helpers.d.ts +36 -1
  20. package/lib/helpers.js +68 -0
  21. package/lib/index.d.ts +10 -1
  22. package/lib/index.js +4 -0
  23. package/lib/ineffective-attr/index.js +2 -0
  24. package/lib/ineffective-attr/meta.d.ts +1 -0
  25. package/lib/ineffective-attr/meta.js +1 -0
  26. package/lib/no-boolean-attr-value/index.js +2 -0
  27. package/lib/no-boolean-attr-value/meta.d.ts +1 -0
  28. package/lib/no-boolean-attr-value/meta.js +1 -0
  29. package/lib/no-consecutive-br/index.js +3 -1
  30. package/lib/no-consecutive-br/meta.d.ts +1 -0
  31. package/lib/no-consecutive-br/meta.js +1 -0
  32. package/lib/no-default-value/index.js +2 -1
  33. package/lib/no-default-value/meta.d.ts +1 -0
  34. package/lib/no-default-value/meta.js +1 -0
  35. package/lib/no-duplicate-dt/index.js +2 -1
  36. package/lib/no-orphaned-end-tag/index.js +1 -0
  37. package/lib/no-orphaned-end-tag/meta.d.ts +1 -0
  38. package/lib/no-orphaned-end-tag/meta.js +1 -0
  39. package/lib/no-unsupported-features/compat-data.js +13 -1
  40. package/lib/no-use-event-handler-attr/index.d.ts +4 -1
  41. package/lib/no-use-event-handler-attr/index.js +43 -1
  42. package/lib/wai-aria/checkings/non-existent-role.js +1 -4
  43. package/lib/wai-aria/checkings/required-owned-elements.js +3 -1
  44. package/package.json +8 -8
  45. package/schema.json +6 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-alpha.3](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2026-02-26)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **rules:** remove redundant untranslated message from non-existent-role ([6c002bb](https://github.com/markuplint/markuplint/commit/6c002bb03d44c9a5561c4a40840820e1ada31025))
11
+
12
+ ### Features
13
+
14
+ - **rules:** add attr-order rule for attribute sorting ([993c0a5](https://github.com/markuplint/markuplint/commit/993c0a53823206dfa24ba699bffc27e25b11ab00))
15
+ - **rules:** add fix callbacks to 6 rules and extract shared helpers ([a3bf3c6](https://github.com/markuplint/markuplint/commit/a3bf3c69c028917548c2cc762e1562e8d99dbd9b))
16
+ - **rules:** add fixable flag to rule metadata ([10e3f26](https://github.com/markuplint/markuplint/commit/10e3f266a3431a0cae9eb402f02f00031421ac7b))
17
+ - **rules:** add head-element-order rule for head element sorting ([4f72f35](https://github.com/markuplint/markuplint/commit/4f72f350fe2fceafbb811e3538e771834fb87854))
18
+ - **rules:** support event name array for no-use-event-handler-attr ([5e854b2](https://github.com/markuplint/markuplint/commit/5e854b2198aff43b83fd5b8a8f93f840c50536ea))
19
+
6
20
  # [5.0.0-alpha.2](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2026-02-23)
7
21
 
8
22
  ### Features
@@ -1,4 +1,5 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
+ import { removeAttr } from '../helpers.js';
2
3
  import meta from './meta.js';
3
4
  /**
4
5
  * Rule that checks for duplicate attributes on the same element.
@@ -29,6 +30,7 @@ export default createRule({
29
30
  raw: scope.raw,
30
31
  },
31
32
  message,
33
+ fix: fixer => removeAttr(fixer, attr),
32
34
  });
33
35
  }
34
36
  else {
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `attr-duplication`: categorized as a validation rule. */
2
2
  declare const _default: {
3
3
  readonly category: "validation";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `attr-duplication`: categorized as a validation rule. */
2
2
  export default {
3
3
  category: 'validation',
4
+ fixable: true,
4
5
  };
@@ -0,0 +1,14 @@
1
+ type PredefinedGroup = 'global' | 'event' | 'aria' | 'data' | 'spread';
2
+ type SortOrder = 'alphabetical' | 'source-order' | string[];
3
+ type OrderEntry = string | {
4
+ name?: string;
5
+ pattern?: string;
6
+ group?: PredefinedGroup;
7
+ order?: SortOrder;
8
+ };
9
+ type Value = OrderEntry[];
10
+ type Options = {
11
+ alphabetical?: boolean;
12
+ };
13
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<Value, Options>>;
14
+ export default _default;
@@ -0,0 +1,292 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ function compareStrings(a, b) {
4
+ if (a < b) {
5
+ return -1;
6
+ }
7
+ if (a > b) {
8
+ return 1;
9
+ }
10
+ return 0;
11
+ }
12
+ function buildAttrContent(attr) {
13
+ const parts = [
14
+ attr.nameNode,
15
+ attr.spacesBeforeEqual,
16
+ attr.equal,
17
+ attr.spacesAfterEqual,
18
+ attr.startQuote,
19
+ attr.valueNode,
20
+ attr.endQuote,
21
+ ];
22
+ return parts.map(p => p?.raw ?? '').join('');
23
+ }
24
+ function getAttrRange(attrs) {
25
+ if (attrs.length === 0) {
26
+ return null;
27
+ }
28
+ const first = attrs[0];
29
+ const last = attrs.at(-1);
30
+ const firstToken = first.tokens.spacesBeforeName ?? first.tokens.nameNode;
31
+ const lastAttr = last.tokens;
32
+ const lastTokens = [
33
+ lastAttr.endQuote,
34
+ lastAttr.valueNode,
35
+ lastAttr.startQuote,
36
+ lastAttr.spacesAfterEqual,
37
+ lastAttr.equal,
38
+ lastAttr.spacesBeforeEqual,
39
+ lastAttr.nameNode,
40
+ lastAttr.spacesBeforeName,
41
+ ];
42
+ const lastToken = lastTokens.find(t => t != null && t.raw.length > 0);
43
+ if (!firstToken || !lastToken) {
44
+ return null;
45
+ }
46
+ return [firstToken.startOffset, lastToken.startOffset + lastToken.raw.length];
47
+ }
48
+ function compileEntries(
49
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
50
+ entries,
51
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
52
+ globalAttrs,
53
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
54
+ eventAttrs) {
55
+ return entries.map((entry, entryIndex) => {
56
+ if (typeof entry === 'string') {
57
+ return {
58
+ entryIndex,
59
+ order: 'alphabetical',
60
+ match: (name) => name === entry,
61
+ };
62
+ }
63
+ const order = entry.order ?? 'alphabetical';
64
+ if (entry.name != null) {
65
+ return {
66
+ entryIndex,
67
+ order,
68
+ match: (name) => name === entry.name,
69
+ };
70
+ }
71
+ if (entry.pattern != null) {
72
+ let re;
73
+ try {
74
+ re = new RegExp(entry.pattern);
75
+ }
76
+ catch {
77
+ // Invalid regex pattern — skip this entry
78
+ return {
79
+ entryIndex,
80
+ order,
81
+ match: () => false,
82
+ };
83
+ }
84
+ return {
85
+ entryIndex,
86
+ order,
87
+ match: (name) => re.test(name),
88
+ };
89
+ }
90
+ if (entry.group != null) {
91
+ const group = entry.group;
92
+ const groupOrder = group === 'spread' && entry.order == null ? 'source-order' : order;
93
+ switch (group) {
94
+ case 'global': {
95
+ return {
96
+ entryIndex,
97
+ order: groupOrder,
98
+ match: (name) => globalAttrs.has(name),
99
+ };
100
+ }
101
+ case 'event': {
102
+ return {
103
+ entryIndex,
104
+ order: groupOrder,
105
+ match: (name) => eventAttrs.has(name),
106
+ };
107
+ }
108
+ case 'aria': {
109
+ return {
110
+ entryIndex,
111
+ order: groupOrder,
112
+ match: (name) => /^aria-.+$/.test(name),
113
+ };
114
+ }
115
+ case 'data': {
116
+ return {
117
+ entryIndex,
118
+ order: groupOrder,
119
+ match: (name) => /^data-.+$/.test(name),
120
+ };
121
+ }
122
+ case 'spread': {
123
+ return {
124
+ entryIndex,
125
+ order: groupOrder,
126
+ match: (_name, isSpread) => isSpread,
127
+ };
128
+ }
129
+ }
130
+ }
131
+ return {
132
+ entryIndex,
133
+ order,
134
+ match: () => false,
135
+ };
136
+ });
137
+ }
138
+ function matchEntry(
139
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
140
+ compiledEntries, name, isSpread, fallbackIndex) {
141
+ for (const entry of compiledEntries) {
142
+ if (entry.match(name, isSpread)) {
143
+ return { entryIndex: entry.entryIndex, order: entry.order };
144
+ }
145
+ }
146
+ return { entryIndex: fallbackIndex, order: 'alphabetical' };
147
+ }
148
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
149
+ function compareWithinGroup(a, b, order) {
150
+ if (order === 'alphabetical') {
151
+ return compareStrings(a.name, b.name);
152
+ }
153
+ if (order === 'source-order') {
154
+ return a.index - b.index;
155
+ }
156
+ if (Array.isArray(order)) {
157
+ const ai = order.indexOf(a.name);
158
+ const bi = order.indexOf(b.name);
159
+ const aIdx = ai === -1 ? order.length : ai;
160
+ const bIdx = bi === -1 ? order.length : bi;
161
+ if (aIdx !== bIdx) {
162
+ return aIdx - bIdx;
163
+ }
164
+ if (ai === -1 && bi === -1) {
165
+ return compareStrings(a.name, b.name);
166
+ }
167
+ return 0;
168
+ }
169
+ return 0;
170
+ }
171
+ function sortAttrs(attrInfos,
172
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
173
+ compiledEntries, alphabetical) {
174
+ const fallbackIndex = compiledEntries.length;
175
+ const tagged = attrInfos.map(info => {
176
+ const { entryIndex, order } = matchEntry(compiledEntries, info.name, info.isSpread, fallbackIndex);
177
+ return { info, entryIndex, order };
178
+ });
179
+ return tagged
180
+ .toSorted((a, b) => {
181
+ if (a.entryIndex !== b.entryIndex) {
182
+ return a.entryIndex - b.entryIndex;
183
+ }
184
+ const isFallback = a.entryIndex === fallbackIndex;
185
+ if (isFallback) {
186
+ return alphabetical ? compareStrings(a.info.name, b.info.name) : a.info.index - b.info.index;
187
+ }
188
+ return compareWithinGroup(a.info, b.info, a.order);
189
+ })
190
+ .map(t => t.info);
191
+ }
192
+ export default createRule({
193
+ meta: meta,
194
+ defaultSeverity: 'warning',
195
+ defaultValue: [],
196
+ defaultOptions: {
197
+ alphabetical: true,
198
+ },
199
+ async verify({ document, report, t }) {
200
+ const globalAttrs = new Set();
201
+ const eventAttrs = new Set();
202
+ const defs = document.specs.def?.['#globalAttrs'];
203
+ if (defs != null) {
204
+ const htmlGlobal = defs['#HTMLGlobalAttrs'];
205
+ if (htmlGlobal) {
206
+ for (const name of Object.keys(htmlGlobal)) {
207
+ globalAttrs.add(name);
208
+ }
209
+ }
210
+ const eventGlobal = defs['#GlobalEventAttrs'];
211
+ if (eventGlobal) {
212
+ for (const name of Object.keys(eventGlobal)) {
213
+ eventAttrs.add(name);
214
+ }
215
+ }
216
+ }
217
+ let cachedValue;
218
+ let cachedCompiled;
219
+ await document.walkOn('Element', node => {
220
+ const attrs = node.getAttributeTokens();
221
+ if (attrs.length <= 1) {
222
+ return;
223
+ }
224
+ const value = node.rule.value;
225
+ const options = node.rule.options;
226
+ const alphabetical = options.alphabetical ?? true;
227
+ const ms = node.rule.severity === 'error' ? 'must' : 'should';
228
+ const entries = value.length > 0 ? value : [];
229
+ const isAlphabeticalOnly = entries.length === 0;
230
+ let compiledEntries;
231
+ if (cachedValue === value) {
232
+ compiledEntries = cachedCompiled;
233
+ }
234
+ else {
235
+ compiledEntries = compileEntries(entries, globalAttrs, eventAttrs);
236
+ cachedValue = value;
237
+ cachedCompiled = compiledEntries;
238
+ }
239
+ const attrInfos = attrs.map((attr, i) => {
240
+ const nameNode = attr.nameNode;
241
+ return {
242
+ name: attr.name,
243
+ index: i,
244
+ isSpread: nameNode == null,
245
+ spacing: attr.spacesBeforeName?.raw ?? '',
246
+ content: buildAttrContent(attr),
247
+ tokens: attr,
248
+ scopeNode: nameNode ?? attr.spacesBeforeName,
249
+ };
250
+ });
251
+ const sorted = isAlphabeticalOnly
252
+ ? alphabetical
253
+ ? [...attrInfos].toSorted((a, b) => compareStrings(a.name, b.name))
254
+ : attrInfos
255
+ : sortAttrs(attrInfos, compiledEntries, alphabetical);
256
+ // Find first violation
257
+ for (let i = 0; i < attrInfos.length; i++) {
258
+ if (attrInfos[i] !== sorted[i]) {
259
+ const actual = attrInfos[i];
260
+ const expected = sorted[i];
261
+ const scope = expected.scopeNode;
262
+ if (!scope) {
263
+ continue;
264
+ }
265
+ report({
266
+ scope: {
267
+ rule: node.rule,
268
+ startLine: scope.startLine,
269
+ startCol: scope.startCol,
270
+ raw: scope.raw,
271
+ },
272
+ message: t(`{0} ${ms} be before {1}`, `"${expected.name || '(spread)'}"`, `"${actual.name || '(spread)'}"`),
273
+ fix: fixer => {
274
+ const range = getAttrRange(attrInfos);
275
+ if (!range) {
276
+ return [];
277
+ }
278
+ const newText = sorted
279
+ .map((info, j) => {
280
+ const spacing = attrInfos[j].spacing;
281
+ return spacing + info.content;
282
+ })
283
+ .join('');
284
+ return fixer.replaceRange(range, newText);
285
+ },
286
+ });
287
+ break;
288
+ }
289
+ }
290
+ });
291
+ },
292
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ readonly category: "style";
3
+ readonly fixable: true;
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ export default {
2
+ category: 'style',
3
+ fixable: true,
4
+ };
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `attr-value-quotes`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `attr-value-quotes`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `case-sensitive-attr-name`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `case-sensitive-attr-name`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `case-sensitive-tag-name`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `case-sensitive-tag-name`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -0,0 +1,8 @@
1
+ type OrderEntry = {
2
+ readonly selector: string;
3
+ readonly order?: 'source-order' | 'alphabetical';
4
+ readonly attr?: string;
5
+ };
6
+ export type Value = readonly (string | OrderEntry)[];
7
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<Value, undefined>>;
8
+ export default _default;
@@ -0,0 +1,147 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ const DEFAULT_VALUE = [
4
+ 'meta[charset]',
5
+ 'meta[http-equiv]',
6
+ 'meta[name="viewport"]',
7
+ 'title',
8
+ { selector: 'meta', order: 'alphabetical', attr: 'name' },
9
+ 'link',
10
+ 'style',
11
+ 'script',
12
+ ];
13
+ function normalizeEntries(value) {
14
+ return value.map(entry => {
15
+ if (typeof entry === 'string') {
16
+ return { selector: entry, order: 'source-order' };
17
+ }
18
+ return { selector: entry.selector, order: entry.order ?? 'source-order', attr: entry.attr };
19
+ });
20
+ }
21
+ export default createRule({
22
+ meta: meta,
23
+ defaultSeverity: 'warning',
24
+ defaultValue: DEFAULT_VALUE,
25
+ verify({ document, report, t }) {
26
+ const head = document.querySelector('head');
27
+ if (!head) {
28
+ return;
29
+ }
30
+ const children = [...head.children];
31
+ if (children.length <= 1) {
32
+ return;
33
+ }
34
+ const value = head.rule.value ?? DEFAULT_VALUE;
35
+ const entries = normalizeEntries(value);
36
+ // Use entries.length as the unmatched group index instead of Infinity
37
+ // to avoid potential Infinity arithmetic issues (e.g. Infinity - Infinity = NaN).
38
+ const unmatchedGroupIndex = entries.length;
39
+ // Use offset-based text extraction instead of AST traversal because
40
+ // raw text elements (script, style) have empty childNodes in the AST.
41
+ const docString = document.toString();
42
+ const blocks = children.map((el, sourceIndex) => {
43
+ let groupIndex = unmatchedGroupIndex;
44
+ for (const [i, entry_] of entries.entries()) {
45
+ if (el.matches(entry_.selector)) {
46
+ groupIndex = i;
47
+ break;
48
+ }
49
+ }
50
+ const entry = groupIndex < entries.length ? entries[groupIndex] : undefined;
51
+ let subKey;
52
+ if (entry && entry.order === 'alphabetical' && entry.attr) {
53
+ subKey = el.getAttribute(entry.attr) ?? '';
54
+ }
55
+ else {
56
+ subKey = sourceIndex;
57
+ }
58
+ const endOffset = getElementEndOffset(el);
59
+ const elementText = docString.slice(el.startOffset, endOffset);
60
+ return {
61
+ startOffset: el.startOffset,
62
+ groupIndex,
63
+ subKey,
64
+ sourceIndex,
65
+ elementText,
66
+ localName: el.localName,
67
+ };
68
+ });
69
+ const sorted = blocks.toSorted((a, b) => {
70
+ if (a.groupIndex !== b.groupIndex) {
71
+ return a.groupIndex - b.groupIndex;
72
+ }
73
+ if (typeof a.subKey === 'string' && typeof b.subKey === 'string') {
74
+ // Use simple comparison operators instead of localeCompare to ensure
75
+ // consistent ordering across different environments and locales.
76
+ if (a.subKey < b.subKey) {
77
+ return -1;
78
+ }
79
+ if (a.subKey > b.subKey) {
80
+ return 1;
81
+ }
82
+ }
83
+ return a.sourceIndex - b.sourceIndex;
84
+ });
85
+ // Only the first violation carries a fix because it replaces
86
+ // the entire <head> content at once to reorder all elements.
87
+ const ms = head.rule.severity === 'error' ? 'must' : 'should';
88
+ let firstViolation = true;
89
+ for (let i = 0; i < blocks.length; i++) {
90
+ if (blocks[i].sourceIndex !== sorted[i].sourceIndex) {
91
+ const currentEl = children[i];
92
+ const expectedInfo = sorted[i];
93
+ const currentInfo = blocks[i];
94
+ report({
95
+ scope: currentEl,
96
+ message: t(`{0} ${ms} be before {1}`, t('the "{0*}" {1}', expectedInfo.localName, 'element'), t('the "{0*}" {1}', currentInfo.localName, 'element')),
97
+ fix: firstViolation
98
+ ? fixer => {
99
+ const headOpenEnd = head.startOffset + head.raw.length;
100
+ const lastChild = children.at(-1);
101
+ const headCloseStart = head.closeTag
102
+ ? head.closeTag.startOffset
103
+ : getElementEndOffset(lastChild);
104
+ const originalContent = docString.slice(headOpenEnd, headCloseStart);
105
+ const newContent = buildFixedContent(originalContent, headOpenEnd, blocks, sorted);
106
+ return fixer.replaceRange([headOpenEnd, headCloseStart], newContent);
107
+ }
108
+ : undefined,
109
+ });
110
+ firstViolation = false;
111
+ }
112
+ }
113
+ },
114
+ });
115
+ function getElementEndOffset(el) {
116
+ if (el.closeTag) {
117
+ return el.closeTag.startOffset + el.closeTag.raw.length;
118
+ }
119
+ return el.startOffset + el.raw.length;
120
+ }
121
+ function buildFixedContent(originalContent, headOpenEnd, blocks, sorted) {
122
+ const prefixes = [];
123
+ for (let i = 0; i < blocks.length; i++) {
124
+ const block = blocks[i];
125
+ const elStart = block.startOffset - headOpenEnd;
126
+ let prefixStart;
127
+ if (i === 0) {
128
+ prefixStart = 0;
129
+ }
130
+ else {
131
+ const prevBlock = blocks[i - 1];
132
+ const prevEnd = prevBlock.startOffset - headOpenEnd + prevBlock.elementText.length;
133
+ prefixStart = prevEnd;
134
+ }
135
+ prefixes.push(originalContent.slice(prefixStart, elStart));
136
+ }
137
+ const lastBlock = blocks.at(-1);
138
+ const trailStart = lastBlock.startOffset - headOpenEnd + lastBlock.elementText.length;
139
+ const trailing = originalContent.slice(trailStart);
140
+ let result = '';
141
+ for (const [i, element] of sorted.entries()) {
142
+ result += prefixes[i];
143
+ result += element.elementText;
144
+ }
145
+ result += trailing;
146
+ return result;
147
+ }
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ readonly category: "style";
3
+ readonly fixable: true;
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ export default {
2
+ category: 'style',
3
+ fixable: true,
4
+ };
package/lib/helpers.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Log } from './debug.js';
2
2
  import type { Translator } from '@markuplint/i18n';
3
- import type { PlainData } from '@markuplint/ml-config';
3
+ import type { FixToken, IRuleFixer, PlainData, TextEdit } from '@markuplint/ml-config';
4
4
  import type { Element, RuleConfigValue, Document } from '@markuplint/ml-core';
5
5
  import type { Attribute } from '@markuplint/ml-spec';
6
6
  import type { WritableDeep } from 'type-fest';
@@ -155,3 +155,38 @@ export declare class Collection<T> {
155
155
  * @returns A mutable deep clone of the input value
156
156
  */
157
157
  export declare function deepCopy<T>(value: T): WritableDeep<T>;
158
+ /**
159
+ * Creates a fix that removes an entire attribute (name + value + surrounding whitespace).
160
+ * Used by rules that need to remove a whole attribute from an element.
161
+ *
162
+ * @param fixer - The rule fixer instance for building TextEdits
163
+ * @param attr - The attribute token parts to remove (from `spacesBeforeName` through `endQuote`)
164
+ * @returns A TextEdit (or empty array if no valid tokens exist) that removes the attribute
165
+ */
166
+ export declare function removeAttr(fixer: IRuleFixer, attr: {
167
+ readonly spacesBeforeName: FixToken | null;
168
+ readonly nameNode: FixToken | null;
169
+ readonly spacesBeforeEqual: FixToken | null;
170
+ readonly equal: FixToken | null;
171
+ readonly spacesAfterEqual: FixToken | null;
172
+ readonly startQuote: FixToken | null;
173
+ readonly valueNode: FixToken | null;
174
+ readonly endQuote: FixToken | null;
175
+ }): TextEdit | TextEdit[];
176
+ /**
177
+ * Creates a fix that removes only the value portion of an attribute
178
+ * (equals sign, quotes, and value), keeping the attribute name.
179
+ * Used by `no-boolean-attr-value` to convert `disabled="disabled"` to `disabled`.
180
+ *
181
+ * @param fixer - The rule fixer instance for building TextEdits
182
+ * @param attr - The attribute token parts to remove (from `spacesBeforeEqual` through `endQuote`)
183
+ * @returns A TextEdit (or empty array if no valid tokens exist) that removes the value portion
184
+ */
185
+ export declare function removeAttrValue(fixer: IRuleFixer, attr: {
186
+ readonly spacesBeforeEqual: FixToken | null;
187
+ readonly equal: FixToken | null;
188
+ readonly spacesAfterEqual: FixToken | null;
189
+ readonly startQuote: FixToken | null;
190
+ readonly valueNode: FixToken | null;
191
+ readonly endQuote: FixToken | null;
192
+ }): TextEdit | TextEdit[];
package/lib/helpers.js CHANGED
@@ -263,3 +263,71 @@ export class Collection {
263
263
  export function deepCopy(value) {
264
264
  return structuredClone(value);
265
265
  }
266
+ /**
267
+ * Computes a removal range from a list of nullable tokens.
268
+ * Filters out null/undefined tokens and tokens with empty `raw`,
269
+ * then returns the range spanning from the first to the last token.
270
+ *
271
+ * @param tokens - An array of nullable tokens to compute the range from
272
+ * @returns `[start, end]` tuple, or `null` if no valid tokens
273
+ */
274
+ function tokenRange(tokens) {
275
+ const valid = tokens.filter((t) => t != null && t.raw.length > 0);
276
+ const first = valid[0];
277
+ const last = valid.at(-1);
278
+ if (!first || !last) {
279
+ return null;
280
+ }
281
+ return [first.startOffset, last.startOffset + last.raw.length];
282
+ }
283
+ /**
284
+ * Creates a fix that removes an entire attribute (name + value + surrounding whitespace).
285
+ * Used by rules that need to remove a whole attribute from an element.
286
+ *
287
+ * @param fixer - The rule fixer instance for building TextEdits
288
+ * @param attr - The attribute token parts to remove (from `spacesBeforeName` through `endQuote`)
289
+ * @returns A TextEdit (or empty array if no valid tokens exist) that removes the attribute
290
+ */
291
+ export function removeAttr(
292
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
293
+ fixer, attr) {
294
+ const range = tokenRange([
295
+ attr.spacesBeforeName,
296
+ attr.nameNode,
297
+ attr.spacesBeforeEqual,
298
+ attr.equal,
299
+ attr.spacesAfterEqual,
300
+ attr.startQuote,
301
+ attr.valueNode,
302
+ attr.endQuote,
303
+ ]);
304
+ if (!range) {
305
+ return [];
306
+ }
307
+ return fixer.removeRange(range);
308
+ }
309
+ /**
310
+ * Creates a fix that removes only the value portion of an attribute
311
+ * (equals sign, quotes, and value), keeping the attribute name.
312
+ * Used by `no-boolean-attr-value` to convert `disabled="disabled"` to `disabled`.
313
+ *
314
+ * @param fixer - The rule fixer instance for building TextEdits
315
+ * @param attr - The attribute token parts to remove (from `spacesBeforeEqual` through `endQuote`)
316
+ * @returns A TextEdit (or empty array if no valid tokens exist) that removes the value portion
317
+ */
318
+ export function removeAttrValue(
319
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
320
+ fixer, attr) {
321
+ const range = tokenRange([
322
+ attr.spacesBeforeEqual,
323
+ attr.equal,
324
+ attr.spacesAfterEqual,
325
+ attr.startQuote,
326
+ attr.valueNode,
327
+ attr.endQuote,
328
+ ]);
329
+ if (!range) {
330
+ return [];
331
+ }
332
+ return fixer.removeRange(range);
333
+ }
package/lib/index.d.ts CHANGED
@@ -12,6 +12,14 @@
12
12
  */
13
13
  declare const rules: {
14
14
  readonly 'attr-duplication': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
15
+ readonly 'attr-order': Readonly<import("@markuplint/ml-core").RuleSeed<(string | {
16
+ name?: string;
17
+ pattern?: string;
18
+ group?: "global" | "event" | "aria" | "data" | "spread";
19
+ order?: "alphabetical" | string[] | "source-order";
20
+ })[], {
21
+ alphabetical?: boolean;
22
+ }>>;
15
23
  readonly 'attr-value-quotes': Readonly<import("@markuplint/ml-core").RuleSeed<import("./attr-value-quotes/index.js").Type, undefined>>;
16
24
  readonly 'case-sensitive-attr-name': Readonly<import("@markuplint/ml-core").RuleSeed<import("./case-sensitive-attr-name/index.js").Value, undefined>>;
17
25
  readonly 'case-sensitive-tag-name': Readonly<import("@markuplint/ml-core").RuleSeed<import("./case-sensitive-tag-name/index.js").Value, undefined>>;
@@ -27,6 +35,7 @@ declare const rules: {
27
35
  denyObsoleteType: boolean;
28
36
  }>>;
29
37
  readonly 'end-tag': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, undefined>>;
38
+ readonly 'head-element-order': Readonly<import("@markuplint/ml-core").RuleSeed<import("./head-element-order/index.js").Value, undefined>>;
30
39
  readonly 'heading-levels': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
31
40
  readonly 'id-duplication': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
32
41
  readonly 'ineffective-attr': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
@@ -74,7 +83,7 @@ declare const rules: {
74
83
  readonly checkExperimental?: boolean;
75
84
  readonly checkNonStandard?: boolean;
76
85
  }>>;
77
- readonly 'no-use-event-handler-attr': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, {
86
+ readonly 'no-use-event-handler-attr': Readonly<import("@markuplint/ml-core").RuleSeed<boolean | readonly string[], {
78
87
  ignore?: string | string[];
79
88
  }>>;
80
89
  readonly 'permitted-contents': Readonly<import("@markuplint/ml-core").RuleSeed<import("./permitted-contents/types.js").TagRule[], import("./permitted-contents/types.js").Options>>;
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * areas such as accessibility, HTML validity, naming conventions, and coding style.
8
8
  */
9
9
  import AttrDuplication from './attr-duplication/index.js';
10
+ import AttrOrder from './attr-order/index.js';
10
11
  import AttrValueQuotes from './attr-value-quotes/index.js';
11
12
  import CaseSensitiveAttrName from './case-sensitive-attr-name/index.js';
12
13
  import CaseSensitiveTagName from './case-sensitive-tag-name/index.js';
@@ -18,6 +19,7 @@ import DeprecatedElement from './deprecated-element/index.js';
18
19
  import DisallowedElement from './disallowed-element/index.js';
19
20
  import Doctype from './doctype/index.js';
20
21
  import EndTag from './end-tag/index.js';
22
+ import HeadElementOrder from './head-element-order/index.js';
21
23
  import HeadingLevels from './heading-levels/index.js';
22
24
  import IdDuplication from './id-duplication/index.js';
23
25
  import IneffectiveAttr from './ineffective-attr/index.js';
@@ -56,6 +58,7 @@ import WaiAria from './wai-aria/index.js';
56
58
  */
57
59
  const rules = {
58
60
  'attr-duplication': AttrDuplication,
61
+ 'attr-order': AttrOrder,
59
62
  'attr-value-quotes': AttrValueQuotes,
60
63
  'case-sensitive-attr-name': CaseSensitiveAttrName,
61
64
  'case-sensitive-tag-name': CaseSensitiveTagName,
@@ -67,6 +70,7 @@ const rules = {
67
70
  'disallowed-element': DisallowedElement,
68
71
  doctype: Doctype,
69
72
  'end-tag': EndTag,
73
+ 'head-element-order': HeadElementOrder,
70
74
  'heading-levels': HeadingLevels,
71
75
  'id-duplication': IdDuplication,
72
76
  'ineffective-attr': IneffectiveAttr,
@@ -1,5 +1,6 @@
1
1
  import { createRule, getAttrSpecs } from '@markuplint/ml-core';
2
2
  import { toNoEmptyStringArrayFromStringOrArray } from '@markuplint/shared';
3
+ import { removeAttr } from '../helpers.js';
3
4
  import meta from './meta.js';
4
5
  /**
5
6
  * Rule that detects attributes that have no effect on their owner element.
@@ -29,6 +30,7 @@ export default createRule({
29
30
  message: t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'ineffective') +
30
31
  t('. ') +
31
32
  t("It doesn't need {0}", t('the {0}', 'attribute')),
33
+ fix: fixer => removeAttr(fixer, attr),
32
34
  });
33
35
  }
34
36
  });
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `ineffective-attr`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `ineffective-attr`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -1,4 +1,5 @@
1
1
  import { createRule, getAttrSpecs } from '@markuplint/ml-core';
2
+ import { removeAttrValue } from '../helpers.js';
2
3
  import meta from './meta.js';
3
4
  /**
4
5
  * Rule that disallows explicit values on boolean attributes.
@@ -45,6 +46,7 @@ export default createRule({
45
46
  message: t('{0} is {1}', t('the "{0*}" {1}', name, 'attribute'), t('a {0}', 'boolean attribute')) +
46
47
  t('. ') +
47
48
  t("It doesn't need {0}", t('the {0}', 'value')),
49
+ fix: fixer => removeAttrValue(fixer, attr),
48
50
  });
49
51
  }
50
52
  });
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `no-boolean-attr-value`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `no-boolean-attr-value`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -19,9 +19,11 @@ export default createRule({
19
19
  next = next.nextSibling;
20
20
  }
21
21
  if (next && next.nodeName === 'BR') {
22
+ const target = next;
22
23
  report({
23
- scope: next,
24
+ scope: target,
24
25
  message: t('{0} detected', t('Consecutive {0}', t('"{0*}" {1}', 'br', 'elements'))),
26
+ fix: fixer => fixer.remove({ startOffset: target.startOffset, raw: target.raw }),
25
27
  });
26
28
  }
27
29
  }
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `no-consecutive-br`: categorized as an accessibility rule. */
2
2
  declare const _default: {
3
3
  readonly category: "a11y";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `no-consecutive-br`: categorized as an accessibility rule. */
2
2
  export default {
3
3
  category: 'a11y',
4
+ fixable: true,
4
5
  };
@@ -1,5 +1,5 @@
1
1
  import { createRule, getAttrSpecs } from '@markuplint/ml-core';
2
- import { toNormalizedValue } from '../helpers.js';
2
+ import { removeAttr, toNormalizedValue } from '../helpers.js';
3
3
  import meta from './meta.js';
4
4
  /**
5
5
  * Rule that disallows explicitly setting an attribute to its default value.
@@ -33,6 +33,7 @@ export default createRule({
33
33
  col: attr.valueNode?.startCol,
34
34
  raw: attr.valueNode?.raw,
35
35
  message: t('It is {0}', t('the {0}', 'default value')),
36
+ fix: fixer => removeAttr(fixer, attr),
36
37
  });
37
38
  }
38
39
  });
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `no-default-value`: categorized as a style rule. */
2
2
  declare const _default: {
3
3
  readonly category: "style";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `no-default-value`: categorized as a style rule. */
2
2
  export default {
3
3
  category: 'style',
4
+ fixable: true,
4
5
  };
@@ -14,7 +14,8 @@ export default createRule({
14
14
  const dtList = [...dl.querySelectorAll(':scope > dt, :scope > div > dt')];
15
15
  const names = new Set();
16
16
  for (const dt of dtList) {
17
- // TODO: Supoort for alternative text for images and accessible names for contained elements.
17
+ // Support for alternative text for images and accessible names for contained elements
18
+ // is not yet implemented; currently only textContent is used for comparison.
18
19
  const name = dt.textContent?.trim();
19
20
  if (!name) {
20
21
  continue;
@@ -15,6 +15,7 @@ export default createRule({
15
15
  report({
16
16
  scope: text,
17
17
  message: t('{0} detected', t('Orphaned end tag')),
18
+ fix: fixer => fixer.remove({ startOffset: text.startOffset, raw: text.raw }),
18
19
  });
19
20
  }
20
21
  });
@@ -1,5 +1,6 @@
1
1
  /** Rule metadata for `no-orphaned-end-tag`: categorized as a validation rule. */
2
2
  declare const _default: {
3
3
  readonly category: "validation";
4
+ readonly fixable: true;
4
5
  };
5
6
  export default _default;
@@ -1,4 +1,5 @@
1
1
  /** Rule metadata for `no-orphaned-end-tag`: categorized as a validation rule. */
2
2
  export default {
3
3
  category: 'validation',
4
+ fixable: true,
4
5
  };
@@ -24,12 +24,24 @@ let bcdPromise;
24
24
  * at startup, even when this rule is disabled. Dynamic import defers the
25
25
  * cost until the rule actually runs.
26
26
  *
27
+ * Uses `forLegacyNode` to avoid `ERR_IMPORT_ATTRIBUTE_MISSING` on Node.js >= 22.
28
+ * The main entry (`@mdn/browser-compat-data`) points directly to `data.json`,
29
+ * which requires `import ... with { type: "json" }` on Node.js >= 22.
30
+ * The `forLegacyNode` wrapper uses `fs.readFileSync` instead, compatible
31
+ * with all Node.js versions.
32
+ *
33
+ * NOTE: If `forLegacyNode` is removed in a future BCD version, migrate to:
34
+ * `import('...', { with: { type: 'json' } })`
35
+ * once the minimum supported Node.js version supports import attributes.
36
+ *
27
37
  * Uses a Promise-based cache to prevent race conditions when multiple
28
38
  * concurrent calls trigger the import simultaneously.
29
39
  */
30
40
  function loadBcd() {
31
41
  if (!bcdPromise) {
32
- bcdPromise = import('@mdn/browser-compat-data').then(mod => mod.default ?? mod);
42
+ // `?? mod` is a defensive fallback for CJS interop environments
43
+ // where `default` may not be set on the module namespace object.
44
+ bcdPromise = import('@mdn/browser-compat-data/forLegacyNode').then(mod => mod.default ?? mod);
33
45
  }
34
46
  return bcdPromise;
35
47
  }
@@ -10,6 +10,9 @@ type Options = {
10
10
  * Reports any attribute on an HTML element whose name starts with `on`,
11
11
  * indicating an inline event handler. An `ignore` option allows specific
12
12
  * attribute names or patterns to be excluded from the check.
13
+ *
14
+ * The value can be `true` (disallow all event handlers) or a `string[]`
15
+ * of event names (without the `on` prefix) to selectively disallow.
13
16
  */
14
- declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
17
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean | readonly string[], Options>>;
15
18
  export default _default;
@@ -1,6 +1,25 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
2
  import { match } from '../helpers.js';
3
3
  import meta from './meta.js';
4
+ /**
5
+ * Extracts the event name from an attribute name by stripping the `on` prefix
6
+ * and lowercasing. Returns `null` if the attribute is not an event handler.
7
+ */
8
+ function extractEventName(attrName) {
9
+ const m = /^on(.+)/i.exec(attrName);
10
+ return m?.[1] ? m[1].toLowerCase() : null;
11
+ }
12
+ /**
13
+ * Tests whether an event name matches a pattern. The pattern is either a plain
14
+ * string (compared case-insensitively) or a regex literal (`/pattern/flags`).
15
+ */
16
+ function matchEventName(eventName, pattern) {
17
+ const regexMatch = /^\/(.*)\/([gim])*$/.exec(pattern);
18
+ if (regexMatch?.[1]) {
19
+ return new RegExp(regexMatch[1], regexMatch[2]).test(eventName);
20
+ }
21
+ return eventName === pattern.toLowerCase();
22
+ }
4
23
  /**
5
24
  * Rule that disallows inline event handler attributes (e.g., `onclick`,
6
25
  * `onchange`).
@@ -8,6 +27,9 @@ import meta from './meta.js';
8
27
  * Reports any attribute on an HTML element whose name starts with `on`,
9
28
  * indicating an inline event handler. An `ignore` option allows specific
10
29
  * attribute names or patterns to be excluded from the check.
30
+ *
31
+ * The value can be `true` (disallow all event handlers) or a `string[]`
32
+ * of event names (without the `on` prefix) to selectively disallow.
11
33
  */
12
34
  export default createRule({
13
35
  meta: meta,
@@ -29,7 +51,12 @@ export default createRule({
29
51
  return;
30
52
  }
31
53
  }
32
- if (/^on/i.test(name)) {
54
+ const eventName = extractEventName(name);
55
+ if (eventName == null) {
56
+ return;
57
+ }
58
+ const { value } = attr.rule;
59
+ if (value === true) {
33
60
  report({
34
61
  scope: attr,
35
62
  raw: attr.raw,
@@ -37,6 +64,21 @@ export default createRule({
37
64
  col: attr.startCol,
38
65
  message: t('{0} is disallowed', t('the "{0*}" {1}', name, 'attribute')),
39
66
  });
67
+ return;
68
+ }
69
+ if (Array.isArray(value)) {
70
+ for (const pattern of value) {
71
+ if (matchEventName(eventName, pattern)) {
72
+ report({
73
+ scope: attr,
74
+ raw: attr.raw,
75
+ line: attr.startLine,
76
+ col: attr.startCol,
77
+ message: t('{0} is disallowed', t('the "{0*}" {1}', name, 'attribute')),
78
+ });
79
+ return;
80
+ }
81
+ }
40
82
  }
41
83
  });
42
84
  },
@@ -30,10 +30,7 @@ export const checkingNonExistentRole = ({ attr }) => t => {
30
30
  if (!role) {
31
31
  return {
32
32
  scope: token,
33
- message: t('{0} according to {1}', t('{0} does not exist', t('the "{0*}" {1}', token.raw, 'role')), 'the WAI-ARIA specification') +
34
- t('.') +
35
- // TODO: Translate
36
- ` This "${token.raw}" role does not exist in WAI-ARIA.`,
33
+ message: t('{0} according to {1}', t('{0} does not exist', t('the "{0*}" {1}', token.raw, 'role')), 'the WAI-ARIA specification') + t('.'),
37
34
  };
38
35
  }
39
36
  }
@@ -33,7 +33,9 @@ export const checkingRequiredOwnedElements = ({ el, role }) => t => {
33
33
  if (el.matches('[aria-busy="true" i]')) {
34
34
  return;
35
35
  }
36
- // TODO: Needs to resolve `aria-own`
36
+ // TODO: Needs to resolve `aria-owns` references to include virtually owned elements.
37
+ // Currently, only DOM-tree children are checked. Elements referenced via `aria-owns`
38
+ // should also be considered as owned elements per the ARIA specification.
37
39
  const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
38
40
  const children = classifyChildren(el, role, ariaVersion);
39
41
  if (children.some(([, type]) => type === 'BUSY')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/rules",
3
- "version": "5.0.0-alpha.2",
3
+ "version": "5.0.0-dev.5+e96392f56",
4
4
  "description": "Built-in rules of markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -27,12 +27,12 @@
27
27
  "./lib/permitted-contents/debug.js": "./lib/permitted-contents/debug.browser.js"
28
28
  },
29
29
  "dependencies": {
30
- "@markuplint/html-spec": "5.0.0-alpha.2",
31
- "@markuplint/ml-core": "5.0.0-alpha.2",
32
- "@markuplint/ml-spec": "5.0.0-alpha.2",
33
- "@markuplint/selector": "5.0.0-alpha.2",
34
- "@markuplint/shared": "5.0.0-alpha.2",
35
- "@markuplint/types": "5.0.0-alpha.2",
30
+ "@markuplint/html-spec": "5.0.0-dev.5+e96392f56",
31
+ "@markuplint/ml-core": "5.0.0-dev.5+e96392f56",
32
+ "@markuplint/ml-spec": "5.0.0-dev.5+e96392f56",
33
+ "@markuplint/selector": "5.0.0-dev.5+e96392f56",
34
+ "@markuplint/shared": "5.0.0-dev.5+e96392f56",
35
+ "@markuplint/types": "5.0.0-dev.5+e96392f56",
36
36
  "@mdn/browser-compat-data": "^7.3.2",
37
37
  "@types/debug": "4.1.12",
38
38
  "ansi-colors": "4.1.3",
@@ -42,5 +42,5 @@
42
42
  "image-size": "^2.0.2",
43
43
  "type-fest": "5.4.4"
44
44
  },
45
- "gitHead": "31ccf1e81443ea3f93597d287595211f1823ddcf"
45
+ "gitHead": "e96392f56e4bc8165ba59622b41c822703a96372"
46
46
  }
package/schema.json CHANGED
@@ -9,6 +9,9 @@
9
9
  "attr-duplication": {
10
10
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/attr-duplication/schema.json"
11
11
  },
12
+ "attr-order": {
13
+ "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/attr-order/schema.json"
14
+ },
12
15
  "attr-value-quotes": {
13
16
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/attr-value-quotes/schema.json"
14
17
  },
@@ -42,6 +45,9 @@
42
45
  "end-tag": {
43
46
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/end-tag/schema.json"
44
47
  },
48
+ "head-element-order": {
49
+ "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/head-element-order/schema.json"
50
+ },
45
51
  "heading-levels": {
46
52
  "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/rules/src/heading-levels/schema.json"
47
53
  },