@markuplint/rules 5.2.0 → 5.4.0

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 (123) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CLAUDE.md +19 -19
  3. package/lib/aria-prop-requires-role/index.js +2 -1
  4. package/lib/attr-check.js +6 -12
  5. package/lib/attr-eligibility.d.ts +15 -8
  6. package/lib/attr-eligibility.js +19 -10
  7. package/lib/attr-order/index.js +24 -18
  8. package/lib/case-sensitive-attr-name/index.js +24 -5
  9. package/lib/class-naming/index.js +6 -0
  10. package/lib/debug.d.ts +8 -1
  11. package/lib/debug.js +8 -1
  12. package/lib/element-supports-aria-prop/index.js +2 -1
  13. package/lib/form-attr-references-form/index.d.ts +6 -3
  14. package/lib/form-attr-references-form/index.js +6 -3
  15. package/lib/helpers.d.ts +111 -1
  16. package/lib/helpers.js +212 -2
  17. package/lib/input-list-references-datalist/index.d.ts +8 -6
  18. package/lib/input-list-references-datalist/index.js +8 -6
  19. package/lib/label-for-references-labelable/index.d.ts +8 -5
  20. package/lib/label-for-references-labelable/index.js +8 -5
  21. package/lib/landmark-roles/roles.js +14 -2
  22. package/lib/map-id-name-match/index.d.ts +3 -3
  23. package/lib/map-id-name-match/index.js +3 -3
  24. package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
  25. package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
  26. package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
  27. package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
  28. package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
  29. package/lib/no-aria-on-presentational-children/index.js +3 -1
  30. package/lib/no-aria-on-unsupported-element/index.js +2 -2
  31. package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
  32. package/lib/no-aria-on-unsupported-element/meta.js +1 -1
  33. package/lib/no-boolean-attr-value/index.js +2 -2
  34. package/lib/no-content-after-body/index.d.ts +2 -2
  35. package/lib/no-content-after-body/index.js +2 -2
  36. package/lib/no-contradictory-aria-prop/index.js +2 -1
  37. package/lib/no-default-aria-value/index.js +2 -1
  38. package/lib/no-default-value/index.js +2 -2
  39. package/lib/no-deprecated-aria-prop/index.js +2 -1
  40. package/lib/no-deprecated-attr/index.js +2 -1
  41. package/lib/no-disallowed-attr/index.d.ts +7 -1
  42. package/lib/no-disallowed-attr/index.js +13 -20
  43. package/lib/no-duplicate-attr/index.js +1 -2
  44. package/lib/no-duplicate-autofocus/index.js +2 -2
  45. package/lib/no-duplicate-h1/index.d.ts +1 -1
  46. package/lib/no-duplicate-h1/index.js +1 -1
  47. package/lib/no-empty-table-track/index.d.ts +6 -2
  48. package/lib/no-empty-table-track/index.js +6 -2
  49. package/lib/no-experimental-features/index.d.ts +5 -1
  50. package/lib/no-experimental-features/index.js +6 -8
  51. package/lib/no-hardcoded-id/index.js +1 -1
  52. package/lib/no-ineffective-attr/index.js +2 -2
  53. package/lib/no-input-file-value/index.d.ts +10 -4
  54. package/lib/no-input-file-value/index.js +10 -4
  55. package/lib/no-invalid-aria-prop-value/index.js +2 -1
  56. package/lib/no-invalid-attr-value/index.d.ts +3 -0
  57. package/lib/no-invalid-attr-value/index.js +9 -24
  58. package/lib/no-nonstandard-features/index.d.ts +5 -1
  59. package/lib/no-nonstandard-features/index.js +6 -8
  60. package/lib/no-obsolete-attr/index.js +3 -3
  61. package/lib/no-prohibited-naming/index.js +2 -1
  62. package/lib/no-redundant-accessible-name/index.js +17 -1
  63. package/lib/no-redundant-aria-prop/index.js +2 -1
  64. package/lib/no-refer-to-non-existent-id/index.js +4 -4
  65. package/lib/no-restricted-attr/index.d.ts +4 -0
  66. package/lib/no-restricted-attr/index.js +5 -22
  67. package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
  68. package/lib/no-stray-head-or-body-tag/index.js +11 -6
  69. package/lib/no-unknown-attr/index.d.ts +7 -1
  70. package/lib/no-unknown-attr/index.js +11 -25
  71. package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
  72. package/lib/no-unpaired-srcset-sizes/index.js +23 -17
  73. package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
  74. package/lib/no-unsupported-browser-features/compat-data.js +48 -3
  75. package/lib/no-unsupported-browser-features/index.d.ts +5 -1
  76. package/lib/no-unsupported-browser-features/index.js +6 -7
  77. package/lib/permitted-contents/choice.js +19 -8
  78. package/lib/permitted-contents/content-model.d.ts +24 -1
  79. package/lib/permitted-contents/content-model.js +56 -9
  80. package/lib/permitted-contents/count-pattern.js +36 -13
  81. package/lib/permitted-contents/debug.browser.d.ts +18 -0
  82. package/lib/permitted-contents/debug.browser.js +27 -0
  83. package/lib/permitted-contents/debug.d.ts +18 -0
  84. package/lib/permitted-contents/debug.js +27 -0
  85. package/lib/permitted-contents/index.js +2 -2
  86. package/lib/permitted-contents/matches-selector.js +40 -18
  87. package/lib/permitted-contents/order.js +23 -11
  88. package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
  89. package/lib/permitted-contents/transparent.js +6 -2
  90. package/lib/placeholder-label-option/index.d.ts +3 -3
  91. package/lib/placeholder-label-option/index.js +163 -37
  92. package/lib/progress-value-bounds/index.d.ts +1 -1
  93. package/lib/progress-value-bounds/index.js +1 -1
  94. package/lib/require-attr/index.d.ts +6 -1
  95. package/lib/require-attr/index.js +13 -5
  96. package/lib/require-h1/index.d.ts +1 -1
  97. package/lib/require-h1/index.js +1 -1
  98. package/lib/role-supports-aria-prop/index.js +2 -1
  99. package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
  100. package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
  101. package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
  102. package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
  103. package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
  104. package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
  105. package/lib/valid-speculation-rules/index.js +1 -1
  106. package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
  107. package/lib/wai-aria/checkings/default-value.js +1 -1
  108. package/lib/wai-aria/checkings/deprecated-props.js +2 -2
  109. package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
  110. package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
  111. package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
  112. package/lib/wai-aria/checkings/no-global-prop.js +1 -1
  113. package/lib/wai-aria/checkings/presentational-children.d.ts +21 -10
  114. package/lib/wai-aria/checkings/presentational-children.js +240 -26
  115. package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
  116. package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
  117. package/lib/wai-aria/checkings/required-prop.js +1 -4
  118. package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
  119. package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
  120. package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
  121. package/lib/wai-aria/checkings/value.d.ts +1 -1
  122. package/lib/wai-aria/checkings/value.js +2 -2
  123. package/package.json +15 -15
@@ -81,30 +81,77 @@ export function resolveContentModel(
81
81
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
82
82
  el, rules, specs, mode) {
83
83
  if (mode === 'origin' && el.pretenderContext?.type === 'pretender') {
84
- const userRule = rules.find(r => r.tag === el.rawName);
85
- return userRule?.contents ?? null;
84
+ const userRule = rules.find(r => hasTagRule(el, r));
85
+ return userRule ? contentsOfTagRule(el, userRule) : null;
86
86
  }
87
- return getContentModel(el, specs.specs);
87
+ const model = getContentModel(el, specs.specs);
88
+ if (model != null || el.pretenderContext?.type === 'pretender') {
89
+ return model;
90
+ }
91
+ // A rule written for a component under another spelling of its name (`FooBar` for `<foo-bar>`):
92
+ // the lookup of the spec is by the name as written, which the element does not have.
93
+ const userRule = rules.find(r => el.matchesTagNameVariant(r.tag));
94
+ if (!userRule) {
95
+ return null;
96
+ }
97
+ return contentsOfTagRule(el, userRule);
98
+ }
99
+ /**
100
+ * The contents of a user's tag rule for the element: those of the first `conditional` entry whose
101
+ * condition the element meets, or else the rule's own, as `getContentModel` of `@markuplint/ml-spec`
102
+ * reads a spec. It is for the lookups that find the rule without `getContentModel`.
103
+ *
104
+ * @param el - The element
105
+ * @param rule - The user's tag rule written for the element
106
+ * @returns The contents that apply to the element
107
+ */
108
+ function contentsOfTagRule(
109
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
110
+ el, rule) {
111
+ return rule.conditional?.find(({ condition }) => el.matches(condition))?.contents ?? rule.contents;
88
112
  }
89
113
  /**
90
- * Cache for merged specs keyed by the JSON-serialized tag rules.
114
+ * Whether the user's tag rule is written for the element: the name of the rule is the name
115
+ * the element is written with, or another spelling of it that the document resolves to the
116
+ * same component (see `MLElement#matchesTagNameVariant`).
117
+ *
118
+ * @param el - The element
119
+ * @param rule - The user's tag rule
120
+ * @returns Whether the rule is written for the element
121
+ */
122
+ export function hasTagRule(
123
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
124
+ el, rule) {
125
+ return rule.tag === el.rawName || el.matchesTagNameVariant(rule.tag);
126
+ }
127
+ /**
128
+ * Cache for merged specs: for each base spec of a document, keyed by the JSON-serialized tag rules.
91
129
  * Avoids re-merging the same set of user-defined tag rules on every element check.
92
130
  */
93
- const caches = new Map();
131
+ const caches = new WeakMap();
94
132
  /**
95
133
  * Returns a Specs object that merges the base ML spec with user-defined
96
- * tag rules. Results are cached by the serialized rules to avoid redundant merging.
134
+ * tag rules. Results are cached by the specs of the document and the serialized rules to avoid redundant
135
+ * merging: the specs differ by document (the `specs` of the config is by file), so what is merged
136
+ * for one must not be given to another.
137
+ *
138
+ * Exported for the spec of the cache.
97
139
  *
98
140
  * @param specs - The base ML spec from the document.
99
141
  * @param rules - User-defined tag rules to merge.
100
142
  * @returns The merged Specs, either from cache or freshly computed.
101
143
  */
102
- function cachedSpecs(specs, rules) {
144
+ export function cachedSpecs(specs, rules) {
103
145
  if (rules.length === 0) {
104
146
  return specs;
105
147
  }
106
148
  const key = JSON.stringify(rules);
107
- const cached = caches.get(key);
149
+ let cache = caches.get(specs);
150
+ if (!cache) {
151
+ cache = new Map();
152
+ caches.set(specs, cache);
153
+ }
154
+ const cached = cache.get(key);
108
155
  if (cached) {
109
156
  return cached;
110
157
  }
@@ -118,6 +165,6 @@ function cachedSpecs(specs, rules) {
118
165
  })),
119
166
  ],
120
167
  };
121
- caches.set(key, merged);
168
+ cache.set(key, merged);
122
169
  return merged;
123
170
  }
@@ -1,4 +1,4 @@
1
- import { cmLog } from './debug.js';
1
+ import { cmLog, depthLog } from './debug.js';
2
2
  import { recursiveBranch } from './recursive-branch.js';
3
3
  import { Collection, mergeHints, modelLog, normalizeModel } from './utils.js';
4
4
  const cLog = cmLog.extend('countCompereResult');
@@ -24,21 +24,28 @@ const cLog = cmLog.extend('countCompereResult');
24
24
  export function countPattern(pattern,
25
25
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
26
26
  childNodes, rules, specs, options, depth, mode) {
27
- const ptLog = cmLog.extend(`countPattern#${depth}`);
27
+ const ptLog = depthLog('countPattern', depth);
28
+ const ptEnabled = ptLog.enabled;
28
29
  const collection = new Collection(childNodes);
29
30
  const { model, min, max, repeat, missingType } = normalizeModel(pattern);
30
- ptLog('Model:\n RegEx: %s\n Schema: %o', modelLog(model, repeat), pattern);
31
+ if (ptEnabled) {
32
+ ptLog('Model:\n RegEx: %s\n Schema: %o', modelLog(model, repeat), pattern);
33
+ }
31
34
  let prevResult = null;
32
35
  let barelyResult = null;
33
36
  let loopCount = 0;
34
37
  while (true) {
35
38
  loopCount++;
36
- ptLog('Check#%s: %s', loopCount, collection);
39
+ if (ptEnabled) {
40
+ ptLog('Check#%s: %s', loopCount, collection);
41
+ }
37
42
  const result = recursiveBranch(model, collection.unmatched, rules, specs, options, depth, mode);
38
43
  const added = collection.addMatched(result.matched);
39
44
  const { matchedCount } = collection;
40
45
  if (result.type === 'UNMATCHED_SELECTOR_BUT_MAY_EMPTY') {
41
- ptLog('MATCHED_ZERO:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
46
+ if (ptEnabled) {
47
+ ptLog('MATCHED_ZERO:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
48
+ }
42
49
  return compereResult({
43
50
  type: 'MATCHED_ZERO',
44
51
  matched: collection.matched,
@@ -50,7 +57,9 @@ childNodes, rules, specs, options, depth, mode) {
50
57
  }
51
58
  if (max < collection.matchedCount) {
52
59
  collection.max(max);
53
- ptLog('UNEXPECTED_EXTRA_NODE:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
60
+ if (ptEnabled) {
61
+ ptLog('UNEXPECTED_EXTRA_NODE:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
62
+ }
54
63
  return compereResult({
55
64
  type: 'UNEXPECTED_EXTRA_NODE',
56
65
  matched: collection.matched,
@@ -64,7 +73,9 @@ childNodes, rules, specs, options, depth, mode) {
64
73
  if (result.type === 'MISSING_NODE_ONE_OR_MORE' ||
65
74
  result.type === 'MISSING_NODE_REQUIRED' ||
66
75
  result.type === 'TRANSPARENT_MODEL_DISALLOWS') {
67
- ptLog('%s(continued): %s; Needs', result.type, collection, result.query);
76
+ if (ptEnabled) {
77
+ ptLog('%s(continued): %s; Needs', result.type, collection, result.query);
78
+ }
68
79
  return compereResult({
69
80
  type: result.type,
70
81
  matched: collection.matched,
@@ -74,7 +85,9 @@ childNodes, rules, specs, options, depth, mode) {
74
85
  hint: result.hint,
75
86
  }, barelyResult);
76
87
  }
77
- ptLog('%s(continued): %s', prevResult.type, collection);
88
+ if (ptEnabled) {
89
+ ptLog('%s(continued): %s', prevResult.type, collection);
90
+ }
78
91
  return compereResult(prevResult, barelyResult);
79
92
  }
80
93
  if (added && collection.unmatched.length > 0) {
@@ -88,7 +101,9 @@ childNodes, rules, specs, options, depth, mode) {
88
101
  hint: result.hint,
89
102
  };
90
103
  }
91
- ptLog('continue⤴️');
104
+ if (ptEnabled) {
105
+ ptLog('continue⤴️');
106
+ }
92
107
  continue;
93
108
  }
94
109
  if (collection.matchedCount + (result.zeroMatch ? 1 : 0) < min) {
@@ -97,7 +112,9 @@ childNodes, rules, specs, options, depth, mode) {
97
112
  result.type === 'TRANSPARENT_MODEL_DISALLOWS'
98
113
  ? result.type
99
114
  : (missingType ?? 'MISSING_NODE_REQUIRED');
100
- ptLog('%s(in %s); Needs %s', resultType, missingType, result.query);
115
+ if (ptEnabled) {
116
+ ptLog('%s(in %s); Needs %s', resultType, missingType, result.query);
117
+ }
101
118
  return compereResult({
102
119
  type: resultType,
103
120
  matched: collection.matched,
@@ -129,10 +146,14 @@ childNodes, rules, specs, options, depth, mode) {
129
146
  };
130
147
  if (collection.unmatched.length > 0) {
131
148
  prevResult = matchedResult;
132
- ptLog('continue⤴️ (add prev)');
149
+ if (ptEnabled) {
150
+ ptLog('continue⤴️ (add prev)');
151
+ }
133
152
  continue;
134
153
  }
135
- ptLog('%s:\n model: %s\n max: %s\n collection: %s\n matched element: %s', resultType, modelLog(model, repeat), max, collection, matchedCount);
154
+ if (ptEnabled) {
155
+ ptLog('%s:\n model: %s\n max: %s\n collection: %s\n matched element: %s', resultType, modelLog(model, repeat), max, collection, matchedCount);
156
+ }
136
157
  // When min=0 and no elements were consumed, the pattern legitimately
137
158
  // matched zero times — don't propagate the inner sequence's failure.
138
159
  // This enables zeroOrMore([oneOrMore dt, oneOrMore dd]) to accept
@@ -168,7 +189,9 @@ function compereResult(
168
189
  a,
169
190
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
170
191
  b) {
171
- cLog('current: %s %O\nbarely: %s %O', a.type, a.hint, b?.type, b?.hint);
192
+ if (cLog.enabled) {
193
+ cLog('current: %s %O\nbarely: %s %O', a.type, a.hint, b?.type, b?.hint);
194
+ }
172
195
  if (b == null) {
173
196
  return a;
174
197
  }
@@ -5,6 +5,24 @@ import type { Log } from '../debug.js';
5
5
  * In the browser environment, color helpers are no-op stubs.
6
6
  */
7
7
  export declare const cmLog: Log;
8
+ /**
9
+ * Returns the `cmLog` child logger for `<name>#<depth>`, created once per
10
+ * name and depth. `debug`'s `extend()` allocates a logger on every call,
11
+ * which showed up in profiles when it ran per recursion step. Use this instead
12
+ * of calling `cmLog.extend()` for per-depth names.
13
+ *
14
+ * The cache is bounded by the number of distinct name × depth pairs. The returned
15
+ * logger is shared by every caller, so setting `.enabled` or `.log` on it affects
16
+ * all of them.
17
+ *
18
+ * Keep in sync with `debug.ts`: `package.json` swaps this file in for that one in
19
+ * the browser build.
20
+ *
21
+ * @param name - The logger's base name, e.g. `order`.
22
+ * @param depth - The recursion depth the logger belongs to.
23
+ * @returns The shared child logger for `<name>#<depth>`.
24
+ */
25
+ export declare function depthLog(name: string, depth: number): Log;
8
26
  /** No-op color stub for browser environments: green background. */
9
27
  export declare const bgGreen: {
10
28
  (): void;
@@ -5,6 +5,33 @@ import { log } from '../debug.js';
5
5
  * In the browser environment, color helpers are no-op stubs.
6
6
  */
7
7
  export const cmLog = log.extend('content-model');
8
+ const depthLogs = new Map();
9
+ /**
10
+ * Returns the `cmLog` child logger for `<name>#<depth>`, created once per
11
+ * name and depth. `debug`'s `extend()` allocates a logger on every call,
12
+ * which showed up in profiles when it ran per recursion step. Use this instead
13
+ * of calling `cmLog.extend()` for per-depth names.
14
+ *
15
+ * The cache is bounded by the number of distinct name × depth pairs. The returned
16
+ * logger is shared by every caller, so setting `.enabled` or `.log` on it affects
17
+ * all of them.
18
+ *
19
+ * Keep in sync with `debug.ts`: `package.json` swaps this file in for that one in
20
+ * the browser build.
21
+ *
22
+ * @param name - The logger's base name, e.g. `order`.
23
+ * @param depth - The recursion depth the logger belongs to.
24
+ * @returns The shared child logger for `<name>#<depth>`.
25
+ */
26
+ export function depthLog(name, depth) {
27
+ const key = `${name}#${depth}`;
28
+ let logger = depthLogs.get(key);
29
+ if (!logger) {
30
+ logger = cmLog.extend(key);
31
+ depthLogs.set(key, logger);
32
+ }
33
+ return logger;
34
+ }
8
35
  const fn = () => { };
9
36
  fn.bold = () => { };
10
37
  /** No-op color stub for browser environments: green background. */
@@ -6,6 +6,24 @@ import color from 'ansi-colors';
6
6
  * for structured debug output during content model evaluation.
7
7
  */
8
8
  export declare const cmLog: Log;
9
+ /**
10
+ * Returns the `cmLog` child logger for `<name>#<depth>`, created once per
11
+ * name and depth. `debug`'s `extend()` allocates a logger on every call,
12
+ * which showed up in profiles when it ran per recursion step. Use this instead
13
+ * of calling `cmLog.extend()` for per-depth names.
14
+ *
15
+ * The cache is bounded by the number of distinct name × depth pairs. The returned
16
+ * logger is shared by every caller, so setting `.enabled` or `.log` on it affects
17
+ * all of them.
18
+ *
19
+ * Keep in sync with `debug.browser.ts`: `package.json` swaps that file in for this
20
+ * one in the browser build.
21
+ *
22
+ * @param name - The logger's base name, e.g. `order`.
23
+ * @param depth - The recursion depth the logger belongs to.
24
+ * @returns The shared child logger for `<name>#<depth>`.
25
+ */
26
+ export declare function depthLog(name: string, depth: number): Log;
9
27
  /** ANSI color helper: green background for highlighting locked-and-matched nodes in debug output. */
10
28
  export declare const bgGreen: color.StyleFunction;
11
29
  /** ANSI color helper: green foreground for highlighting matched nodes in debug output. */
@@ -6,6 +6,33 @@ import { log } from '../debug.js';
6
6
  * for structured debug output during content model evaluation.
7
7
  */
8
8
  export const cmLog = log.extend('content-model');
9
+ const depthLogs = new Map();
10
+ /**
11
+ * Returns the `cmLog` child logger for `<name>#<depth>`, created once per
12
+ * name and depth. `debug`'s `extend()` allocates a logger on every call,
13
+ * which showed up in profiles when it ran per recursion step. Use this instead
14
+ * of calling `cmLog.extend()` for per-depth names.
15
+ *
16
+ * The cache is bounded by the number of distinct name × depth pairs. The returned
17
+ * logger is shared by every caller, so setting `.enabled` or `.log` on it affects
18
+ * all of them.
19
+ *
20
+ * Keep in sync with `debug.browser.ts`: `package.json` swaps that file in for this
21
+ * one in the browser build.
22
+ *
23
+ * @param name - The logger's base name, e.g. `order`.
24
+ * @param depth - The recursion depth the logger belongs to.
25
+ * @returns The shared child logger for `<name>#<depth>`.
26
+ */
27
+ export function depthLog(name, depth) {
28
+ const key = `${name}#${depth}`;
29
+ let logger = depthLogs.get(key);
30
+ if (!logger) {
31
+ logger = cmLog.extend(key);
32
+ depthLogs.set(key, logger);
33
+ }
34
+ return logger;
35
+ }
9
36
  /** ANSI color helper: green background for highlighting locked-and-matched nodes in debug output. */
10
37
  export const bgGreen = color.bgGreen;
11
38
  /** ANSI color helper: green foreground for highlighting matched nodes in debug output. */
@@ -1,5 +1,5 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
- import { contentModel } from './content-model.js';
2
+ import { contentModel, hasTagRule } from './content-model.js';
3
3
  import meta from './meta.js';
4
4
  import { transparentMode } from './represent-transparent-nodes.js';
5
5
  import { getContentOwner, getReportScope, isContentMutable } from './slot-content.js';
@@ -18,7 +18,7 @@ export default createRule({
18
18
  // that user-defined custom-tag rules are not silently bypassed by pretending
19
19
  // (see issue #3739).
20
20
  const modes = ['pretended'];
21
- if (el.pretenderContext?.type === 'pretender' && el.rule.value.some(r => r.tag === el.rawName)) {
21
+ if (el.pretenderContext?.type === 'pretender' && el.rule.value.some(r => hasTagRule(el, r))) {
22
22
  modes.push('origin');
23
23
  }
24
24
  for (const mode of modes) {
@@ -1,5 +1,5 @@
1
1
  import { contentModelCategoryToTagNames } from '@markuplint/ml-spec';
2
- import { cmLog } from './debug.js';
2
+ import { depthLog } from './debug.js';
3
3
  import { cleanObject, matches } from './utils.js';
4
4
  /**
5
5
  * Tests whether a single child node matches a content model query selector.
@@ -18,11 +18,14 @@ import { cleanObject, matches } from './utils.js';
18
18
  export function matchesSelector(query,
19
19
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
20
20
  childNode, specs, depth, mode) {
21
- const nodeLog = cmLog.extend(`node#${depth}`);
21
+ const nodeLog = depthLog('node', depth);
22
+ const nodeEnabled = nodeLog.enabled;
22
23
  const { selector, hasText, hasNonEmptyText, hasCustom } = optCondition(query, specs);
23
24
  if (childNode == null) {
24
25
  if (hasText) {
25
- nodeLog('<#text>.matches(%s) => ""', query);
26
+ if (nodeEnabled) {
27
+ nodeLog('<#text>.matches(%s) => ""', query);
28
+ }
26
29
  return {
27
30
  type: 'MATCHED_ZERO',
28
31
  matched: [],
@@ -45,7 +48,9 @@ childNode, specs, depth, mode) {
45
48
  if (childNode.is(childNode.TEXT_NODE)) {
46
49
  if (hasNonEmptyText) {
47
50
  if (childNode.isWhitespace()) {
48
- nodeLog('<#nonEmptyText>.matches(%s) => WHITESPACE (rejected)', query);
51
+ if (nodeEnabled) {
52
+ nodeLog('<#nonEmptyText>.matches(%s) => WHITESPACE (rejected)', query);
53
+ }
49
54
  return {
50
55
  type: 'MISSING_NODE',
51
56
  matched: [],
@@ -55,7 +60,9 @@ childNode, specs, depth, mode) {
55
60
  hint: {},
56
61
  };
57
62
  }
58
- nodeLog('<#nonEmptyText>.matches(%s) => "%s"', query, childNode.raw.trim());
63
+ if (nodeEnabled) {
64
+ nodeLog('<#nonEmptyText>.matches(%s) => "%s"', query, childNode.raw.trim());
65
+ }
59
66
  return {
60
67
  type: 'MATCHED',
61
68
  matched: [childNode],
@@ -66,7 +73,9 @@ childNode, specs, depth, mode) {
66
73
  };
67
74
  }
68
75
  if (hasText) {
69
- nodeLog('<#text>.matches(%s) => "%s"', query, childNode.raw.trim());
76
+ if (nodeEnabled) {
77
+ nodeLog('<#text>.matches(%s) => "%s"', query, childNode.raw.trim());
78
+ }
70
79
  return {
71
80
  type: 'MATCHED',
72
81
  matched: [childNode],
@@ -77,7 +86,9 @@ childNode, specs, depth, mode) {
77
86
  };
78
87
  }
79
88
  if (childNode.isWhitespace()) {
80
- nodeLog('<#text>.matches(%s) => WHITESPACE', query);
89
+ if (nodeEnabled) {
90
+ nodeLog('<#text>.matches(%s) => WHITESPACE', query);
91
+ }
81
92
  return {
82
93
  type: 'MATCHED',
83
94
  matched: [childNode],
@@ -98,7 +109,9 @@ childNode, specs, depth, mode) {
98
109
  };
99
110
  }
100
111
  if (childNode.is(childNode.MARKUPLINT_PREPROCESSOR_BLOCK)) {
101
- nodeLog('%s.matches(%s) => PBlock', childNode.raw, query);
112
+ if (nodeEnabled) {
113
+ nodeLog('%s.matches(%s) => PBlock', childNode.raw, query);
114
+ }
102
115
  return {
103
116
  type: 'MATCHED',
104
117
  matched: [childNode],
@@ -110,7 +123,9 @@ childNode, specs, depth, mode) {
110
123
  }
111
124
  if (childNode.is(childNode.ELEMENT_NODE)) {
112
125
  if (hasCustom && isTreatedAsCustom(childNode, mode)) {
113
- nodeLog('%s.matches(%s) => CustomElement', childNode.raw, query);
126
+ if (nodeEnabled) {
127
+ nodeLog('%s.matches(%s) => CustomElement', childNode.raw, query);
128
+ }
114
129
  return {
115
130
  type: 'MATCHED',
116
131
  matched: [childNode],
@@ -121,7 +136,9 @@ childNode, specs, depth, mode) {
121
136
  };
122
137
  }
123
138
  const result = matches(selector, childNode, specs, mode);
124
- nodeLog('%s.matches(%s) => %s', childNode.raw, query, result.matched);
139
+ if (nodeEnabled) {
140
+ nodeLog('%s.matches(%s) => %s', childNode.raw, query, result.matched);
141
+ }
125
142
  if (result.matched) {
126
143
  return {
127
144
  type: 'MATCHED',
@@ -172,25 +189,30 @@ childNode, specs, depth, mode) {
172
189
  * pretended, so a component that stands for `<section>` would still look
173
190
  * authored and slip through `<p>`, `<span>` and `<button>`. In `'pretended'`
174
191
  * mode such an element is judged by what it stands for instead, exactly as if
175
- * the user had written that element. The pretender target is always created
176
- * with `elementType: 'html'`, so a target that is itself a custom element
177
- * (its name contains a hyphen) is recognised by name.
192
+ * the user had written that element, and that comes first: a parser that cannot
193
+ * tell a component from an element (HTML sees `<MyTabs>` as `html`) still gives a
194
+ * pretender. The pretender target is created with `elementType: 'html'`, so a
195
+ * target that is itself a custom element (its name contains a hyphen) is
196
+ * recognised by name. A target that is a component name (`Tabs.Root`, where
197
+ * the chain of `pretenders.auto` stopped) is `'authored'`, and what that
198
+ * component renders is not known, so it is accepted like one written directly.
178
199
  *
179
200
  * In `'origin'` mode the pretender context is suppressed while matching (see
180
201
  * {@link matches}) and the element is seen under its authored name, so it stays
181
202
  * accepted as a custom element.
203
+ *
204
+ * The `'pretended'` criterion is the one `MLElement.isCustomElement` gives a
205
+ * `:model()` selector; change them together.
182
206
  */
183
207
  function isTreatedAsCustom(
184
208
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
185
209
  element, mode) {
186
- if (element.elementType === 'html') {
187
- return false;
188
- }
189
210
  const pretenderContext = element.pretenderContext;
190
211
  if (mode === 'pretended' && pretenderContext?.type === 'pretender') {
191
- return pretenderContext.as.localName.includes('-');
212
+ const { as } = pretenderContext;
213
+ return as.elementType === 'authored' || as.localName.includes('-');
192
214
  }
193
- return true;
215
+ return element.elementType !== 'html';
194
216
  }
195
217
  /**
196
218
  * Pre-computed conditions for well-known queries that do not depend on specs.
@@ -1,6 +1,6 @@
1
1
  import { deepCopy } from '../helpers.js';
2
2
  import { complexBranch } from './complex-branch.js';
3
- import { cmLog } from './debug.js';
3
+ import { depthLog } from './debug.js';
4
4
  import { Collection, mergeHints, modelLog } from './utils.js';
5
5
  /**
6
6
  * Validates an ordered sequence of content model patterns against a list of child nodes.
@@ -29,12 +29,16 @@ import { Collection, mergeHints, modelLog } from './utils.js';
29
29
  export function order(contents,
30
30
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
31
31
  childNodes, rules, specs, options, depth, mode) {
32
- const orderLog = cmLog.extend(`order#${depth}`);
33
- const btLog = cmLog.extend(`backtrack#${depth}`);
32
+ const orderLog = depthLog('order', depth);
33
+ const btLog = depthLog('backtrack', depth);
34
+ const orderEnabled = orderLog.enabled;
35
+ const btEnabled = btLog.enabled;
34
36
  const patterns = deepCopy(contents);
35
37
  const collection = new Collection(childNodes);
36
- orderLog('Model:\n RegEx: %s\n Schema: %o', modelLog(patterns, ''), patterns);
37
- orderLog('Starts: %s', collection);
38
+ if (orderEnabled) {
39
+ orderLog('Model:\n RegEx: %s\n Schema: %o', modelLog(patterns, ''), patterns);
40
+ orderLog('Starts: %s', collection);
41
+ }
38
42
  let result = undefined;
39
43
  let backtrackMode = false;
40
44
  let afterBacktrack = false;
@@ -46,7 +50,9 @@ childNodes, rules, specs, options, depth, mode) {
46
50
  unmatchedResults.push(result);
47
51
  if (backtrackMode) {
48
52
  collection.back();
49
- btLog('🔙◀BACK');
53
+ if (btEnabled) {
54
+ btLog('🔙◀BACK');
55
+ }
50
56
  backtrackMode = false;
51
57
  afterBacktrack = true;
52
58
  continue;
@@ -55,9 +61,11 @@ childNodes, rules, specs, options, depth, mode) {
55
61
  if (!barelyMatchedResult) {
56
62
  throw new Error('Unreachable code');
57
63
  }
58
- orderLog('Result (%s): %s%s', result.type, collection.toString(true), barelyMatchedResult.hint.missing?.barelyMatchedElements == null
59
- ? ''
60
- : `; But ${barelyMatchedResult.hint.missing.barelyMatchedElements} elements hit out of pattern`);
64
+ if (orderEnabled) {
65
+ orderLog('Result (%s): %s%s', result.type, collection.toString(true), barelyMatchedResult.hint.missing?.barelyMatchedElements == null
66
+ ? ''
67
+ : `; But ${barelyMatchedResult.hint.missing.barelyMatchedElements} elements hit out of pattern`);
68
+ }
61
69
  return {
62
70
  type: barelyMatchedResult.type,
63
71
  matched: collection.matched,
@@ -85,7 +93,9 @@ childNodes, rules, specs, options, depth, mode) {
85
93
  patterns.shift();
86
94
  }
87
95
  if (collection.unmatched.length > 0) {
88
- orderLog('Result (UNEXPECTED_EXTRA_NODE): %s', collection.toString(true));
96
+ if (orderEnabled) {
97
+ orderLog('Result (UNEXPECTED_EXTRA_NODE): %s', collection.toString(true));
98
+ }
89
99
  return {
90
100
  type: 'UNEXPECTED_EXTRA_NODE',
91
101
  matched: collection.matched,
@@ -96,7 +106,9 @@ childNodes, rules, specs, options, depth, mode) {
96
106
  };
97
107
  }
98
108
  const resultType = collection.matched.length > 0 ? 'MATCHED' : 'MATCHED_ZERO';
99
- orderLog('Result (%s): %s', resultType, collection.toString(true));
109
+ if (orderEnabled) {
110
+ orderLog('Result (%s): %s', resultType, collection.toString(true));
111
+ }
100
112
  return {
101
113
  type: resultType,
102
114
  matched: collection.matched,
@@ -195,7 +195,9 @@ childNodes, rules, specs, options, mode) {
195
195
  // Cap exceeded (#3895): fall back to the conservative
196
196
  // over-approximation documented on MAX_PATTERNS. False negatives
197
197
  // are possible beyond this point, so leave a debug trace.
198
- cmLog('Transparent pattern cap exceeded on <%s> (%d patterns x %d branches > %d): merging all branch children into every pattern; false negatives are possible', childNode.nodeName, patterns.length, branchGroups.length, MAX_PATTERNS);
198
+ if (cmLog.enabled) {
199
+ cmLog('Transparent pattern cap exceeded on <%s> (%d patterns x %d branches > %d): merging all branch children into every pattern; false negatives are possible', childNode.nodeName, patterns.length, branchGroups.length, MAX_PATTERNS);
200
+ }
199
201
  const allChildren = branchGroups.flat();
200
202
  for (const p of patterns) {
201
203
  p.push(childNode, ...allChildren);
@@ -16,7 +16,9 @@ export function transparent(
16
16
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
17
17
  childNodes) {
18
18
  if (childNodes.length === 0 || childNodes[0]?.parentElement?.parentElement) {
19
- transparentLog('Skipped');
19
+ if (transparentLog.enabled) {
20
+ transparentLog('Skipped');
21
+ }
20
22
  return {
21
23
  type: childNodes.length === 0 ? 'MATCHED_ZERO' : 'MATCHED',
22
24
  matched: [...childNodes],
@@ -26,7 +28,9 @@ childNodes) {
26
28
  hint: {},
27
29
  };
28
30
  }
29
- transparentLog('Transparent model element is component root');
31
+ if (transparentLog.enabled) {
32
+ transparentLog('Transparent model element is component root');
33
+ }
30
34
  return {
31
35
  type: 'MATCHED',
32
36
  matched: [...childNodes],
@@ -5,9 +5,9 @@
5
5
  * > multiple attribute specified, and has a display size of 1, then the
6
6
  * > select element must have a placeholder label option.
7
7
  *
8
- * `hasPlaceholderLabelOption` decides whether an existing option already
9
- * satisfies the requirement; `needPlaceholderLabelOption` gates whether the
10
- * requirement applies to this `<select>`.
8
+ * `needPlaceholderLabelOption` gates whether the requirement applies to this
9
+ * `<select>`; `hasPlaceholderLabelOption` then decides whether an existing
10
+ * option already satisfies it.
11
11
  *
12
12
  * @see https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
13
13
  */