@markuplint/rules 4.11.2 → 5.0.0-alpha.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 (266) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/SKILL.md +76 -0
  3. package/lib/attr-check.d.ts +46 -7
  4. package/lib/attr-check.js +42 -8
  5. package/lib/attr-duplication/index.d.ts +7 -0
  6. package/lib/attr-duplication/index.js +7 -0
  7. package/lib/attr-duplication/meta.d.ts +1 -0
  8. package/lib/attr-duplication/meta.js +1 -0
  9. package/lib/attr-value-quotes/index.d.ts +10 -0
  10. package/lib/attr-value-quotes/index.js +10 -0
  11. package/lib/attr-value-quotes/meta.d.ts +1 -0
  12. package/lib/attr-value-quotes/meta.js +1 -0
  13. package/lib/case-sensitive-attr-name/index.d.ts +9 -0
  14. package/lib/case-sensitive-attr-name/index.js +8 -0
  15. package/lib/case-sensitive-attr-name/meta.d.ts +1 -0
  16. package/lib/case-sensitive-attr-name/meta.js +1 -0
  17. package/lib/case-sensitive-tag-name/index.d.ts +9 -0
  18. package/lib/case-sensitive-tag-name/index.js +8 -0
  19. package/lib/case-sensitive-tag-name/meta.d.ts +1 -0
  20. package/lib/case-sensitive-tag-name/meta.js +1 -0
  21. package/lib/character-reference/index.d.ts +9 -0
  22. package/lib/character-reference/index.js +20 -0
  23. package/lib/character-reference/meta.d.ts +1 -0
  24. package/lib/character-reference/meta.js +1 -0
  25. package/lib/class-naming/index.d.ts +9 -0
  26. package/lib/class-naming/index.js +8 -0
  27. package/lib/class-naming/meta.d.ts +1 -0
  28. package/lib/class-naming/meta.js +1 -0
  29. package/lib/correct-aspect-ratio/index.d.ts +5 -0
  30. package/lib/correct-aspect-ratio/index.js +80 -0
  31. package/lib/correct-aspect-ratio/meta.d.ts +4 -0
  32. package/lib/correct-aspect-ratio/meta.js +3 -0
  33. package/lib/correct-aspect-ratio/resolve-image-size.d.ts +24 -0
  34. package/lib/correct-aspect-ratio/resolve-image-size.js +165 -0
  35. package/lib/create-message.d.ts +29 -2
  36. package/lib/create-message.js +60 -2
  37. package/lib/debug.d.ts +9 -0
  38. package/lib/debug.js +5 -0
  39. package/lib/deprecated-attr/index.d.ts +6 -0
  40. package/lib/deprecated-attr/index.js +6 -0
  41. package/lib/deprecated-attr/meta.d.ts +1 -0
  42. package/lib/deprecated-attr/meta.js +1 -0
  43. package/lib/deprecated-element/index.d.ts +6 -0
  44. package/lib/deprecated-element/index.js +8 -2
  45. package/lib/deprecated-element/meta.d.ts +1 -0
  46. package/lib/deprecated-element/meta.js +1 -0
  47. package/lib/disallowed-element/index.d.ts +7 -0
  48. package/lib/disallowed-element/index.js +7 -0
  49. package/lib/disallowed-element/meta.d.ts +1 -0
  50. package/lib/disallowed-element/meta.js +1 -0
  51. package/lib/doctype/index.d.ts +10 -0
  52. package/lib/doctype/index.js +8 -0
  53. package/lib/doctype/meta.d.ts +1 -0
  54. package/lib/doctype/meta.js +1 -0
  55. package/lib/end-tag/index.d.ts +7 -0
  56. package/lib/end-tag/index.js +8 -1
  57. package/lib/end-tag/meta.d.ts +1 -0
  58. package/lib/end-tag/meta.js +1 -0
  59. package/lib/heading-levels/index.d.ts +6 -0
  60. package/lib/heading-levels/index.js +6 -1
  61. package/lib/heading-levels/meta.d.ts +1 -0
  62. package/lib/heading-levels/meta.js +1 -0
  63. package/lib/helpers.d.ts +105 -0
  64. package/lib/helpers.js +112 -16
  65. package/lib/id-duplication/index.d.ts +7 -0
  66. package/lib/id-duplication/index.js +7 -0
  67. package/lib/id-duplication/meta.d.ts +1 -0
  68. package/lib/id-duplication/meta.js +1 -0
  69. package/lib/index.d.ts +43 -41
  70. package/lib/index.js +24 -0
  71. package/lib/ineffective-attr/index.d.ts +7 -0
  72. package/lib/ineffective-attr/index.js +7 -0
  73. package/lib/ineffective-attr/meta.d.ts +1 -0
  74. package/lib/ineffective-attr/meta.js +1 -0
  75. package/lib/invalid-attr/index.d.ts +29 -21
  76. package/lib/invalid-attr/index.js +60 -109
  77. package/lib/invalid-attr/meta.d.ts +1 -0
  78. package/lib/invalid-attr/meta.js +1 -0
  79. package/lib/label-has-control/index.d.ts +7 -0
  80. package/lib/label-has-control/index.js +11 -0
  81. package/lib/label-has-control/meta.d.ts +1 -0
  82. package/lib/label-has-control/meta.js +1 -0
  83. package/lib/landmark-roles/index.d.ts +14 -0
  84. package/lib/landmark-roles/index.js +21 -0
  85. package/lib/landmark-roles/meta.d.ts +1 -0
  86. package/lib/landmark-roles/meta.js +1 -0
  87. package/lib/link-types/index.d.ts +10 -0
  88. package/lib/link-types/index.js +180 -0
  89. package/lib/link-types/meta.d.ts +5 -0
  90. package/lib/link-types/meta.js +4 -0
  91. package/lib/neighbor-popovers/index.d.ts +8 -0
  92. package/lib/neighbor-popovers/index.js +28 -6
  93. package/lib/neighbor-popovers/meta.d.ts +1 -0
  94. package/lib/neighbor-popovers/meta.js +1 -0
  95. package/lib/no-ambiguous-navigable-target-names/index.d.ts +8 -0
  96. package/lib/no-ambiguous-navigable-target-names/index.js +8 -0
  97. package/lib/no-ambiguous-navigable-target-names/meta.d.ts +1 -0
  98. package/lib/no-ambiguous-navigable-target-names/meta.js +1 -0
  99. package/lib/no-boolean-attr-value/index.d.ts +7 -0
  100. package/lib/no-boolean-attr-value/index.js +7 -0
  101. package/lib/no-boolean-attr-value/meta.d.ts +1 -0
  102. package/lib/no-boolean-attr-value/meta.js +1 -0
  103. package/lib/no-consecutive-br/index.d.ts +8 -0
  104. package/lib/no-consecutive-br/index.js +8 -0
  105. package/lib/no-consecutive-br/meta.d.ts +1 -0
  106. package/lib/no-consecutive-br/meta.js +1 -0
  107. package/lib/no-default-value/index.d.ts +7 -0
  108. package/lib/no-default-value/index.js +7 -0
  109. package/lib/no-default-value/meta.d.ts +1 -0
  110. package/lib/no-default-value/meta.js +1 -0
  111. package/lib/no-duplicate-dt/index.d.ts +6 -0
  112. package/lib/no-duplicate-dt/index.js +6 -0
  113. package/lib/no-duplicate-dt/meta.d.ts +1 -0
  114. package/lib/no-duplicate-dt/meta.js +1 -0
  115. package/lib/no-empty-palpable-content/index.d.ts +13 -0
  116. package/lib/no-empty-palpable-content/index.js +12 -0
  117. package/lib/no-empty-palpable-content/meta.d.ts +1 -0
  118. package/lib/no-empty-palpable-content/meta.js +1 -0
  119. package/lib/no-hard-code-id/index.d.ts +8 -0
  120. package/lib/no-hard-code-id/index.js +8 -0
  121. package/lib/no-hard-code-id/meta.d.ts +1 -0
  122. package/lib/no-hard-code-id/meta.js +1 -0
  123. package/lib/no-orphaned-end-tag/index.d.ts +7 -0
  124. package/lib/no-orphaned-end-tag/index.js +8 -2
  125. package/lib/no-orphaned-end-tag/meta.d.ts +1 -0
  126. package/lib/no-orphaned-end-tag/meta.js +1 -0
  127. package/lib/no-refer-to-non-existent-id/index.d.ts +9 -1
  128. package/lib/no-refer-to-non-existent-id/index.js +11 -2
  129. package/lib/no-refer-to-non-existent-id/meta.d.ts +1 -0
  130. package/lib/no-refer-to-non-existent-id/meta.js +1 -0
  131. package/lib/no-unsupported-features/compat-data.d.ts +71 -0
  132. package/lib/no-unsupported-features/compat-data.js +204 -0
  133. package/lib/no-unsupported-features/index.d.ts +24 -0
  134. package/lib/no-unsupported-features/index.js +139 -0
  135. package/lib/no-unsupported-features/meta.d.ts +5 -0
  136. package/lib/no-unsupported-features/meta.js +4 -0
  137. package/lib/no-unsupported-features/resolve-browsers.d.ts +30 -0
  138. package/lib/no-unsupported-features/resolve-browsers.js +125 -0
  139. package/lib/no-use-event-handler-attr/index.d.ts +10 -0
  140. package/lib/no-use-event-handler-attr/index.js +8 -0
  141. package/lib/no-use-event-handler-attr/meta.d.ts +1 -0
  142. package/lib/no-use-event-handler-attr/meta.js +1 -0
  143. package/lib/permitted-contents/choice.d.ts +17 -0
  144. package/lib/permitted-contents/choice.js +31 -1
  145. package/lib/permitted-contents/complex-branch.d.ts +11 -7
  146. package/lib/permitted-contents/complex-branch.js +11 -7
  147. package/lib/permitted-contents/content-model.d.ts +11 -0
  148. package/lib/permitted-contents/content-model.js +32 -0
  149. package/lib/permitted-contents/count-pattern.d.ts +13 -7
  150. package/lib/permitted-contents/count-pattern.js +24 -8
  151. package/lib/permitted-contents/debug.browser.d.ts +12 -0
  152. package/lib/permitted-contents/debug.browser.js +12 -0
  153. package/lib/permitted-contents/debug.d.ts +12 -0
  154. package/lib/permitted-contents/debug.js +12 -0
  155. package/lib/permitted-contents/index.d.ts +12 -0
  156. package/lib/permitted-contents/index.js +21 -0
  157. package/lib/permitted-contents/matches-selector.d.ts +19 -0
  158. package/lib/permitted-contents/matches-selector.js +33 -0
  159. package/lib/permitted-contents/meta.d.ts +1 -0
  160. package/lib/permitted-contents/meta.js +1 -0
  161. package/lib/permitted-contents/order.d.ts +14 -7
  162. package/lib/permitted-contents/order.js +15 -8
  163. package/lib/permitted-contents/recursive-branch.d.ts +17 -0
  164. package/lib/permitted-contents/recursive-branch.js +17 -0
  165. package/lib/permitted-contents/represent-transparent-nodes.d.ts +54 -0
  166. package/lib/permitted-contents/represent-transparent-nodes.js +119 -12
  167. package/lib/permitted-contents/start.d.ts +12 -6
  168. package/lib/permitted-contents/start.js +12 -6
  169. package/lib/permitted-contents/transparent.d.ts +12 -0
  170. package/lib/permitted-contents/transparent.js +12 -0
  171. package/lib/permitted-contents/types.d.ts +67 -0
  172. package/lib/permitted-contents/utils.d.ts +160 -0
  173. package/lib/permitted-contents/utils.js +216 -35
  174. package/lib/placeholder-label-option/index.d.ts +7 -0
  175. package/lib/placeholder-label-option/index.js +19 -15
  176. package/lib/placeholder-label-option/meta.d.ts +1 -0
  177. package/lib/placeholder-label-option/meta.js +1 -0
  178. package/lib/redundant-accessible-name/index.d.ts +10 -0
  179. package/lib/redundant-accessible-name/index.js +123 -0
  180. package/lib/redundant-accessible-name/meta.d.ts +5 -0
  181. package/lib/redundant-accessible-name/meta.js +4 -0
  182. package/lib/require-accessible-name/index.d.ts +10 -4
  183. package/lib/require-accessible-name/index.js +13 -5
  184. package/lib/require-accessible-name/meta.d.ts +1 -0
  185. package/lib/require-accessible-name/meta.js +1 -0
  186. package/lib/require-datetime/index.d.ts +12 -0
  187. package/lib/require-datetime/index.js +8 -0
  188. package/lib/require-datetime/meta.d.ts +1 -0
  189. package/lib/require-datetime/meta.js +1 -0
  190. package/lib/require-datetime/types.d.ts +7 -0
  191. package/lib/require-datetime/utils.d.ts +18 -5
  192. package/lib/require-datetime/utils.js +53 -9
  193. package/lib/require-dialog-autofocus/index.d.ts +22 -0
  194. package/lib/require-dialog-autofocus/index.js +62 -0
  195. package/lib/require-dialog-autofocus/meta.d.ts +5 -0
  196. package/lib/require-dialog-autofocus/meta.js +4 -0
  197. package/lib/required-attr/index.d.ts +21 -1
  198. package/lib/required-attr/index.js +34 -4
  199. package/lib/required-attr/meta.d.ts +1 -0
  200. package/lib/required-attr/meta.js +1 -0
  201. package/lib/required-element/index.d.ts +14 -0
  202. package/lib/required-element/index.js +14 -2
  203. package/lib/required-element/meta.d.ts +1 -0
  204. package/lib/required-element/meta.js +1 -0
  205. package/lib/required-h1/index.d.ts +12 -0
  206. package/lib/required-h1/index.js +7 -0
  207. package/lib/required-h1/meta.d.ts +1 -0
  208. package/lib/required-h1/meta.js +1 -0
  209. package/lib/srcset-sizes-constraint/index.d.ts +6 -0
  210. package/lib/srcset-sizes-constraint/index.js +108 -0
  211. package/lib/srcset-sizes-constraint/meta.d.ts +5 -0
  212. package/lib/srcset-sizes-constraint/meta.js +4 -0
  213. package/lib/srcset-sizes-constraint/parse-srcset.d.ts +40 -0
  214. package/lib/srcset-sizes-constraint/parse-srcset.js +56 -0
  215. package/lib/table-row-column-alignment/find-children.d.ts +10 -0
  216. package/lib/table-row-column-alignment/find-children.js +11 -0
  217. package/lib/table-row-column-alignment/grid.d.ts +60 -0
  218. package/lib/table-row-column-alignment/grid.js +104 -26
  219. package/lib/table-row-column-alignment/index.d.ts +8 -0
  220. package/lib/table-row-column-alignment/index.js +8 -0
  221. package/lib/table-row-column-alignment/meta.d.ts +1 -0
  222. package/lib/table-row-column-alignment/meta.js +1 -0
  223. package/lib/table-row-column-alignment/types.d.ts +10 -0
  224. package/lib/use-list/index.d.ts +16 -0
  225. package/lib/use-list/index.js +20 -0
  226. package/lib/use-list/meta.d.ts +1 -0
  227. package/lib/use-list/meta.js +1 -0
  228. package/lib/wai-aria/checkings/abstract-role.d.ts +9 -0
  229. package/lib/wai-aria/checkings/abstract-role.js +14 -2
  230. package/lib/wai-aria/checkings/default-value.d.ts +11 -0
  231. package/lib/wai-aria/checkings/default-value.js +11 -0
  232. package/lib/wai-aria/checkings/deprecated-props.d.ts +11 -0
  233. package/lib/wai-aria/checkings/deprecated-props.js +11 -0
  234. package/lib/wai-aria/checkings/deprecated-role.d.ts +10 -0
  235. package/lib/wai-aria/checkings/deprecated-role.js +10 -0
  236. package/lib/wai-aria/checkings/disallowed-prop.d.ts +14 -0
  237. package/lib/wai-aria/checkings/disallowed-prop.js +19 -2
  238. package/lib/wai-aria/checkings/implicit-props.d.ts +13 -0
  239. package/lib/wai-aria/checkings/implicit-props.js +13 -0
  240. package/lib/wai-aria/checkings/implicit-role.d.ts +9 -0
  241. package/lib/wai-aria/checkings/implicit-role.js +14 -2
  242. package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +9 -0
  243. package/lib/wai-aria/checkings/interaction-in-hidden.js +15 -4
  244. package/lib/wai-aria/checkings/no-global-prop.d.ts +10 -0
  245. package/lib/wai-aria/checkings/no-global-prop.js +10 -0
  246. package/lib/wai-aria/checkings/non-existent-role.d.ts +11 -0
  247. package/lib/wai-aria/checkings/non-existent-role.js +19 -2
  248. package/lib/wai-aria/checkings/permitted-roles.d.ts +10 -0
  249. package/lib/wai-aria/checkings/permitted-roles.js +15 -2
  250. package/lib/wai-aria/checkings/presentational-children.d.ts +7 -1
  251. package/lib/wai-aria/checkings/presentational-children.js +17 -3
  252. package/lib/wai-aria/checkings/required-accessibility-parent-role.d.ts +25 -0
  253. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +56 -0
  254. package/lib/wai-aria/checkings/required-owned-elements.d.ts +10 -1
  255. package/lib/wai-aria/checkings/required-owned-elements.js +69 -25
  256. package/lib/wai-aria/checkings/required-prop.d.ts +13 -0
  257. package/lib/wai-aria/checkings/required-prop.js +18 -2
  258. package/lib/wai-aria/checkings/value.d.ts +23 -0
  259. package/lib/wai-aria/checkings/value.js +32 -0
  260. package/lib/wai-aria/index.d.ts +10 -0
  261. package/lib/wai-aria/index.js +22 -4
  262. package/lib/wai-aria/meta.d.ts +1 -0
  263. package/lib/wai-aria/meta.js +1 -0
  264. package/lib/wai-aria/types.d.ts +25 -1
  265. package/package.json +16 -11
  266. package/schema.json +18 -0
@@ -0,0 +1,80 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ import { getImageDimensions } from './resolve-image-size.js';
4
+ export default createRule({
5
+ meta: meta,
6
+ defaultSeverity: 'warning',
7
+ defaultOptions: {},
8
+ async verify({ document, report }) {
9
+ await document.walkOn('Element', async (el) => {
10
+ const localName = el.localName;
11
+ // Only check img and source (inside picture) elements
12
+ if (localName !== 'img' && localName !== 'source') {
13
+ return;
14
+ }
15
+ // source is only relevant inside <picture>
16
+ if (localName === 'source' && el.parentElement?.localName !== 'picture') {
17
+ return;
18
+ }
19
+ if (el.hasSpreadAttr) {
20
+ return;
21
+ }
22
+ const widthAttr = el.getAttributeNode('width');
23
+ const heightAttr = el.getAttributeNode('height');
24
+ if (!widthAttr || !heightAttr) {
25
+ return;
26
+ }
27
+ if (widthAttr.isDynamicValue || heightAttr.isDynamicValue) {
28
+ return;
29
+ }
30
+ // Resolve the image URL from src (img) or srcset (source)
31
+ let src;
32
+ if (localName === 'img') {
33
+ const srcAttr = el.getAttributeNode('src');
34
+ if (!srcAttr || srcAttr.isDynamicValue) {
35
+ return;
36
+ }
37
+ src = srcAttr.value;
38
+ }
39
+ else {
40
+ const srcsetAttr = el.getAttributeNode('srcset');
41
+ if (!srcsetAttr || srcsetAttr.isDynamicValue) {
42
+ return;
43
+ }
44
+ const extracted = extractFirstSrcsetUrl(srcsetAttr.value);
45
+ if (!extracted) {
46
+ return;
47
+ }
48
+ src = extracted;
49
+ }
50
+ const widthStr = widthAttr.value;
51
+ const heightStr = heightAttr.value;
52
+ // Parse width/height as integers; skip if non-numeric
53
+ const attrWidth = Number.parseInt(widthStr, 10);
54
+ const attrHeight = Number.parseInt(heightStr, 10);
55
+ if (!Number.isFinite(attrWidth) || !Number.isFinite(attrHeight) || attrWidth <= 0 || attrHeight <= 0) {
56
+ return;
57
+ }
58
+ const documentRoot = el.rule.options?.documentRoot;
59
+ const dimensions = await getImageDimensions(src, documentRoot, document.filename);
60
+ if (!dimensions) {
61
+ return;
62
+ }
63
+ const { width: actualWidth, height: actualHeight } = dimensions;
64
+ // Cross-multiplication comparison to avoid floating-point errors
65
+ if (attrWidth * actualHeight !== attrHeight * actualWidth) {
66
+ report({
67
+ scope: el,
68
+ message: `The aspect ratio of the image (${actualWidth}:${actualHeight}) does not match the width/height attributes (${widthStr}:${heightStr})`,
69
+ });
70
+ }
71
+ });
72
+ },
73
+ });
74
+ function extractFirstSrcsetUrl(srcset) {
75
+ const first = srcset.trim().split(',')[0]?.trim();
76
+ if (!first) {
77
+ return null;
78
+ }
79
+ return first.split(/\s+/)[0] ?? null;
80
+ }
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ readonly category: "validation";
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ category: 'validation',
3
+ };
@@ -0,0 +1,24 @@
1
+ interface ImageDimensions {
2
+ readonly width: number;
3
+ readonly height: number;
4
+ }
5
+ /**
6
+ * Reset module-level caches. **Only for testing.**
7
+ */
8
+ export declare function _resetCacheForTesting(): void;
9
+ /**
10
+ * Get the dimensions of an image file, using a cache keyed by absolute path,
11
+ * file size, and the original `src` value.
12
+ *
13
+ * Query strings and fragments in `src` are stripped when resolving the file
14
+ * path, but the original `src` is kept in the cache key so that different
15
+ * cache-busting suffixes produce separate cache entries (mirroring browser
16
+ * cache-busting semantics).
17
+ *
18
+ * @param src - The `src` attribute value
19
+ * @param documentRoot - Root directory for absolute paths
20
+ * @param documentFilename - The filename of the document being linted
21
+ * @returns The image dimensions, or `null` if the image cannot be resolved or read
22
+ */
23
+ export declare function getImageDimensions(src: string, documentRoot: string | undefined, documentFilename: string | undefined): Promise<ImageDimensions | null>;
24
+ export {};
@@ -0,0 +1,165 @@
1
+ import { readFile, stat, mkdir, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { imageSize } from 'image-size';
6
+ // In-memory cache: key = "absolutePath:fileSize:src"
7
+ const memoryCache = new Map();
8
+ let cacheLoaded = false;
9
+ let _cacheDir;
10
+ function getCacheDir() {
11
+ if (_cacheDir != null) {
12
+ return _cacheDir;
13
+ }
14
+ const req = createRequire(import.meta.url);
15
+ const { version } = req('../../package.json');
16
+ _cacheDir = path.join(tmpdir(), `markuplint-v${version}`);
17
+ return _cacheDir;
18
+ }
19
+ /**
20
+ * Reset module-level caches. **Only for testing.**
21
+ */
22
+ export function _resetCacheForTesting() {
23
+ memoryCache.clear();
24
+ cacheLoaded = false;
25
+ _cacheDir = undefined;
26
+ }
27
+ function getCacheFilePath() {
28
+ return path.join(getCacheDir(), 'image-size-cache.json');
29
+ }
30
+ async function loadCacheFromDisk() {
31
+ if (cacheLoaded) {
32
+ return;
33
+ }
34
+ cacheLoaded = true;
35
+ let raw;
36
+ try {
37
+ raw = await readFile(getCacheFilePath(), 'utf8');
38
+ }
39
+ catch {
40
+ return;
41
+ }
42
+ let data;
43
+ try {
44
+ data = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ for (const [key, value] of Object.entries(data)) {
50
+ memoryCache.set(key, value);
51
+ }
52
+ }
53
+ async function saveCacheToDisk() {
54
+ const dir = getCacheDir();
55
+ try {
56
+ await mkdir(dir, { recursive: true });
57
+ }
58
+ catch {
59
+ return;
60
+ }
61
+ const obj = {};
62
+ for (const [key, value] of memoryCache) {
63
+ obj[key] = value;
64
+ }
65
+ try {
66
+ await writeFile(getCacheFilePath(), JSON.stringify(obj));
67
+ }
68
+ catch {
69
+ // Ignore write failures
70
+ }
71
+ }
72
+ function stripQueryAndFragment(src) {
73
+ const qIndex = src.indexOf('?');
74
+ const hIndex = src.indexOf('#');
75
+ let end = src.length;
76
+ if (qIndex !== -1)
77
+ end = Math.min(end, qIndex);
78
+ if (hIndex !== -1)
79
+ end = Math.min(end, hIndex);
80
+ return src.slice(0, end);
81
+ }
82
+ /**
83
+ * Resolve the absolute path of an image from its `src` attribute value.
84
+ *
85
+ * Query strings (`?...`) and fragments (`#...`) are stripped before resolving
86
+ * the file path so that cache-busting suffixes do not prevent file lookup.
87
+ *
88
+ * @param src - The `src` attribute value
89
+ * @param documentRoot - Root directory for absolute paths (defaults to cwd)
90
+ * @param documentFilename - The filename of the document being linted
91
+ * @returns The absolute path, or `null` if the path cannot be resolved (remote URLs, data URIs)
92
+ */
93
+ function resolveImagePath(src, documentRoot, documentFilename) {
94
+ // Skip remote URLs and data URIs
95
+ if (/^https?:\/\//i.test(src) || /^data:/i.test(src)) {
96
+ return null;
97
+ }
98
+ const cleanSrc = stripQueryAndFragment(src);
99
+ // Absolute path (starts with /)
100
+ if (cleanSrc.startsWith('/')) {
101
+ return path.join(documentRoot ?? process.cwd(), cleanSrc);
102
+ }
103
+ // Relative path
104
+ if (documentFilename) {
105
+ return path.resolve(path.dirname(documentFilename), cleanSrc);
106
+ }
107
+ return path.join(documentRoot ?? process.cwd(), cleanSrc);
108
+ }
109
+ /**
110
+ * Get the dimensions of an image file, using a cache keyed by absolute path,
111
+ * file size, and the original `src` value.
112
+ *
113
+ * Query strings and fragments in `src` are stripped when resolving the file
114
+ * path, but the original `src` is kept in the cache key so that different
115
+ * cache-busting suffixes produce separate cache entries (mirroring browser
116
+ * cache-busting semantics).
117
+ *
118
+ * @param src - The `src` attribute value
119
+ * @param documentRoot - Root directory for absolute paths
120
+ * @param documentFilename - The filename of the document being linted
121
+ * @returns The image dimensions, or `null` if the image cannot be resolved or read
122
+ */
123
+ export async function getImageDimensions(src, documentRoot, documentFilename) {
124
+ const absolutePath = resolveImagePath(src, documentRoot, documentFilename);
125
+ if (absolutePath == null) {
126
+ return null;
127
+ }
128
+ await loadCacheFromDisk();
129
+ // Get file size for cache key
130
+ let fileSize;
131
+ try {
132
+ const fileStat = await stat(absolutePath);
133
+ fileSize = fileStat.size;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ const cacheKey = `${absolutePath}:${fileSize}:${src}`;
139
+ const cached = memoryCache.get(cacheKey);
140
+ if (cached) {
141
+ return cached;
142
+ }
143
+ // Read and measure the image
144
+ let buffer;
145
+ try {
146
+ buffer = await readFile(absolutePath);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ let result;
152
+ try {
153
+ result = imageSize(buffer);
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ if (result.width == null || result.height == null) {
159
+ return null;
160
+ }
161
+ const dimensions = { width: result.width, height: result.height };
162
+ memoryCache.set(cacheKey, dimensions);
163
+ await saveCacheToDisk();
164
+ return dimensions;
165
+ }
@@ -1,6 +1,33 @@
1
1
  import type { Translator } from '@markuplint/i18n';
2
2
  import type { AttributeType } from '@markuplint/ml-spec';
3
- import type { UnmatchedResult } from '@markuplint/types';
3
+ import type { UnmatchedResult, Type } from '@markuplint/types';
4
4
  import type { ReadonlyDeep } from 'type-fest';
5
- export declare function createMessageValueExpected(t: Translator, baseTarget: string, type: ReadonlyDeep<AttributeType>, matches: UnmatchedResult): string;
5
+ /**
6
+ * Builds a human-readable error message explaining why an attribute value
7
+ * does not match its expected type. Handles both plain types and list types
8
+ * (space-separated or comma-separated).
9
+ *
10
+ * @param t - The i18n translator for generating localized messages
11
+ * @param baseTarget - A human-readable description of the attribute being validated
12
+ * (e.g. `'the "href" attribute'`)
13
+ * @param type - The attribute type definition that the value was validated against
14
+ * @param matches - The result object from a failed type check, containing
15
+ * details about the mismatch (reason, candidate, part name, etc.)
16
+ * @returns A localized error message describing the expected value
17
+ */
18
+ export declare function createMessageValueExpected(t: Translator, baseTarget: string, type: ReadonlyDeep<AttributeType | Type>, matches: UnmatchedResult): string;
19
+ /**
20
+ * Assembles the detailed portion of a value-expected error message by
21
+ * inspecting the mismatch reason and composing reason, expectation,
22
+ * candidate suggestion, and fallback parts into a single string.
23
+ *
24
+ * This is an internal helper exposed for testing and advanced use.
25
+ * The double-underscore prefix indicates it is not part of the stable public API.
26
+ *
27
+ * @param t - The i18n translator for generating localized messages
28
+ * @param baseTarget - A human-readable description of the validation target
29
+ * @param expected - A description of the expected value(s), or `null` if unknown
30
+ * @param matches - Partial match result containing reason, candidate, and other details
31
+ * @returns A localized error message string
32
+ */
6
33
  export declare function __createMessageValueExpected(t: Translator, baseTarget: string, expected: string | null, matches: Pick<UnmatchedResult, 'partName' | 'reason' | 'raw' | 'candidate' | 'ref' | 'extra' | 'fallbackTo'>): string;
@@ -1,4 +1,17 @@
1
- import { isList, isKeyword, isEnum, isNumber, isDirective } from '@markuplint/types';
1
+ import { isList, isKeyword, isEnum, isNumber, isDirective, isPattern } from '@markuplint/types';
2
+ /**
3
+ * Builds a human-readable error message explaining why an attribute value
4
+ * does not match its expected type. Handles both plain types and list types
5
+ * (space-separated or comma-separated).
6
+ *
7
+ * @param t - The i18n translator for generating localized messages
8
+ * @param baseTarget - A human-readable description of the attribute being validated
9
+ * (e.g. `'the "href" attribute'`)
10
+ * @param type - The attribute type definition that the value was validated against
11
+ * @param matches - The result object from a failed type check, containing
12
+ * details about the mismatch (reason, candidate, part name, etc.)
13
+ * @returns A localized error message describing the expected value
14
+ */
2
15
  export function createMessageValueExpected(t, baseTarget, type, matches) {
3
16
  let target = baseTarget;
4
17
  let listDescriptionPart;
@@ -17,6 +30,20 @@ export function createMessageValueExpected(t, baseTarget, type, matches) {
17
30
  .join(t('. '));
18
31
  return message;
19
32
  }
33
+ /**
34
+ * Assembles the detailed portion of a value-expected error message by
35
+ * inspecting the mismatch reason and composing reason, expectation,
36
+ * candidate suggestion, and fallback parts into a single string.
37
+ *
38
+ * This is an internal helper exposed for testing and advanced use.
39
+ * The double-underscore prefix indicates it is not part of the stable public API.
40
+ *
41
+ * @param t - The i18n translator for generating localized messages
42
+ * @param baseTarget - A human-readable description of the validation target
43
+ * @param expected - A description of the expected value(s), or `null` if unknown
44
+ * @param matches - Partial match result containing reason, candidate, and other details
45
+ * @returns A localized error message string
46
+ */
20
47
  export function __createMessageValueExpected(t, baseTarget, expected, matches) {
21
48
  let target = baseTarget;
22
49
  let reasonPart;
@@ -132,7 +159,7 @@ export function __createMessageValueExpected(t, baseTarget, expected, matches) {
132
159
  switch (matches.reason.type) {
133
160
  case 'out-of-range-length-digit': {
134
161
  const { gte, lte } = matches.reason;
135
- let expectedDigits = null;
162
+ let expectedDigits;
136
163
  if (lte != null && gte === lte) {
137
164
  expectedDigits = t('{0} digits', gte);
138
165
  }
@@ -199,6 +226,17 @@ export function __createMessageValueExpected(t, baseTarget, expected, matches) {
199
226
  }
200
227
  return message;
201
228
  }
229
+ /**
230
+ * Derives a human-readable description of the expected value from an attribute
231
+ * type definition and the unmatched result. Inspects keywords, enums, numbers,
232
+ * directives, and explicit `expects` entries from the match result.
233
+ *
234
+ * @param type - The attribute type (excluding list wrappers)
235
+ * @param matches - The unmatched result containing optional `expects` hints
236
+ * @param t - The i18n translator
237
+ * @returns A localized string describing the expected value(s), or `null` if
238
+ * no expectation can be determined
239
+ */
202
240
  function createExpectedObject(type, matches, t) {
203
241
  const expectedObject = [];
204
242
  if (matches.expects && matches.expects.length > 0) {
@@ -220,6 +258,9 @@ function createExpectedObject(type, matches, t) {
220
258
  else if (isDirective(type)) {
221
259
  expectedObject.push(t('a {0}', 'directive'));
222
260
  }
261
+ else if (isPattern(type)) {
262
+ expectedObject.push(t('{0} ({1})', 'regular expression', type.pattern));
263
+ }
223
264
  const expects = expectedObject.length === 0
224
265
  ? null
225
266
  : 1 < expectedObject.length
@@ -227,6 +268,14 @@ function createExpectedObject(type, matches, t) {
227
268
  : (expectedObject[0] ?? null);
228
269
  return expects;
229
270
  }
271
+ /**
272
+ * Converts a single `Expect` descriptor into a human-readable word or phrase.
273
+ *
274
+ * @param t - The i18n translator
275
+ * @param expect - The expectation descriptor from a type-check result
276
+ * @param type - The attribute type context for determining phrasing (e.g. CSS syntax)
277
+ * @returns A localized word or phrase describing the expected value
278
+ */
230
279
  function expectValueToWord(t, expect, type) {
231
280
  switch (expect.type) {
232
281
  case 'common': {
@@ -249,6 +298,15 @@ function expectValueToWord(t, expect, type) {
249
298
  }
250
299
  }
251
300
  }
301
+ /**
302
+ * Builds a human-readable description of an expected numeric range
303
+ * from a `Number` type definition (e.g. "integer greater than or equal to 0").
304
+ *
305
+ * @param t - The i18n translator
306
+ * @param type - The number type definition containing range constraints
307
+ * (`gt`, `gte`, `lt`, `lte`) and the numeric type label
308
+ * @returns A localized description of the expected numeric range
309
+ */
252
310
  function createExpectedNumber(t, type) {
253
311
  if (type.gt != null) {
254
312
  if (type.lt != null) {
package/lib/debug.d.ts CHANGED
@@ -1,3 +1,12 @@
1
1
  import debug from 'debug';
2
+ /**
3
+ * Type alias for the debug logger function provided by the `debug` package.
4
+ * Used throughout the rules package for conditional diagnostic logging.
5
+ */
2
6
  export type Log = debug.Debugger;
7
+ /**
8
+ * Shared debug logger instance for the `@markuplint/rules` package.
9
+ * Enable by setting the `DEBUG` environment variable to include `ml-rules`
10
+ * (e.g. `DEBUG=ml-rules`).
11
+ */
3
12
  export declare const log: debug.Debugger;
package/lib/debug.js CHANGED
@@ -1,2 +1,7 @@
1
1
  import debug from 'debug';
2
+ /**
3
+ * Shared debug logger instance for the `@markuplint/rules` package.
4
+ * Enable by setting the `DEBUG` environment variable to include `ml-rules`
5
+ * (e.g. `DEBUG=ml-rules`).
6
+ */
2
7
  export const log = debug('ml-rules');
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Rule that reports the use of deprecated or obsolete HTML attributes.
3
+ *
4
+ * Checks each attribute against the HTML spec and reports any that are
5
+ * marked as deprecated or obsolete.
6
+ */
1
7
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
2
8
  export default _default;
@@ -1,5 +1,11 @@
1
1
  import { createRule, getAttrSpecs } from '@markuplint/ml-core';
2
2
  import meta from './meta.js';
3
+ /**
4
+ * Rule that reports the use of deprecated or obsolete HTML attributes.
5
+ *
6
+ * Checks each attribute against the HTML spec and reports any that are
7
+ * marked as deprecated or obsolete.
8
+ */
3
9
  export default createRule({
4
10
  meta: meta,
5
11
  async verify({ document, report, t }) {
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `deprecated-attr`: categorized as a validation rule. */
1
2
  declare const _default: {
2
3
  readonly category: "validation";
3
4
  };
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `deprecated-attr`: categorized as a validation rule. */
1
2
  export default {
2
3
  category: 'validation',
3
4
  };
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Rule that reports the use of deprecated or obsolete HTML elements.
3
+ *
4
+ * Walks HTML and SVG elements and checks their spec status. Reports any
5
+ * element that is marked as deprecated or obsolete in the HTML specification.
6
+ */
1
7
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
2
8
  export default _default;
@@ -1,5 +1,11 @@
1
1
  import { createRule, getSpec } from '@markuplint/ml-core';
2
2
  import meta from './meta.js';
3
+ /**
4
+ * Rule that reports the use of deprecated or obsolete HTML elements.
5
+ *
6
+ * Walks HTML and SVG elements and checks their spec status. Reports any
7
+ * element that is marked as deprecated or obsolete in the HTML specification.
8
+ */
3
9
  export default createRule({
4
10
  meta: meta,
5
11
  async verify({ document, report, t }) {
@@ -10,8 +16,8 @@ export default createRule({
10
16
  return;
11
17
  }
12
18
  const spec = getSpec(el, document.specs.specs);
13
- if (spec && (spec.obsolete != null || spec.deprecated || spec.nonStandard)) {
14
- const message = t('{0} is {1:c}', t('the "{0*}" {1}', el.localName, 'element'), spec.deprecated ? 'deprecated' : spec.obsolete == null ? 'non-standard' : 'obsolete');
19
+ if (spec && (spec.obsolete != null || spec.deprecated)) {
20
+ const message = t('{0} is {1:c}', t('the "{0*}" {1}', el.localName, 'element'), spec.deprecated ? 'deprecated' : 'obsolete');
15
21
  report({
16
22
  scope: el,
17
23
  message,
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `deprecated-element`: categorized as a validation rule. */
1
2
  declare const _default: {
2
3
  readonly category: "validation";
3
4
  };
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `deprecated-element`: categorized as a validation rule. */
1
2
  export default {
2
3
  category: 'validation',
3
4
  };
@@ -1,2 +1,9 @@
1
+ /**
2
+ * Rule that disallows elements matching the configured CSS selectors.
3
+ *
4
+ * Accepts an array of selector strings as its value. Queries the document
5
+ * (and per-element overrides) for matching elements and reports each one
6
+ * as disallowed.
7
+ */
1
8
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<string[], undefined>>;
2
9
  export default _default;
@@ -1,5 +1,12 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
2
  import meta from './meta.js';
3
+ /**
4
+ * Rule that disallows elements matching the configured CSS selectors.
5
+ *
6
+ * Accepts an array of selector strings as its value. Queries the document
7
+ * (and per-element overrides) for matching elements and reports each one
8
+ * as disallowed.
9
+ */
3
10
  export default createRule({
4
11
  meta: meta,
5
12
  defaultValue: [],
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `disallowed-element`: categorized as a validation rule. */
1
2
  declare const _default: {
2
3
  readonly category: "validation";
3
4
  };
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `disallowed-element`: categorized as a validation rule. */
1
2
  export default {
2
3
  category: 'validation',
3
4
  };
@@ -1,5 +1,15 @@
1
+ /** Configuration options for the doctype rule. */
1
2
  type Option = {
3
+ /** Whether to report obsolete doctypes that include a public or system identifier. */
2
4
  denyObsoleteType: boolean;
3
5
  };
6
+ /**
7
+ * Rule that validates the presence and correctness of the document's DOCTYPE
8
+ * declaration.
9
+ *
10
+ * Reports an error when no DOCTYPE is found (skipped for document fragments)
11
+ * and when an obsolete DOCTYPE (one with a public or system identifier) is
12
+ * declared.
13
+ */
4
14
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<"always", Option>>;
5
15
  export default _default;
@@ -1,5 +1,13 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
2
  import meta from './meta.js';
3
+ /**
4
+ * Rule that validates the presence and correctness of the document's DOCTYPE
5
+ * declaration.
6
+ *
7
+ * Reports an error when no DOCTYPE is found (skipped for document fragments)
8
+ * and when an obsolete DOCTYPE (one with a public or system identifier) is
9
+ * declared.
10
+ */
3
11
  export default createRule({
4
12
  meta: meta,
5
13
  defaultValue: 'always',
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `doctype`: categorized as a validation rule. */
1
2
  declare const _default: {
2
3
  readonly category: "validation";
3
4
  };
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `doctype`: categorized as a validation rule. */
1
2
  export default {
2
3
  category: 'validation',
3
4
  };
@@ -1,2 +1,9 @@
1
+ /**
2
+ * Rule that checks for missing end tags on non-void HTML elements.
3
+ *
4
+ * Reports elements that are neither void nor self-closing and have no
5
+ * closing tag. Omitted elements and elements in documents configured
6
+ * with `endTag: 'never'` are excluded.
7
+ */
1
8
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, undefined>>;
2
9
  export default _default;
@@ -1,6 +1,13 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
2
  import { isVoidElement } from '@markuplint/ml-spec';
3
3
  import meta from './meta.js';
4
+ /**
5
+ * Rule that checks for missing end tags on non-void HTML elements.
6
+ *
7
+ * Reports elements that are neither void nor self-closing and have no
8
+ * closing tag. Omitted elements and elements in documents configured
9
+ * with `endTag: 'never'` are excluded.
10
+ */
4
11
  export default createRule({
5
12
  meta: meta,
6
13
  defaultSeverity: 'warning',
@@ -18,7 +25,7 @@ export default createRule({
18
25
  if (el.closeTag != null) {
19
26
  return;
20
27
  }
21
- if ((document.endTag === 'xml' || el.isForeignElement) && el.selfClosingSolidus?.raw) {
28
+ if ((document.endTag === 'xml' || el.isForeignElement) && el.tagCloseChar.startsWith('/')) {
22
29
  return;
23
30
  }
24
31
  report({
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `end-tag`: categorized as a style rule. */
1
2
  declare const _default: {
2
3
  readonly category: "style";
3
4
  };
@@ -1,3 +1,4 @@
1
+ /** Rule metadata for `end-tag`: categorized as a style rule. */
1
2
  export default {
2
3
  category: 'style',
3
4
  };
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Rule that validates heading levels (h1-h6) are not skipped.
3
+ *
4
+ * Ensures that heading elements appear in a sequential order without
5
+ * gaps (e.g., an h4 must not follow an h2 directly without an h3 in between).
6
+ */
1
7
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
2
8
  export default _default;