@brandlab-digital/conformance 3.7.0 → 3.9.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 (290) hide show
  1. package/README.md +26 -0
  2. package/dist/current-release.js +5 -5
  3. package/dist/current-release.js.map +1 -1
  4. package/dist/engineering.js +4 -2
  5. package/dist/engineering.js.map +1 -1
  6. package/dist/public-copy.d.ts +7 -0
  7. package/dist/public-copy.d.ts.map +1 -1
  8. package/dist/public-copy.js +8 -3
  9. package/dist/public-copy.js.map +1 -1
  10. package/dist/ready-cli.d.ts +38 -0
  11. package/dist/ready-cli.d.ts.map +1 -0
  12. package/dist/ready-cli.js +327 -0
  13. package/dist/ready-cli.js.map +1 -0
  14. package/dist/site-system/checks.d.ts +389 -0
  15. package/dist/site-system/checks.d.ts.map +1 -0
  16. package/dist/site-system/checks.js +96 -0
  17. package/dist/site-system/checks.js.map +1 -0
  18. package/dist/site-system/config.d.ts +40 -0
  19. package/dist/site-system/config.d.ts.map +1 -0
  20. package/dist/site-system/config.js +214 -0
  21. package/dist/site-system/config.js.map +1 -0
  22. package/dist/site-system/index.d.ts +14 -0
  23. package/dist/site-system/index.d.ts.map +1 -0
  24. package/dist/site-system/index.js +14 -0
  25. package/dist/site-system/index.js.map +1 -0
  26. package/dist/site-system/journeys/access.d.ts +7 -0
  27. package/dist/site-system/journeys/access.d.ts.map +1 -0
  28. package/dist/site-system/journeys/access.js +263 -0
  29. package/dist/site-system/journeys/access.js.map +1 -0
  30. package/dist/site-system/journeys/browser.d.ts +79 -0
  31. package/dist/site-system/journeys/browser.d.ts.map +1 -0
  32. package/dist/site-system/journeys/browser.js +218 -0
  33. package/dist/site-system/journeys/browser.js.map +1 -0
  34. package/dist/site-system/journeys/conversion.d.ts +3 -0
  35. package/dist/site-system/journeys/conversion.d.ts.map +1 -0
  36. package/dist/site-system/journeys/conversion.js +152 -0
  37. package/dist/site-system/journeys/conversion.js.map +1 -0
  38. package/dist/site-system/journeys/menu.d.ts +11 -0
  39. package/dist/site-system/journeys/menu.d.ts.map +1 -0
  40. package/dist/site-system/journeys/menu.js +395 -0
  41. package/dist/site-system/journeys/menu.js.map +1 -0
  42. package/dist/site-system/journeys/page-helpers.d.ts +146 -0
  43. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -0
  44. package/dist/site-system/journeys/page-helpers.js +426 -0
  45. package/dist/site-system/journeys/page-helpers.js.map +1 -0
  46. package/dist/site-system/journeys/plan.d.ts +82 -0
  47. package/dist/site-system/journeys/plan.d.ts.map +1 -0
  48. package/dist/site-system/journeys/plan.js +112 -0
  49. package/dist/site-system/journeys/plan.js.map +1 -0
  50. package/dist/site-system/journeys/shell.d.ts +24 -0
  51. package/dist/site-system/journeys/shell.d.ts.map +1 -0
  52. package/dist/site-system/journeys/shell.js +269 -0
  53. package/dist/site-system/journeys/shell.js.map +1 -0
  54. package/dist/site-system/playwright-report.d.ts +22 -0
  55. package/dist/site-system/playwright-report.d.ts.map +1 -0
  56. package/dist/site-system/playwright-report.js +131 -0
  57. package/dist/site-system/playwright-report.js.map +1 -0
  58. package/dist/site-system/records.d.ts +30 -0
  59. package/dist/site-system/records.d.ts.map +1 -0
  60. package/dist/site-system/records.js +98 -0
  61. package/dist/site-system/records.js.map +1 -0
  62. package/dist/site-system/report.d.ts +116 -0
  63. package/dist/site-system/report.d.ts.map +1 -0
  64. package/dist/site-system/report.js +366 -0
  65. package/dist/site-system/report.js.map +1 -0
  66. package/dist/site-system/rule-modules.d.ts +8 -0
  67. package/dist/site-system/rule-modules.d.ts.map +1 -0
  68. package/dist/site-system/rule-modules.js +33 -0
  69. package/dist/site-system/rule-modules.js.map +1 -0
  70. package/dist/site-system/rules/design/d4-distinctness.d.ts +36 -0
  71. package/dist/site-system/rules/design/d4-distinctness.d.ts.map +1 -0
  72. package/dist/site-system/rules/design/d4-distinctness.js +207 -0
  73. package/dist/site-system/rules/design/d4-distinctness.js.map +1 -0
  74. package/dist/site-system/rules/design/index.d.ts +17 -0
  75. package/dist/site-system/rules/design/index.d.ts.map +1 -0
  76. package/dist/site-system/rules/design/index.js +7 -0
  77. package/dist/site-system/rules/design/index.js.map +1 -0
  78. package/dist/site-system/rules/enquiry/c2-quote-form.d.ts +31 -0
  79. package/dist/site-system/rules/enquiry/c2-quote-form.d.ts.map +1 -0
  80. package/dist/site-system/rules/enquiry/c2-quote-form.js +211 -0
  81. package/dist/site-system/rules/enquiry/c2-quote-form.js.map +1 -0
  82. package/dist/site-system/rules/enquiry/common.d.ts +37 -0
  83. package/dist/site-system/rules/enquiry/common.d.ts.map +1 -0
  84. package/dist/site-system/rules/enquiry/common.js +59 -0
  85. package/dist/site-system/rules/enquiry/common.js.map +1 -0
  86. package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts +8 -0
  87. package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts.map +1 -0
  88. package/dist/site-system/rules/enquiry/e1-recorded-titles.js +59 -0
  89. package/dist/site-system/rules/enquiry/e1-recorded-titles.js.map +1 -0
  90. package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts +22 -0
  91. package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts.map +1 -0
  92. package/dist/site-system/rules/enquiry/e9-cost-guides.js +55 -0
  93. package/dist/site-system/rules/enquiry/e9-cost-guides.js.map +1 -0
  94. package/dist/site-system/rules/enquiry/html.d.ts +53 -0
  95. package/dist/site-system/rules/enquiry/html.d.ts.map +1 -0
  96. package/dist/site-system/rules/enquiry/html.js +294 -0
  97. package/dist/site-system/rules/enquiry/html.js.map +1 -0
  98. package/dist/site-system/rules/enquiry/index.d.ts +17 -0
  99. package/dist/site-system/rules/enquiry/index.d.ts.map +1 -0
  100. package/dist/site-system/rules/enquiry/index.js +26 -0
  101. package/dist/site-system/rules/enquiry/index.js.map +1 -0
  102. package/dist/site-system/rules/enquiry/k8-sections.d.ts +17 -0
  103. package/dist/site-system/rules/enquiry/k8-sections.d.ts.map +1 -0
  104. package/dist/site-system/rules/enquiry/k8-sections.js +76 -0
  105. package/dist/site-system/rules/enquiry/k8-sections.js.map +1 -0
  106. package/dist/site-system/rules/enquiry/literal.d.ts +29 -0
  107. package/dist/site-system/rules/enquiry/literal.d.ts.map +1 -0
  108. package/dist/site-system/rules/enquiry/literal.js +189 -0
  109. package/dist/site-system/rules/enquiry/literal.js.map +1 -0
  110. package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts +73 -0
  111. package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts.map +1 -0
  112. package/dist/site-system/rules/enquiry/t11-licence-classes.js +275 -0
  113. package/dist/site-system/rules/enquiry/t11-licence-classes.js.map +1 -0
  114. package/dist/site-system/rules/enquiry/t5-reviews.d.ts +23 -0
  115. package/dist/site-system/rules/enquiry/t5-reviews.d.ts.map +1 -0
  116. package/dist/site-system/rules/enquiry/t5-reviews.js +91 -0
  117. package/dist/site-system/rules/enquiry/t5-reviews.js.map +1 -0
  118. package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
  119. package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
  120. package/dist/site-system/rules/form/c11-contact-route.js +132 -0
  121. package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
  122. package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
  123. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
  124. package/dist/site-system/rules/form/c7-thank-you.js +67 -0
  125. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
  126. package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
  127. package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
  128. package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
  129. package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
  130. package/dist/site-system/rules/form/index.d.ts +14 -0
  131. package/dist/site-system/rules/form/index.d.ts.map +1 -0
  132. package/dist/site-system/rules/form/index.js +13 -0
  133. package/dist/site-system/rules/form/index.js.map +1 -0
  134. package/dist/site-system/rules/library/common.d.ts +36 -0
  135. package/dist/site-system/rules/library/common.d.ts.map +1 -0
  136. package/dist/site-system/rules/library/common.js +70 -0
  137. package/dist/site-system/rules/library/common.js.map +1 -0
  138. package/dist/site-system/rules/library/index.d.ts +16 -0
  139. package/dist/site-system/rules/library/index.d.ts.map +1 -0
  140. package/dist/site-system/rules/library/index.js +19 -0
  141. package/dist/site-system/rules/library/index.js.map +1 -0
  142. package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
  143. package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
  144. package/dist/site-system/rules/library/k1-shared-components.js +97 -0
  145. package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
  146. package/dist/site-system/rules/navigation/extract.d.ts +50 -0
  147. package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
  148. package/dist/site-system/rules/navigation/extract.js +220 -0
  149. package/dist/site-system/rules/navigation/extract.js.map +1 -0
  150. package/dist/site-system/rules/navigation/html.d.ts +23 -0
  151. package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
  152. package/dist/site-system/rules/navigation/html.js +219 -0
  153. package/dist/site-system/rules/navigation/html.js.map +1 -0
  154. package/dist/site-system/rules/navigation/index.d.ts +37 -0
  155. package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
  156. package/dist/site-system/rules/navigation/index.js +163 -0
  157. package/dist/site-system/rules/navigation/index.js.map +1 -0
  158. package/dist/site-system/rules/navigation/record.d.ts +8 -0
  159. package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
  160. package/dist/site-system/rules/navigation/record.js +39 -0
  161. package/dist/site-system/rules/navigation/record.js.map +1 -0
  162. package/dist/site-system/rules/sections/exif.d.ts +22 -0
  163. package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
  164. package/dist/site-system/rules/sections/exif.js +218 -0
  165. package/dist/site-system/rules/sections/exif.js.map +1 -0
  166. package/dist/site-system/rules/sections/images.d.ts +31 -0
  167. package/dist/site-system/rules/sections/images.d.ts.map +1 -0
  168. package/dist/site-system/rules/sections/images.js +96 -0
  169. package/dist/site-system/rules/sections/images.js.map +1 -0
  170. package/dist/site-system/rules/sections/index.d.ts +20 -0
  171. package/dist/site-system/rules/sections/index.d.ts.map +1 -0
  172. package/dist/site-system/rules/sections/index.js +34 -0
  173. package/dist/site-system/rules/sections/index.js.map +1 -0
  174. package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
  175. package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
  176. package/dist/site-system/rules/sections/k11-density.js +88 -0
  177. package/dist/site-system/rules/sections/k11-density.js.map +1 -0
  178. package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
  179. package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
  180. package/dist/site-system/rules/sections/k12-contact.js +70 -0
  181. package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
  182. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
  183. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
  184. package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
  185. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
  186. package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
  187. package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
  188. package/dist/site-system/rules/sections/n9-footer.js +186 -0
  189. package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
  190. package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
  191. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
  192. package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
  193. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
  194. package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
  195. package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
  196. package/dist/site-system/rules/sections/t7-main-images.js +41 -0
  197. package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
  198. package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
  199. package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
  200. package/dist/site-system/rules/sections/t8-prices.js +92 -0
  201. package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
  202. package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
  203. package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
  204. package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
  205. package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
  206. package/dist/site-system/rules/shell/banned-wording.d.ts +24 -0
  207. package/dist/site-system/rules/shell/banned-wording.d.ts.map +1 -0
  208. package/dist/site-system/rules/shell/banned-wording.js +212 -0
  209. package/dist/site-system/rules/shell/banned-wording.js.map +1 -0
  210. package/dist/site-system/rules/shell/budgets.d.ts +43 -0
  211. package/dist/site-system/rules/shell/budgets.d.ts.map +1 -0
  212. package/dist/site-system/rules/shell/budgets.js +297 -0
  213. package/dist/site-system/rules/shell/budgets.js.map +1 -0
  214. package/dist/site-system/rules/shell/cta-labels.d.ts +17 -0
  215. package/dist/site-system/rules/shell/cta-labels.d.ts.map +1 -0
  216. package/dist/site-system/rules/shell/cta-labels.js +86 -0
  217. package/dist/site-system/rules/shell/cta-labels.js.map +1 -0
  218. package/dist/site-system/rules/shell/facts.d.ts +33 -0
  219. package/dist/site-system/rules/shell/facts.d.ts.map +1 -0
  220. package/dist/site-system/rules/shell/facts.js +217 -0
  221. package/dist/site-system/rules/shell/facts.js.map +1 -0
  222. package/dist/site-system/rules/shell/headings.d.ts +5 -0
  223. package/dist/site-system/rules/shell/headings.d.ts.map +1 -0
  224. package/dist/site-system/rules/shell/headings.js +23 -0
  225. package/dist/site-system/rules/shell/headings.js.map +1 -0
  226. package/dist/site-system/rules/shell/hero-location.d.ts +4 -0
  227. package/dist/site-system/rules/shell/hero-location.d.ts.map +1 -0
  228. package/dist/site-system/rules/shell/hero-location.js +45 -0
  229. package/dist/site-system/rules/shell/hero-location.js.map +1 -0
  230. package/dist/site-system/rules/shell/html.d.ts +53 -0
  231. package/dist/site-system/rules/shell/html.d.ts.map +1 -0
  232. package/dist/site-system/rules/shell/html.js +341 -0
  233. package/dist/site-system/rules/shell/html.js.map +1 -0
  234. package/dist/site-system/rules/shell/index.d.ts +18 -0
  235. package/dist/site-system/rules/shell/index.d.ts.map +1 -0
  236. package/dist/site-system/rules/shell/index.js +65 -0
  237. package/dist/site-system/rules/shell/index.js.map +1 -0
  238. package/dist/site-system/rules/shell/licence.d.ts +4 -0
  239. package/dist/site-system/rules/shell/licence.d.ts.map +1 -0
  240. package/dist/site-system/rules/shell/licence.js +60 -0
  241. package/dist/site-system/rules/shell/licence.js.map +1 -0
  242. package/dist/site-system/rules/shell/location-wording.d.ts +13 -0
  243. package/dist/site-system/rules/shell/location-wording.d.ts.map +1 -0
  244. package/dist/site-system/rules/shell/location-wording.js +15 -0
  245. package/dist/site-system/rules/shell/location-wording.js.map +1 -0
  246. package/dist/site-system/rules/shell/navigation.d.ts +5 -0
  247. package/dist/site-system/rules/shell/navigation.d.ts.map +1 -0
  248. package/dist/site-system/rules/shell/navigation.js +272 -0
  249. package/dist/site-system/rules/shell/navigation.js.map +1 -0
  250. package/dist/site-system/rules/shell/overlays.d.ts +4 -0
  251. package/dist/site-system/rules/shell/overlays.d.ts.map +1 -0
  252. package/dist/site-system/rules/shell/overlays.js +93 -0
  253. package/dist/site-system/rules/shell/overlays.js.map +1 -0
  254. package/dist/site-system/rules/shell/page.d.ts +51 -0
  255. package/dist/site-system/rules/shell/page.d.ts.map +1 -0
  256. package/dist/site-system/rules/shell/page.js +256 -0
  257. package/dist/site-system/rules/shell/page.js.map +1 -0
  258. package/dist/site-system/rules/shell/phone.d.ts +13 -0
  259. package/dist/site-system/rules/shell/phone.d.ts.map +1 -0
  260. package/dist/site-system/rules/shell/phone.js +99 -0
  261. package/dist/site-system/rules/shell/phone.js.map +1 -0
  262. package/dist/site-system/rules/shell/proof-numbers.d.ts +7 -0
  263. package/dist/site-system/rules/shell/proof-numbers.d.ts.map +1 -0
  264. package/dist/site-system/rules/shell/proof-numbers.js +81 -0
  265. package/dist/site-system/rules/shell/proof-numbers.js.map +1 -0
  266. package/dist/site-system/rules/shell/types.d.ts +80 -0
  267. package/dist/site-system/rules/shell/types.d.ts.map +1 -0
  268. package/dist/site-system/rules/shell/types.js +17 -0
  269. package/dist/site-system/rules/shell/types.js.map +1 -0
  270. package/dist/site-system/runners.d.ts +37 -0
  271. package/dist/site-system/runners.d.ts.map +1 -0
  272. package/dist/site-system/runners.js +48 -0
  273. package/dist/site-system/runners.js.map +1 -0
  274. package/dist/site-system/samples.d.ts +43 -0
  275. package/dist/site-system/samples.d.ts.map +1 -0
  276. package/dist/site-system/samples.js +99 -0
  277. package/dist/site-system/samples.js.map +1 -0
  278. package/dist/site-system/steps.d.ts +66 -0
  279. package/dist/site-system/steps.d.ts.map +1 -0
  280. package/dist/site-system/steps.js +45 -0
  281. package/dist/site-system/steps.js.map +1 -0
  282. package/dist/site-system/streamed-html.d.ts +9 -0
  283. package/dist/site-system/streamed-html.d.ts.map +1 -0
  284. package/dist/site-system/streamed-html.js +43 -0
  285. package/dist/site-system/streamed-html.js.map +1 -0
  286. package/dist/site-system-e2e.d.ts +6 -0
  287. package/dist/site-system-e2e.d.ts.map +1 -0
  288. package/dist/site-system-e2e.js +54 -0
  289. package/dist/site-system-e2e.js.map +1 -0
  290. package/package.json +22 -5
@@ -0,0 +1,60 @@
1
+ import { licenceRenderState, renderableLicences } from "@brandlab-digital/site-core/business";
2
+ import { normalisePagePath } from "./page.js";
3
+ import { failFinding, notApplicableFinding, passFinding } from "./types.js";
4
+ /**
5
+ * T3: a licence line renders wherever licensed work is advertised. The licence register in the business facts (read
6
+ * by T11's reader) says the trade needs a licence; every sampled page that advertises the work (not the legal and
7
+ * utility pages) shows the number of a licence that may render (site-core's renderableLicences: never an expired
8
+ * one), and the licensee's name beside it when the licensee differs from the trading name. An expired licence number
9
+ * on a page fails. Not applicable only when the evidence records no licence; facts or licence data that cannot be
10
+ * read give no finding, so the check shows as not run. The entity gate and the register check are agent steps.
11
+ */
12
+ function compact(text) {
13
+ return text.toLowerCase().replace(/[\s\-. ]/g, "");
14
+ }
15
+ function pageText(page) {
16
+ return [...page.units, ...page.attributeCopy].map((unit) => unit.text).join(" ");
17
+ }
18
+ export function checkLicenceLine(pages, facts, _factsReason, exemptPaths = [], asOf = new Date()) {
19
+ // No facts, or licence data that cannot be read: not run, never not-applicable.
20
+ if (!facts || facts.licencesUnreadable)
21
+ return [];
22
+ if (facts.licences.length === 0) {
23
+ return [notApplicableFinding("T3", "The business facts record no licence, so no licensed work is advertised on the sampled pages.")];
24
+ }
25
+ const renderable = renderableLicences(facts.licences, asOf);
26
+ const expired = facts.licences.filter((licence) => licenceRenderState(licence, asOf).state === "expired");
27
+ const exempt = new Set(exemptPaths.map(normalisePagePath));
28
+ const findings = [];
29
+ let checked = 0;
30
+ for (const page of pages) {
31
+ if (page.family === "legal" || page.family === "utility" || page.noindex || exempt.has(page.path))
32
+ continue;
33
+ checked += 1;
34
+ const text = compact(pageText(page));
35
+ for (const licence of expired.filter((candidate) => text.includes(compact(candidate.identifier)))) {
36
+ findings.push(failFinding("T3", page.path, `The page ${page.path} shows licence ${licence.identifier}, which expired on ${licence.expiresOn}; an expired licence never renders.`));
37
+ }
38
+ const shown = renderable.filter((licence) => text.includes(compact(licence.identifier)));
39
+ if (shown.length === 0) {
40
+ findings.push(failFinding("T3", page.path, renderable.length === 0
41
+ ? `The page ${page.path} advertises licensed work, but every recorded licence has expired (${facts.licences.map((licence) => licence.identifier).join(", ")}), so none may render.`
42
+ : `The page ${page.path} advertises licensed work but shows none of the recorded licence numbers (${renderable.map((licence) => licence.identifier).join(", ")}).`));
43
+ continue;
44
+ }
45
+ for (const licence of shown) {
46
+ const holder = licence.holder?.trim();
47
+ if (holder && holder.toLowerCase() !== (facts.tradingName ?? "").toLowerCase() && !text.includes(compact(holder))) {
48
+ findings.push(failFinding("T3", page.path, `The page ${page.path} shows licence ${licence.identifier} without the licensee, ${holder}, who differs from the trading name.`));
49
+ }
50
+ }
51
+ }
52
+ if (findings.length > 0)
53
+ return findings;
54
+ if (checked === 0)
55
+ return [notApplicableFinding("T3", "The sampled pages are all legal or utility pages, which advertise no licensed work.")];
56
+ return [passFinding("T3", checked === 1
57
+ ? "A recorded licence number shows on the sampled page that advertises licensed work."
58
+ : `A recorded licence number shows on all ${checked} sampled pages that advertise licensed work.`)];
59
+ }
60
+ //# sourceMappingURL=licence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"licence.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/licence.ts"],"names":[],"mappings":"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","sourcesContent":["import { licenceRenderState, renderableLicences } from \"@brandlab-digital/site-core/business\";\nimport type { ShellPage } from \"./page.js\";\nimport { normalisePagePath } from \"./page.js\";\nimport { failFinding, notApplicableFinding, passFinding, type ShellFinding, type ShellProjectFacts } from \"./types.js\";\n\n/**\n * T3: a licence line renders wherever licensed work is advertised. The licence register in the business facts (read\n * by T11's reader) says the trade needs a licence; every sampled page that advertises the work (not the legal and\n * utility pages) shows the number of a licence that may render (site-core's renderableLicences: never an expired\n * one), and the licensee's name beside it when the licensee differs from the trading name. An expired licence number\n * on a page fails. Not applicable only when the evidence records no licence; facts or licence data that cannot be\n * read give no finding, so the check shows as not run. The entity gate and the register check are agent steps.\n */\n\nfunction compact(text: string): string {\n return text.toLowerCase().replace(/[\\s\\-. ]/g, \"\");\n}\n\nfunction pageText(page: ShellPage): string {\n return [...page.units, ...page.attributeCopy].map((unit) => unit.text).join(\" \");\n}\n\nexport function checkLicenceLine(\n pages: readonly ShellPage[],\n facts: ShellProjectFacts | null,\n _factsReason: string,\n exemptPaths: readonly string[] = [],\n asOf: Date = new Date(),\n): ShellFinding[] {\n // No facts, or licence data that cannot be read: not run, never not-applicable.\n if (!facts || facts.licencesUnreadable) return [];\n if (facts.licences.length === 0) {\n return [notApplicableFinding(\"T3\", \"The business facts record no licence, so no licensed work is advertised on the sampled pages.\")];\n }\n const renderable = renderableLicences(facts.licences, asOf);\n const expired = facts.licences.filter((licence) => licenceRenderState(licence, asOf).state === \"expired\");\n const exempt = new Set(exemptPaths.map(normalisePagePath));\n const findings: ShellFinding[] = [];\n let checked = 0;\n for (const page of pages) {\n if (page.family === \"legal\" || page.family === \"utility\" || page.noindex || exempt.has(page.path)) continue;\n checked += 1;\n const text = compact(pageText(page));\n for (const licence of expired.filter((candidate) => text.includes(compact(candidate.identifier)))) {\n findings.push(failFinding(\"T3\", page.path, `The page ${page.path} shows licence ${licence.identifier}, which expired on ${licence.expiresOn}; an expired licence never renders.`));\n }\n const shown = renderable.filter((licence) => text.includes(compact(licence.identifier)));\n if (shown.length === 0) {\n findings.push(failFinding(\"T3\", page.path, renderable.length === 0\n ? `The page ${page.path} advertises licensed work, but every recorded licence has expired (${facts.licences.map((licence) => licence.identifier).join(\", \")}), so none may render.`\n : `The page ${page.path} advertises licensed work but shows none of the recorded licence numbers (${renderable.map((licence) => licence.identifier).join(\", \")}).`));\n continue;\n }\n for (const licence of shown) {\n const holder = licence.holder?.trim();\n if (holder && holder.toLowerCase() !== (facts.tradingName ?? \"\").toLowerCase() && !text.includes(compact(holder))) {\n findings.push(failFinding(\"T3\", page.path, `The page ${page.path} shows licence ${licence.identifier} without the licensee, ${holder}, who differs from the trading name.`));\n }\n }\n }\n if (findings.length > 0) return findings;\n if (checked === 0) return [notApplicableFinding(\"T3\", \"The sampled pages are all legal or utility pages, which advertise no licensed work.\")];\n return [passFinding(\"T3\", checked === 1\n ? \"A recorded licence number shows on the sampled page that advertises licensed work.\"\n : `A recorded licence number shows on all ${checked} sampled pages that advertise licensed work.`)];\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The base and distance wording that hero copy (L10-hero) and the banned list's layer 2 (E8) both refuse. One
3
+ * definition, so the two checks never disagree about what counts.
4
+ */
5
+ export declare const BASED_IN: RegExp;
6
+ /** A kilometre figure: "8 km", "12.5km", "25 kilometres". */
7
+ export declare const KM_FIGURE: RegExp;
8
+ export declare const KM_FROM: RegExp;
9
+ /** A distance from the base: "12 km from", "5 km away from", "8 km of". */
10
+ export declare const KM_FROM_BASE: RegExp;
11
+ /** The same pattern with the global flag, for `matchAll`. */
12
+ export declare function globally(pattern: RegExp): RegExp;
13
+ //# sourceMappingURL=location-wording.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location-wording.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/location-wording.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,QAAQ,QAAoB,CAAC;AAC1C,6DAA6D;AAC7D,eAAO,MAAM,SAAS,QAAyD,CAAC;AAChF,eAAO,MAAM,OAAO,QAAiD,CAAC;AACtE,2EAA2E;AAC3E,eAAO,MAAM,YAAY,QAAmF,CAAC;AAE7G,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhD"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The base and distance wording that hero copy (L10-hero) and the banned list's layer 2 (E8) both refuse. One
3
+ * definition, so the two checks never disagree about what counts.
4
+ */
5
+ export const BASED_IN = /\bbased\s+in\b/i;
6
+ /** A kilometre figure: "8 km", "12.5km", "25 kilometres". */
7
+ export const KM_FIGURE = /\b\d+(?:[.,]\d+)?\s*(?:km|kms|kilomet(?:re|er)s?)\b/i;
8
+ export const KM_FROM = /\bkm\s+from\b|\bkilomet(?:re|er)s?\s+from\b/i;
9
+ /** A distance from the base: "12 km from", "5 km away from", "8 km of". */
10
+ export const KM_FROM_BASE = /\b\d+(?:[.,]\d+)?\s*(?:km|kms|kilomet(?:re|er)s?)\s+(?:from|away\s+from|of)\b/i;
11
+ /** The same pattern with the global flag, for `matchAll`. */
12
+ export function globally(pattern) {
13
+ return new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
14
+ }
15
+ //# sourceMappingURL=location-wording.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location-wording.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/location-wording.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAC1C,6DAA6D;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAChF,MAAM,CAAC,MAAM,OAAO,GAAG,8CAA8C,CAAC;AACtE,2EAA2E;AAC3E,MAAM,CAAC,MAAM,YAAY,GAAG,gFAAgF,CAAC;AAE7G,6DAA6D;AAC7D,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;AACvG,CAAC","sourcesContent":["/**\n * The base and distance wording that hero copy (L10-hero) and the banned list's layer 2 (E8) both refuse. One\n * definition, so the two checks never disagree about what counts.\n */\n\nexport const BASED_IN = /\\bbased\\s+in\\b/i;\n/** A kilometre figure: \"8 km\", \"12.5km\", \"25 kilometres\". */\nexport const KM_FIGURE = /\\b\\d+(?:[.,]\\d+)?\\s*(?:km|kms|kilomet(?:re|er)s?)\\b/i;\nexport const KM_FROM = /\\bkm\\s+from\\b|\\bkilomet(?:re|er)s?\\s+from\\b/i;\n/** A distance from the base: \"12 km from\", \"5 km away from\", \"8 km of\". */\nexport const KM_FROM_BASE = /\\b\\d+(?:[.,]\\d+)?\\s*(?:km|kms|kilomet(?:re|er)s?)\\s+(?:from|away\\s+from|of)\\b/i;\n\n/** The same pattern with the global flag, for `matchAll`. */\nexport function globally(pattern: RegExp): RegExp {\n return new RegExp(pattern.source, pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`);\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { ShellPage } from "./page.js";
2
+ import { type ShellFinding } from "./types.js";
3
+ /** N2 and K7 over the sampled pages. */
4
+ export declare function checkNavigationCrawlability(pages: readonly ShellPage[]): ShellFinding[];
5
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAkD,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAmQ/F,wCAAwC;AACxC,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,YAAY,EAAE,CAgCvF"}
@@ -0,0 +1,272 @@
1
+ import { closest, findAll, findFirst, hasContent, isWithin, textOf } from "./html.js";
2
+ import { failFinding, notApplicableFinding, passFinding } from "./types.js";
3
+ /**
4
+ * N2 and K7: what a crawler reads without running scripts. A dropdown's links must be `<a href>` elements in the
5
+ * server HTML (N2 and K7 both name them); K7 adds accordion and tab panels with content and review text that is not
6
+ * cut short with no full copy in the HTML.
7
+ */
8
+ const DRAWER_LABEL = /^(?:open |close |toggle |show |hide )?(?:the )?(?:main |mobile |site )?(?:menu|navigation|nav)$/i;
9
+ const DRAWER_HOOKS = new Set(["menu-button", "menu-toggle", "drawer-toggle", "hamburger"]);
10
+ /** Controls that open something built on demand (a dialog, a popover, a lightbox), not content on the page. */
11
+ const ON_DEMAND_ID = /(?:dialog|modal|drawer|popover|lightbox|overlay)/i;
12
+ const READ_MORE = /^(?:read|show|see|view)\s+(?:more|full|all)\b/i;
13
+ function isRealHref(anchor) {
14
+ const href = anchor.attrs.href?.trim();
15
+ return href !== undefined && href !== "" && href !== "#" && !/^(?:javascript|data|vbscript):/i.test(href);
16
+ }
17
+ /** The name of a disclosure button: "Services menu" and "Open Services menu" both give "Services". */
18
+ function labelOf(button) {
19
+ const own = button.attrs["aria-label"]?.trim() || textOf(button);
20
+ const sibling = button.parent?.children.find((child) => child.kind === "element" && child.tag === "a");
21
+ const label = own || (sibling ? textOf(sibling) : "");
22
+ return label.replace(/^(?:open|toggle|show|hide|close)\s+/i, "").replace(/\s+(?:menu|submenu|dropdown)$/i, "").trim();
23
+ }
24
+ function controlledIds(button) {
25
+ return (button.attrs["aria-controls"] ?? "").split(/\s+/).filter(Boolean);
26
+ }
27
+ function isButtonLike(element) {
28
+ return element.tag === "button" || element.attrs.role === "button";
29
+ }
30
+ function opensDialog(button, page) {
31
+ if (button.attrs["aria-haspopup"] === "dialog")
32
+ return true;
33
+ const label = (button.attrs["aria-label"] ?? textOf(button)).trim();
34
+ if (DRAWER_LABEL.test(label))
35
+ return true;
36
+ if (DRAWER_HOOKS.has(button.attrs["data-bl"] ?? "") || DRAWER_HOOKS.has(button.attrs["data-slot"] ?? ""))
37
+ return true;
38
+ const target = controlledIds(button).map((id) => page.doc.byId.get(id)).find(Boolean);
39
+ return target !== undefined && (target.tag === "dialog" || /^(?:dialog|alertdialog)$/.test(target.attrs.role ?? "") || target.attrs["data-bl"] === "drawer");
40
+ }
41
+ function headerScopes(page) {
42
+ if (page.headers.length > 0)
43
+ return [...page.headers];
44
+ return findAll(page.body, (element) => element.tag === "nav" && !page.footers.some((footer) => isWithin(element, footer)));
45
+ }
46
+ function isDisclosure(element) {
47
+ const popup = element.attrs["aria-haspopup"];
48
+ return element.attrs["aria-expanded"] !== undefined || (popup !== undefined && popup !== "false" && popup !== "dialog");
49
+ }
50
+ /** Anything inside a <dialog> (the mobile drawer) is not a header dropdown. */
51
+ function inDialog(element) {
52
+ return closest(element, (ancestor) => ancestor.tag === "dialog" || /^(?:dialog|alertdialog)$/.test(ancestor.attrs.role ?? "") || ancestor.attrs["aria-modal"] === "true") !== null;
53
+ }
54
+ function hubAnchors(button) {
55
+ return (button.parent?.children ?? []).filter((child) => child.kind === "element" && child.tag === "a");
56
+ }
57
+ /** A menu item: inside the header's <nav>, or beside its own link (a split trigger). A search toggle is neither. */
58
+ function isNavItem(button) {
59
+ return closest(button, (ancestor) => ancestor.tag === "nav") !== null || hubAnchors(button).length > 0;
60
+ }
61
+ /** The parts beside a trigger that has no aria-controls: its sibling regions, and the rest of its list item. */
62
+ function regionsBeside(button) {
63
+ const parent = button.parent;
64
+ if (!parent)
65
+ return [];
66
+ const regions = parent.children.filter((child) => child.kind === "element" && child !== button && child.tag !== "a" && child.tag !== "script" && child.tag !== "style");
67
+ const owner = closest(parent, (element) => element.tag === "li" || element.attrs["data-slot"] === "item");
68
+ if (owner && owner !== parent) {
69
+ for (const child of owner.children) {
70
+ if (child.kind === "element" && child !== parent && !isWithin(button, child) && child.tag !== "a" && child.tag !== "script")
71
+ regions.push(child);
72
+ }
73
+ }
74
+ return regions;
75
+ }
76
+ function anchorsIn(elements) {
77
+ return elements.flatMap((element) => findAll(element, (candidate) => candidate.tag === "a"));
78
+ }
79
+ /** Dropdown panels in the header: each needs real links in the server HTML. */
80
+ function inspectDropdowns(page) {
81
+ const issues = [];
82
+ const panels = [];
83
+ const seen = new Set();
84
+ let dropdowns = 0;
85
+ for (const scope of headerScopes(page)) {
86
+ for (const button of findAll(scope, (element) => isButtonLike(element) && !inDialog(element) && (controlledIds(element).length > 0 || isDisclosure(element)))) {
87
+ if (opensDialog(button, page))
88
+ continue;
89
+ const name = labelOf(button) || controlledIds(button).join(", ") || "dropdown";
90
+ const navItem = isNavItem(button);
91
+ const ids = controlledIds(button);
92
+ // A trigger with no aria-controls is a dropdown only as a menu item; a lone toggle (search) is not.
93
+ if (ids.length === 0 && !navItem)
94
+ continue;
95
+ const found = ids.map((id) => ({ id, panel: page.doc.byId.get(id) }));
96
+ const elements = ids.length > 0 ? found.flatMap((entry) => (entry.panel && !inDialog(entry.panel) ? [entry.panel] : [])) : regionsBeside(button);
97
+ const missing = found.filter((entry) => !entry.panel);
98
+ const droppedForDialog = ids.length > 0 && elements.length === 0 && missing.length === 0;
99
+ if (droppedForDialog)
100
+ continue;
101
+ // A disclosure that is not a nav item and holds no links is a search box or similar, not a dropdown.
102
+ if (!navItem && missing.length === 0 && anchorsIn(elements).length === 0 && !hasPseudoLinks(elements))
103
+ continue;
104
+ if (!navItem && missing.length > 0)
105
+ continue;
106
+ dropdowns += 1;
107
+ for (const entry of missing) {
108
+ issues.push({ path: page.path, message: `The ${name} menu on ${page.path} points at #${entry.id}, which is not in the page HTML, so its links are not in the server HTML.` });
109
+ }
110
+ for (const hub of hubAnchors(button).filter((anchor) => !isRealHref(anchor))) {
111
+ issues.push({ path: page.path, message: `The ${name} link beside its menu button on ${page.path} has no real address${textOf(hub) ? ` (${textOf(hub)})` : ""}.` });
112
+ }
113
+ if (ids.length === 0 || elements.length > 0)
114
+ panels.push({ name, elements });
115
+ for (const element of elements)
116
+ seen.add(element);
117
+ }
118
+ for (const panel of findAll(scope, (element) => element.attrs["data-slot"] === "panel" && !inDialog(element))) {
119
+ if (seen.has(panel))
120
+ continue;
121
+ dropdowns += 1;
122
+ const owner = closest(panel.parent ?? panel, (element) => element.tag === "li" || element.attrs["data-slot"] === "item");
123
+ const trigger = owner ? findFirst(owner, (element) => (element.tag === "a" || element.tag === "button") && !isWithin(element, panel)) : null;
124
+ panels.push({ name: (trigger ? labelOf(trigger) : "") || "dropdown", elements: [panel] });
125
+ seen.add(panel);
126
+ }
127
+ }
128
+ for (const { name, elements } of panels) {
129
+ const anchors = anchorsIn(elements);
130
+ const withoutAddress = anchors.filter((anchor) => !isRealHref(anchor));
131
+ const pseudoLinks = pseudoLinksIn(elements);
132
+ if (anchors.length === 0 && pseudoLinks.length === 0) {
133
+ issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has no links in the page HTML.` });
134
+ }
135
+ if (withoutAddress.length > 0) {
136
+ issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has ${withoutAddress.length} link${withoutAddress.length === 1 ? "" : "s"} with no real address (${withoutAddress.slice(0, 2).map((anchor) => textOf(anchor) || "no text").join(", ")}).` });
137
+ }
138
+ if (pseudoLinks.length > 0) {
139
+ issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has ${pseudoLinks.length} item${pseudoLinks.length === 1 ? " that looks" : "s that look"} like ${pseudoLinks.length === 1 ? "a link" : "links"} but ${pseudoLinks.length === 1 ? "is" : "are"} not an <a href> element.`, });
140
+ }
141
+ }
142
+ return { dropdowns, issues };
143
+ }
144
+ function pseudoLinksIn(elements) {
145
+ return elements.flatMap((element) => findAll(element, (candidate) => candidate.tag !== "a" && (candidate.attrs.role === "link" || candidate.attrs["data-href"] !== undefined || candidate.attrs["data-url"] !== undefined || candidate.attrs.onclick !== undefined)));
146
+ }
147
+ function hasPseudoLinks(elements) {
148
+ return pseudoLinksIn(elements).length > 0;
149
+ }
150
+ function summaryText(details) {
151
+ const summary = details.children.find((child) => child.kind === "element" && child.tag === "summary");
152
+ return summary ? textOf(summary) : "";
153
+ }
154
+ /** Accordion and tab panels: the content has to be in the HTML even when closed. */
155
+ function inspectPanels(page) {
156
+ const issues = [];
157
+ let panels = 0;
158
+ for (const details of findAll(page.body, (element) => element.tag === "details")) {
159
+ panels += 1;
160
+ const content = details.children.filter((child) => child.kind === "element" && child.tag !== "summary");
161
+ const rawText = details.children.filter((child) => child.kind === "text").map((child) => (child.kind === "text" ? child.text : "")).join("").trim();
162
+ if (rawText === "" && !content.some(hasContent)) {
163
+ const name = summaryText(details);
164
+ issues.push({ path: page.path, message: `The ${name ? `"${name}" ` : ""}accordion on ${page.path} has no content in the page HTML.` });
165
+ }
166
+ }
167
+ const scopes = headerScopes(page);
168
+ const inHeader = (element) => scopes.some((scope) => isWithin(element, scope));
169
+ for (const button of findAll(page.body, (element) => isButtonLike(element) && element.attrs["aria-expanded"] !== undefined && controlledIds(element).length > 0)) {
170
+ if (inHeader(button) || closest(button, (element) => element.tag === "nav"))
171
+ continue;
172
+ if (opensDialog(button, page) || (button.attrs["aria-haspopup"] ?? "false") !== "false")
173
+ continue;
174
+ if (controlledIds(button).every((id) => ON_DEMAND_ID.test(id)))
175
+ continue;
176
+ if (READ_MORE.test(textOf(button)) && closest(button, (element) => element.attrs["data-bl-section"] === "reviews"))
177
+ continue;
178
+ panels += 1;
179
+ const name = labelOf(button) || "accordion";
180
+ for (const id of controlledIds(button)) {
181
+ const panel = page.doc.byId.get(id);
182
+ if (!panel)
183
+ issues.push({ path: page.path, message: `The "${name}" accordion on ${page.path} points at #${id}, which is not in the page HTML.` });
184
+ else if (!hasContent(panel))
185
+ issues.push({ path: page.path, message: `The "${name}" accordion on ${page.path} has an empty panel in the page HTML.` });
186
+ }
187
+ }
188
+ for (const tab of findAll(page.body, (element) => element.attrs.role === "tab" && controlledIds(element).length > 0)) {
189
+ panels += 1;
190
+ const name = labelOf(tab) || "tab";
191
+ for (const id of controlledIds(tab)) {
192
+ const panel = page.doc.byId.get(id);
193
+ if (!panel)
194
+ issues.push({ path: page.path, message: `The "${name}" tab on ${page.path} points at #${id}, which is not in the page HTML.` });
195
+ else if (!hasContent(panel))
196
+ issues.push({ path: page.path, message: `The "${name}" tab on ${page.path} has an empty panel in the page HTML.` });
197
+ }
198
+ }
199
+ for (const panel of findAll(page.body, (element) => element.attrs.role === "tabpanel")) {
200
+ panels += 1;
201
+ if (!hasContent(panel))
202
+ issues.push({ path: page.path, message: `A tab panel on ${page.path} is empty in the page HTML.` });
203
+ }
204
+ return { panels, issues };
205
+ }
206
+ const ELLIPSIS_END = /[\s"'”’)]*(?:…|\.{3})[\s"'”’)]*$/;
207
+ const MIN_STEM = 40;
208
+ function comparable(text) {
209
+ return text.toLowerCase().replace(/^[\s"'“‘(]+/, "").replace(/\s+/g, " ");
210
+ }
211
+ /** Review text cut short with an ellipsis and no longer copy of it anywhere in the same reviews section. */
212
+ function inspectReviews(page) {
213
+ const issues = [];
214
+ let reviews = 0;
215
+ for (const section of findAll(page.body, (element) => element.attrs["data-bl-section"] === "reviews" || element.attrs["data-bl"] === "reviews")) {
216
+ const blocks = findAll(section, (element) => element.tag === "blockquote" || element.tag === "p" || element.attrs.itemprop === "reviewBody" || ["text", "body", "quote", "content", "review"].includes(element.attrs["data-slot"] ?? ""))
217
+ .map((element) => ({ element, text: textOf(element) }))
218
+ .filter((block) => block.text !== "");
219
+ reviews += blocks.length;
220
+ for (const block of blocks) {
221
+ if (!ELLIPSIS_END.test(block.text) || closest(block.element, (ancestor) => ancestor.attrs["data-bl-source"] === "feed"))
222
+ continue;
223
+ const stem = comparable(block.text.replace(ELLIPSIS_END, ""));
224
+ if (stem.length < MIN_STEM)
225
+ continue;
226
+ const hasFullCopy = blocks.some((other) => other !== block && other.text.length > block.text.length + 3 && comparable(other.text).startsWith(stem.slice(0, MIN_STEM)));
227
+ if (!hasFullCopy) {
228
+ issues.push({ path: page.path, message: `A review on ${page.path} is cut short ("${block.text.slice(0, 50)}…") with no full text in the page HTML.` });
229
+ }
230
+ }
231
+ for (const control of findAll(section, (element) => isButtonLike(element) && READ_MORE.test(textOf(element)) && controlledIds(element).length > 0)) {
232
+ for (const id of controlledIds(control)) {
233
+ if (!page.doc.byId.get(id))
234
+ issues.push({ path: page.path, message: `The "${textOf(control)}" control on ${page.path} points at #${id}, which is not in the page HTML.` });
235
+ }
236
+ }
237
+ }
238
+ return { panels: reviews, issues };
239
+ }
240
+ /** N2 and K7 over the sampled pages. */
241
+ export function checkNavigationCrawlability(pages) {
242
+ const dropdownIssues = [];
243
+ const contentIssues = [];
244
+ let dropdowns = 0;
245
+ let contentItems = 0;
246
+ for (const page of pages) {
247
+ const navigation = inspectDropdowns(page);
248
+ dropdowns += navigation.dropdowns;
249
+ dropdownIssues.push(...navigation.issues);
250
+ for (const result of [inspectPanels(page), inspectReviews(page)]) {
251
+ contentItems += result.panels;
252
+ contentIssues.push(...result.issues);
253
+ }
254
+ }
255
+ const unique = (issues) => {
256
+ const seen = new Set();
257
+ return issues.filter((issue) => !seen.has(issue.message) && seen.add(issue.message));
258
+ };
259
+ const n2 = dropdowns === 0
260
+ ? [notApplicableFinding("N2", "No dropdown menu was found in the header of the sampled pages, so there are no dropdown links to check.")]
261
+ : dropdownIssues.length > 0
262
+ ? unique(dropdownIssues).map((issue) => failFinding("N2", issue.path, issue.message))
263
+ : [passFinding("N2", `Every link in the ${dropdowns} header dropdown${dropdowns === 1 ? "" : "s"} across ${pages.length} page${pages.length === 1 ? "" : "s"} is an <a href> in the server HTML.`)];
264
+ const checked = dropdowns + contentItems;
265
+ const k7 = checked === 0
266
+ ? [notApplicableFinding("K7", "The sampled pages have no dropdowns, accordions, tabs or reviews to check.")]
267
+ : dropdownIssues.length + contentIssues.length > 0
268
+ ? unique([...dropdownIssues, ...contentIssues]).map((issue) => failFinding("K7", issue.path, issue.message))
269
+ : [passFinding("K7", `Dropdown links, accordion and tab panels and review text are all in the server HTML on ${pages.length} page${pages.length === 1 ? "" : "s"}.`)];
270
+ return [...n2, ...k7];
271
+ }
272
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/navigation.ts"],"names":[],"mappings":"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","sourcesContent":["import { closest, findAll, findFirst, hasContent, isWithin, textOf, type HtmlElement } from \"./html.js\";\nimport type { ShellPage } from \"./page.js\";\nimport { failFinding, notApplicableFinding, passFinding, type ShellFinding } from \"./types.js\";\n\n/**\n * N2 and K7: what a crawler reads without running scripts. A dropdown's links must be `<a href>` elements in the\n * server HTML (N2 and K7 both name them); K7 adds accordion and tab panels with content and review text that is not\n * cut short with no full copy in the HTML.\n */\n\nconst DRAWER_LABEL = /^(?:open |close |toggle |show |hide )?(?:the )?(?:main |mobile |site )?(?:menu|navigation|nav)$/i;\nconst DRAWER_HOOKS = new Set([\"menu-button\", \"menu-toggle\", \"drawer-toggle\", \"hamburger\"]);\n/** Controls that open something built on demand (a dialog, a popover, a lightbox), not content on the page. */\nconst ON_DEMAND_ID = /(?:dialog|modal|drawer|popover|lightbox|overlay)/i;\nconst READ_MORE = /^(?:read|show|see|view)\\s+(?:more|full|all)\\b/i;\n\nfunction isRealHref(anchor: HtmlElement): boolean {\n const href = anchor.attrs.href?.trim();\n return href !== undefined && href !== \"\" && href !== \"#\" && !/^(?:javascript|data|vbscript):/i.test(href);\n}\n\n/** The name of a disclosure button: \"Services menu\" and \"Open Services menu\" both give \"Services\". */\nfunction labelOf(button: HtmlElement): string {\n const own = button.attrs[\"aria-label\"]?.trim() || textOf(button);\n const sibling = button.parent?.children.find((child): child is HtmlElement => child.kind === \"element\" && child.tag === \"a\");\n const label = own || (sibling ? textOf(sibling) : \"\");\n return label.replace(/^(?:open|toggle|show|hide|close)\\s+/i, \"\").replace(/\\s+(?:menu|submenu|dropdown)$/i, \"\").trim();\n}\n\nfunction controlledIds(button: HtmlElement): string[] {\n return (button.attrs[\"aria-controls\"] ?? \"\").split(/\\s+/).filter(Boolean);\n}\n\nfunction isButtonLike(element: HtmlElement): boolean {\n return element.tag === \"button\" || element.attrs.role === \"button\";\n}\n\nfunction opensDialog(button: HtmlElement, page: ShellPage): boolean {\n if (button.attrs[\"aria-haspopup\"] === \"dialog\") return true;\n const label = (button.attrs[\"aria-label\"] ?? textOf(button)).trim();\n if (DRAWER_LABEL.test(label)) return true;\n if (DRAWER_HOOKS.has(button.attrs[\"data-bl\"] ?? \"\") || DRAWER_HOOKS.has(button.attrs[\"data-slot\"] ?? \"\")) return true;\n const target = controlledIds(button).map((id) => page.doc.byId.get(id)).find(Boolean);\n return target !== undefined && (target.tag === \"dialog\" || /^(?:dialog|alertdialog)$/.test(target.attrs.role ?? \"\") || target.attrs[\"data-bl\"] === \"drawer\");\n}\n\ninterface Issue {\n path: string;\n message: string;\n}\n\ninterface NavigationResult {\n dropdowns: number;\n issues: Issue[];\n}\n\nfunction headerScopes(page: ShellPage): HtmlElement[] {\n if (page.headers.length > 0) return [...page.headers];\n return findAll(page.body, (element) => element.tag === \"nav\" && !page.footers.some((footer) => isWithin(element, footer)));\n}\n\nfunction isDisclosure(element: HtmlElement): boolean {\n const popup = element.attrs[\"aria-haspopup\"];\n return element.attrs[\"aria-expanded\"] !== undefined || (popup !== undefined && popup !== \"false\" && popup !== \"dialog\");\n}\n\n/** Anything inside a <dialog> (the mobile drawer) is not a header dropdown. */\nfunction inDialog(element: HtmlElement): boolean {\n return closest(element, (ancestor) => ancestor.tag === \"dialog\" || /^(?:dialog|alertdialog)$/.test(ancestor.attrs.role ?? \"\") || ancestor.attrs[\"aria-modal\"] === \"true\") !== null;\n}\n\nfunction hubAnchors(button: HtmlElement): HtmlElement[] {\n return (button.parent?.children ?? []).filter((child): child is HtmlElement => child.kind === \"element\" && child.tag === \"a\");\n}\n\n/** A menu item: inside the header's <nav>, or beside its own link (a split trigger). A search toggle is neither. */\nfunction isNavItem(button: HtmlElement): boolean {\n return closest(button, (ancestor) => ancestor.tag === \"nav\") !== null || hubAnchors(button).length > 0;\n}\n\n/** The parts beside a trigger that has no aria-controls: its sibling regions, and the rest of its list item. */\nfunction regionsBeside(button: HtmlElement): HtmlElement[] {\n const parent = button.parent;\n if (!parent) return [];\n const regions = parent.children.filter((child): child is HtmlElement =>\n child.kind === \"element\" && child !== button && child.tag !== \"a\" && child.tag !== \"script\" && child.tag !== \"style\");\n const owner = closest(parent, (element) => element.tag === \"li\" || element.attrs[\"data-slot\"] === \"item\");\n if (owner && owner !== parent) {\n for (const child of owner.children) {\n if (child.kind === \"element\" && child !== parent && !isWithin(button, child) && child.tag !== \"a\" && child.tag !== \"script\") regions.push(child);\n }\n }\n return regions;\n}\n\ninterface Panel {\n name: string;\n /** The elements that hold the dropdown's links; a trigger without aria-controls has the regions beside it. */\n elements: HtmlElement[];\n}\n\nfunction anchorsIn(elements: readonly HtmlElement[]): HtmlElement[] {\n return elements.flatMap((element) => findAll(element, (candidate) => candidate.tag === \"a\"));\n}\n\n/** Dropdown panels in the header: each needs real links in the server HTML. */\nfunction inspectDropdowns(page: ShellPage): NavigationResult {\n const issues: Issue[] = [];\n const panels: Panel[] = [];\n const seen = new Set<HtmlElement>();\n let dropdowns = 0;\n for (const scope of headerScopes(page)) {\n for (const button of findAll(scope, (element) => isButtonLike(element) && !inDialog(element) && (controlledIds(element).length > 0 || isDisclosure(element)))) {\n if (opensDialog(button, page)) continue;\n const name = labelOf(button) || controlledIds(button).join(\", \") || \"dropdown\";\n const navItem = isNavItem(button);\n const ids = controlledIds(button);\n // A trigger with no aria-controls is a dropdown only as a menu item; a lone toggle (search) is not.\n if (ids.length === 0 && !navItem) continue;\n const found = ids.map((id) => ({ id, panel: page.doc.byId.get(id) }));\n const elements = ids.length > 0 ? found.flatMap((entry) => (entry.panel && !inDialog(entry.panel) ? [entry.panel] : [])) : regionsBeside(button);\n const missing = found.filter((entry) => !entry.panel);\n const droppedForDialog = ids.length > 0 && elements.length === 0 && missing.length === 0;\n if (droppedForDialog) continue;\n // A disclosure that is not a nav item and holds no links is a search box or similar, not a dropdown.\n if (!navItem && missing.length === 0 && anchorsIn(elements).length === 0 && !hasPseudoLinks(elements)) continue;\n if (!navItem && missing.length > 0) continue;\n dropdowns += 1;\n for (const entry of missing) {\n issues.push({ path: page.path, message: `The ${name} menu on ${page.path} points at #${entry.id}, which is not in the page HTML, so its links are not in the server HTML.` });\n }\n for (const hub of hubAnchors(button).filter((anchor) => !isRealHref(anchor))) {\n issues.push({ path: page.path, message: `The ${name} link beside its menu button on ${page.path} has no real address${textOf(hub) ? ` (${textOf(hub)})` : \"\"}.` });\n }\n if (ids.length === 0 || elements.length > 0) panels.push({ name, elements });\n for (const element of elements) seen.add(element);\n }\n for (const panel of findAll(scope, (element) => element.attrs[\"data-slot\"] === \"panel\" && !inDialog(element))) {\n if (seen.has(panel)) continue;\n dropdowns += 1;\n const owner = closest(panel.parent ?? panel, (element) => element.tag === \"li\" || element.attrs[\"data-slot\"] === \"item\");\n const trigger = owner ? findFirst(owner, (element) => (element.tag === \"a\" || element.tag === \"button\") && !isWithin(element, panel)) : null;\n panels.push({ name: (trigger ? labelOf(trigger) : \"\") || \"dropdown\", elements: [panel] });\n seen.add(panel);\n }\n }\n for (const { name, elements } of panels) {\n const anchors = anchorsIn(elements);\n const withoutAddress = anchors.filter((anchor) => !isRealHref(anchor));\n const pseudoLinks = pseudoLinksIn(elements);\n if (anchors.length === 0 && pseudoLinks.length === 0) {\n issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has no links in the page HTML.` });\n }\n if (withoutAddress.length > 0) {\n issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has ${withoutAddress.length} link${withoutAddress.length === 1 ? \"\" : \"s\"} with no real address (${withoutAddress.slice(0, 2).map((anchor) => textOf(anchor) || \"no text\").join(\", \")}).` });\n }\n if (pseudoLinks.length > 0) {\n issues.push({ path: page.path, message: `The ${name} menu on ${page.path} has ${pseudoLinks.length} item${pseudoLinks.length === 1 ? \" that looks\" : \"s that look\"} like ${pseudoLinks.length === 1 ? \"a link\" : \"links\"} but ${pseudoLinks.length === 1 ? \"is\" : \"are\"} not an <a href> element.`, });\n }\n }\n return { dropdowns, issues };\n}\n\nfunction pseudoLinksIn(elements: readonly HtmlElement[]): HtmlElement[] {\n return elements.flatMap((element) => findAll(element, (candidate) =>\n candidate.tag !== \"a\" && (candidate.attrs.role === \"link\" || candidate.attrs[\"data-href\"] !== undefined || candidate.attrs[\"data-url\"] !== undefined || candidate.attrs.onclick !== undefined)));\n}\n\nfunction hasPseudoLinks(elements: readonly HtmlElement[]): boolean {\n return pseudoLinksIn(elements).length > 0;\n}\n\nfunction summaryText(details: HtmlElement): string {\n const summary = details.children.find((child): child is HtmlElement => child.kind === \"element\" && child.tag === \"summary\");\n return summary ? textOf(summary) : \"\";\n}\n\ninterface ContentResult {\n panels: number;\n issues: Issue[];\n}\n\n/** Accordion and tab panels: the content has to be in the HTML even when closed. */\nfunction inspectPanels(page: ShellPage): ContentResult {\n const issues: Issue[] = [];\n let panels = 0;\n for (const details of findAll(page.body, (element) => element.tag === \"details\")) {\n panels += 1;\n const content = details.children.filter((child): child is HtmlElement => child.kind === \"element\" && child.tag !== \"summary\");\n const rawText = details.children.filter((child) => child.kind === \"text\").map((child) => (child.kind === \"text\" ? child.text : \"\")).join(\"\").trim();\n if (rawText === \"\" && !content.some(hasContent)) {\n const name = summaryText(details);\n issues.push({ path: page.path, message: `The ${name ? `\"${name}\" ` : \"\"}accordion on ${page.path} has no content in the page HTML.` });\n }\n }\n const scopes = headerScopes(page);\n const inHeader = (element: HtmlElement) => scopes.some((scope) => isWithin(element, scope));\n for (const button of findAll(page.body, (element) => isButtonLike(element) && element.attrs[\"aria-expanded\"] !== undefined && controlledIds(element).length > 0)) {\n if (inHeader(button) || closest(button, (element) => element.tag === \"nav\")) continue;\n if (opensDialog(button, page) || (button.attrs[\"aria-haspopup\"] ?? \"false\") !== \"false\") continue;\n if (controlledIds(button).every((id) => ON_DEMAND_ID.test(id))) continue;\n if (READ_MORE.test(textOf(button)) && closest(button, (element) => element.attrs[\"data-bl-section\"] === \"reviews\")) continue;\n panels += 1;\n const name = labelOf(button) || \"accordion\";\n for (const id of controlledIds(button)) {\n const panel = page.doc.byId.get(id);\n if (!panel) issues.push({ path: page.path, message: `The \"${name}\" accordion on ${page.path} points at #${id}, which is not in the page HTML.` });\n else if (!hasContent(panel)) issues.push({ path: page.path, message: `The \"${name}\" accordion on ${page.path} has an empty panel in the page HTML.` });\n }\n }\n for (const tab of findAll(page.body, (element) => element.attrs.role === \"tab\" && controlledIds(element).length > 0)) {\n panels += 1;\n const name = labelOf(tab) || \"tab\";\n for (const id of controlledIds(tab)) {\n const panel = page.doc.byId.get(id);\n if (!panel) issues.push({ path: page.path, message: `The \"${name}\" tab on ${page.path} points at #${id}, which is not in the page HTML.` });\n else if (!hasContent(panel)) issues.push({ path: page.path, message: `The \"${name}\" tab on ${page.path} has an empty panel in the page HTML.` });\n }\n }\n for (const panel of findAll(page.body, (element) => element.attrs.role === \"tabpanel\")) {\n panels += 1;\n if (!hasContent(panel)) issues.push({ path: page.path, message: `A tab panel on ${page.path} is empty in the page HTML.` });\n }\n return { panels, issues };\n}\n\nconst ELLIPSIS_END = /[\\s\"'”’)]*(?:…|\\.{3})[\\s\"'”’)]*$/;\nconst MIN_STEM = 40;\n\nfunction comparable(text: string): string {\n return text.toLowerCase().replace(/^[\\s\"'“‘(]+/, \"\").replace(/\\s+/g, \" \");\n}\n\n/** Review text cut short with an ellipsis and no longer copy of it anywhere in the same reviews section. */\nfunction inspectReviews(page: ShellPage): ContentResult {\n const issues: Issue[] = [];\n let reviews = 0;\n for (const section of findAll(page.body, (element) => element.attrs[\"data-bl-section\"] === \"reviews\" || element.attrs[\"data-bl\"] === \"reviews\")) {\n const blocks = findAll(section, (element) =>\n element.tag === \"blockquote\" || element.tag === \"p\" || element.attrs.itemprop === \"reviewBody\" || [\"text\", \"body\", \"quote\", \"content\", \"review\"].includes(element.attrs[\"data-slot\"] ?? \"\"))\n .map((element) => ({ element, text: textOf(element) }))\n .filter((block) => block.text !== \"\");\n reviews += blocks.length;\n for (const block of blocks) {\n if (!ELLIPSIS_END.test(block.text) || closest(block.element, (ancestor) => ancestor.attrs[\"data-bl-source\"] === \"feed\")) continue;\n const stem = comparable(block.text.replace(ELLIPSIS_END, \"\"));\n if (stem.length < MIN_STEM) continue;\n const hasFullCopy = blocks.some((other) => other !== block && other.text.length > block.text.length + 3 && comparable(other.text).startsWith(stem.slice(0, MIN_STEM)));\n if (!hasFullCopy) {\n issues.push({ path: page.path, message: `A review on ${page.path} is cut short (\"${block.text.slice(0, 50)}…\") with no full text in the page HTML.` });\n }\n }\n for (const control of findAll(section, (element) => isButtonLike(element) && READ_MORE.test(textOf(element)) && controlledIds(element).length > 0)) {\n for (const id of controlledIds(control)) {\n if (!page.doc.byId.get(id)) issues.push({ path: page.path, message: `The \"${textOf(control)}\" control on ${page.path} points at #${id}, which is not in the page HTML.` });\n }\n }\n }\n return { panels: reviews, issues };\n}\n\n/** N2 and K7 over the sampled pages. */\nexport function checkNavigationCrawlability(pages: readonly ShellPage[]): ShellFinding[] {\n const dropdownIssues: Issue[] = [];\n const contentIssues: Issue[] = [];\n let dropdowns = 0;\n let contentItems = 0;\n for (const page of pages) {\n const navigation = inspectDropdowns(page);\n dropdowns += navigation.dropdowns;\n dropdownIssues.push(...navigation.issues);\n for (const result of [inspectPanels(page), inspectReviews(page)]) {\n contentItems += result.panels;\n contentIssues.push(...result.issues);\n }\n }\n const unique = (issues: readonly Issue[]) => {\n const seen = new Set<string>();\n return issues.filter((issue) => !seen.has(issue.message) && seen.add(issue.message));\n };\n\n const n2: ShellFinding[] = dropdowns === 0\n ? [notApplicableFinding(\"N2\", \"No dropdown menu was found in the header of the sampled pages, so there are no dropdown links to check.\")]\n : dropdownIssues.length > 0\n ? unique(dropdownIssues).map((issue) => failFinding(\"N2\", issue.path, issue.message))\n : [passFinding(\"N2\", `Every link in the ${dropdowns} header dropdown${dropdowns === 1 ? \"\" : \"s\"} across ${pages.length} page${pages.length === 1 ? \"\" : \"s\"} is an <a href> in the server HTML.`)];\n\n const checked = dropdowns + contentItems;\n const k7: ShellFinding[] = checked === 0\n ? [notApplicableFinding(\"K7\", \"The sampled pages have no dropdowns, accordions, tabs or reviews to check.\")]\n : dropdownIssues.length + contentIssues.length > 0\n ? unique([...dropdownIssues, ...contentIssues]).map((issue) => failFinding(\"K7\", issue.path, issue.message))\n : [passFinding(\"K7\", `Dropdown links, accordion and tab panels and review text are all in the server HTML on ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}.`)];\n return [...n2, ...k7];\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { ShellPage } from "./page.js";
2
+ import { type ShellFinding } from "./types.js";
3
+ export declare function checkNoInterstitial(pages: readonly ShellPage[]): ShellFinding[];
4
+ //# sourceMappingURL=overlays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/overlays.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAoEvF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,YAAY,EAAE,CAkB/E"}
@@ -0,0 +1,93 @@
1
+ import { classTokens, findAll, selfAndAncestors } from "./html.js";
2
+ import { failFinding, passFinding, sampledPages } from "./types.js";
3
+ /**
4
+ * C9: no interstitial overlay in the first screen. The server HTML holds what shows on load, so an open modal dialog,
5
+ * an open popup or a fixed full-screen cover in it covers the first screen. A closed drawer or dialog (`hidden`,
6
+ * `inert`, `data-state="closed"`, a `<dialog>` without `open`) does not.
7
+ */
8
+ const OVERLAY_NAME = /(?:^|[\s_-])(?:pop-?ups?|modals?|interstitials?|lightbox|splash)(?:$|[\s_-])|popup|modal|interstitial|lightbox/i;
9
+ const OPEN_CLASSES = new Set(["open", "opened", "is-open", "show", "shown", "is-shown", "visible", "is-visible", "active", "is-active", "in", "displayed"]);
10
+ const HIDDEN_CLASSES = new Set(["hidden", "invisible", "sr-only", "closed", "is-closed", "d-none", "collapse"]);
11
+ /** Utility classes that move a panel off screen or make it unclickable until it opens. */
12
+ const OFF_SCREEN_CLASS = /^(?:-?translate-[xy]-full|opacity-0|pointer-events-none|scale-0)$/;
13
+ const FULL_SCREEN_STYLE = /position\s*:\s*(?:fixed|sticky)/i;
14
+ function styleHides(style) {
15
+ return /display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)/i.test(style);
16
+ }
17
+ /** Whether the element, or an ancestor, is hidden or inert in the server HTML. */
18
+ function isHiddenOnLoad(element) {
19
+ for (const candidate of selfAndAncestors(element)) {
20
+ const { attrs } = candidate;
21
+ if (attrs.hidden !== undefined || attrs.inert !== undefined)
22
+ return true;
23
+ if (attrs["aria-hidden"] === "true" && candidate === element)
24
+ return true;
25
+ if (/^(?:closed|hidden)$/.test(attrs["data-state"] ?? ""))
26
+ return true;
27
+ if (attrs["data-open"] === "false" || (attrs["aria-expanded"] === "false" && candidate === element))
28
+ return true;
29
+ if (candidate.tag === "dialog" && attrs.open === undefined)
30
+ return true;
31
+ if (styleHides(attrs.style ?? ""))
32
+ return true;
33
+ if (classTokens(candidate).some((token) => HIDDEN_CLASSES.has(token) || OFF_SCREEN_CLASS.test(token)))
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ function opensOnLoad(element) {
39
+ const { attrs } = element;
40
+ return attrs.open !== undefined || attrs["data-state"] === "open" || attrs["data-open"] === "true" || attrs["aria-hidden"] === "false" ||
41
+ attrs["data-visible"] === "true" || classTokens(element).some((token) => OPEN_CLASSES.has(token));
42
+ }
43
+ function namedLikeOverlay(element) {
44
+ return [attrsOf(element, "id"), attrsOf(element, "class"), attrsOf(element, "data-bl"), attrsOf(element, "data-slot"), attrsOf(element, "aria-label")]
45
+ .some((value) => OVERLAY_NAME.test(value));
46
+ }
47
+ function attrsOf(element, name) {
48
+ return element.attrs[name] ?? "";
49
+ }
50
+ /** Tailwind-style utilities that make a full-screen fixed cover. */
51
+ function coversScreen(element) {
52
+ const tokens = new Set(classTokens(element));
53
+ const fixed = tokens.has("fixed") || tokens.has("sticky");
54
+ const inset = tokens.has("inset-0") || (tokens.has("top-0") && tokens.has("left-0") && tokens.has("right-0") && tokens.has("bottom-0"));
55
+ const sized = (tokens.has("w-screen") || tokens.has("w-full") || tokens.has("w-dvw")) && (tokens.has("h-screen") || tokens.has("h-full") || tokens.has("h-dvh") || tokens.has("min-h-screen"));
56
+ if (fixed && (inset || sized))
57
+ return true;
58
+ const style = element.attrs.style ?? "";
59
+ if (!FULL_SCREEN_STYLE.test(style))
60
+ return false;
61
+ const insetZero = /inset\s*:\s*0(?:px)?\b/i.test(style) || (/top\s*:\s*0/i.test(style) && /left\s*:\s*0/i.test(style) && /right\s*:\s*0/i.test(style) && /bottom\s*:\s*0/i.test(style));
62
+ const full = /width\s*:\s*100(?:vw|dvw|%)/i.test(style) && /(?:min-)?height\s*:\s*100(?:vh|dvh|%)/i.test(style);
63
+ return insetZero || full;
64
+ }
65
+ function describe(element) {
66
+ const name = element.attrs["aria-label"] || element.attrs.id || element.attrs["data-bl"] || classTokens(element)[0] || element.tag;
67
+ return `${element.tag} "${name}"`;
68
+ }
69
+ export function checkNoInterstitial(pages) {
70
+ const findings = [];
71
+ for (const page of pages) {
72
+ const reasons = new Map();
73
+ for (const element of findAll(page.body, () => true)) {
74
+ if (isHiddenOnLoad(element))
75
+ continue;
76
+ if (element.attrs["aria-modal"] === "true")
77
+ reasons.set(element, "a modal dialog that is open on load");
78
+ else if (element.tag === "dialog" && element.attrs.open !== undefined)
79
+ reasons.set(element, "a dialog that is open on load");
80
+ else if (namedLikeOverlay(element) && opensOnLoad(element) && !(element.tag === "button" || element.tag === "a"))
81
+ reasons.set(element, "a popup that is open on load");
82
+ else if (coversScreen(element))
83
+ reasons.set(element, "a fixed full-screen cover");
84
+ }
85
+ // Report the outermost cover only: its children are part of it.
86
+ const outer = [...reasons.entries()].filter(([element]) => ![...selfAndAncestors(element)].slice(1).some((ancestor) => reasons.has(ancestor)));
87
+ for (const [element, reason] of outer.slice(0, 3)) {
88
+ findings.push(failFinding("C9", page.path, `The page ${page.path} loads with ${reason} (${describe(element)}), which covers the first screen.`));
89
+ }
90
+ }
91
+ return findings.length > 0 ? findings : [passFinding("C9", `No interstitial overlay is open on load on ${sampledPages(pages.length)}.`)];
92
+ }
93
+ //# sourceMappingURL=overlays.js.map