@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.
- package/README.md +26 -0
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.js +4 -2
- package/dist/engineering.js.map +1 -1
- package/dist/public-copy.d.ts +7 -0
- package/dist/public-copy.d.ts.map +1 -1
- package/dist/public-copy.js +8 -3
- package/dist/public-copy.js.map +1 -1
- package/dist/ready-cli.d.ts +38 -0
- package/dist/ready-cli.d.ts.map +1 -0
- package/dist/ready-cli.js +327 -0
- package/dist/ready-cli.js.map +1 -0
- package/dist/site-system/checks.d.ts +389 -0
- package/dist/site-system/checks.d.ts.map +1 -0
- package/dist/site-system/checks.js +96 -0
- package/dist/site-system/checks.js.map +1 -0
- package/dist/site-system/config.d.ts +40 -0
- package/dist/site-system/config.d.ts.map +1 -0
- package/dist/site-system/config.js +214 -0
- package/dist/site-system/config.js.map +1 -0
- package/dist/site-system/index.d.ts +14 -0
- package/dist/site-system/index.d.ts.map +1 -0
- package/dist/site-system/index.js +14 -0
- package/dist/site-system/index.js.map +1 -0
- package/dist/site-system/journeys/access.d.ts +7 -0
- package/dist/site-system/journeys/access.d.ts.map +1 -0
- package/dist/site-system/journeys/access.js +263 -0
- package/dist/site-system/journeys/access.js.map +1 -0
- package/dist/site-system/journeys/browser.d.ts +79 -0
- package/dist/site-system/journeys/browser.d.ts.map +1 -0
- package/dist/site-system/journeys/browser.js +218 -0
- package/dist/site-system/journeys/browser.js.map +1 -0
- package/dist/site-system/journeys/conversion.d.ts +3 -0
- package/dist/site-system/journeys/conversion.d.ts.map +1 -0
- package/dist/site-system/journeys/conversion.js +152 -0
- package/dist/site-system/journeys/conversion.js.map +1 -0
- package/dist/site-system/journeys/menu.d.ts +11 -0
- package/dist/site-system/journeys/menu.d.ts.map +1 -0
- package/dist/site-system/journeys/menu.js +395 -0
- package/dist/site-system/journeys/menu.js.map +1 -0
- package/dist/site-system/journeys/page-helpers.d.ts +146 -0
- package/dist/site-system/journeys/page-helpers.d.ts.map +1 -0
- package/dist/site-system/journeys/page-helpers.js +426 -0
- package/dist/site-system/journeys/page-helpers.js.map +1 -0
- package/dist/site-system/journeys/plan.d.ts +82 -0
- package/dist/site-system/journeys/plan.d.ts.map +1 -0
- package/dist/site-system/journeys/plan.js +112 -0
- package/dist/site-system/journeys/plan.js.map +1 -0
- package/dist/site-system/journeys/shell.d.ts +24 -0
- package/dist/site-system/journeys/shell.d.ts.map +1 -0
- package/dist/site-system/journeys/shell.js +269 -0
- package/dist/site-system/journeys/shell.js.map +1 -0
- package/dist/site-system/playwright-report.d.ts +22 -0
- package/dist/site-system/playwright-report.d.ts.map +1 -0
- package/dist/site-system/playwright-report.js +131 -0
- package/dist/site-system/playwright-report.js.map +1 -0
- package/dist/site-system/records.d.ts +30 -0
- package/dist/site-system/records.d.ts.map +1 -0
- package/dist/site-system/records.js +98 -0
- package/dist/site-system/records.js.map +1 -0
- package/dist/site-system/report.d.ts +116 -0
- package/dist/site-system/report.d.ts.map +1 -0
- package/dist/site-system/report.js +366 -0
- package/dist/site-system/report.js.map +1 -0
- package/dist/site-system/rule-modules.d.ts +8 -0
- package/dist/site-system/rule-modules.d.ts.map +1 -0
- package/dist/site-system/rule-modules.js +33 -0
- package/dist/site-system/rule-modules.js.map +1 -0
- package/dist/site-system/rules/design/d4-distinctness.d.ts +36 -0
- package/dist/site-system/rules/design/d4-distinctness.d.ts.map +1 -0
- package/dist/site-system/rules/design/d4-distinctness.js +207 -0
- package/dist/site-system/rules/design/d4-distinctness.js.map +1 -0
- package/dist/site-system/rules/design/index.d.ts +17 -0
- package/dist/site-system/rules/design/index.d.ts.map +1 -0
- package/dist/site-system/rules/design/index.js +7 -0
- package/dist/site-system/rules/design/index.js.map +1 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.d.ts +31 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.js +211 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.js.map +1 -0
- package/dist/site-system/rules/enquiry/common.d.ts +37 -0
- package/dist/site-system/rules/enquiry/common.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/common.js +59 -0
- package/dist/site-system/rules/enquiry/common.js.map +1 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts +8 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.js +59 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.js.map +1 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts +22 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.js +55 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.js.map +1 -0
- package/dist/site-system/rules/enquiry/html.d.ts +53 -0
- package/dist/site-system/rules/enquiry/html.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/html.js +294 -0
- package/dist/site-system/rules/enquiry/html.js.map +1 -0
- package/dist/site-system/rules/enquiry/index.d.ts +17 -0
- package/dist/site-system/rules/enquiry/index.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/index.js +26 -0
- package/dist/site-system/rules/enquiry/index.js.map +1 -0
- package/dist/site-system/rules/enquiry/k8-sections.d.ts +17 -0
- package/dist/site-system/rules/enquiry/k8-sections.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/k8-sections.js +76 -0
- package/dist/site-system/rules/enquiry/k8-sections.js.map +1 -0
- package/dist/site-system/rules/enquiry/literal.d.ts +29 -0
- package/dist/site-system/rules/enquiry/literal.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/literal.js +189 -0
- package/dist/site-system/rules/enquiry/literal.js.map +1 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts +73 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.js +275 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.js.map +1 -0
- package/dist/site-system/rules/enquiry/t5-reviews.d.ts +23 -0
- package/dist/site-system/rules/enquiry/t5-reviews.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/t5-reviews.js +91 -0
- package/dist/site-system/rules/enquiry/t5-reviews.js.map +1 -0
- package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
- package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
- package/dist/site-system/rules/form/c11-contact-route.js +132 -0
- package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.js +67 -0
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
- package/dist/site-system/rules/form/index.d.ts +14 -0
- package/dist/site-system/rules/form/index.d.ts.map +1 -0
- package/dist/site-system/rules/form/index.js +13 -0
- package/dist/site-system/rules/form/index.js.map +1 -0
- package/dist/site-system/rules/library/common.d.ts +36 -0
- package/dist/site-system/rules/library/common.d.ts.map +1 -0
- package/dist/site-system/rules/library/common.js +70 -0
- package/dist/site-system/rules/library/common.js.map +1 -0
- package/dist/site-system/rules/library/index.d.ts +16 -0
- package/dist/site-system/rules/library/index.d.ts.map +1 -0
- package/dist/site-system/rules/library/index.js +19 -0
- package/dist/site-system/rules/library/index.js.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.js +97 -0
- package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
- package/dist/site-system/rules/navigation/extract.d.ts +50 -0
- package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/extract.js +220 -0
- package/dist/site-system/rules/navigation/extract.js.map +1 -0
- package/dist/site-system/rules/navigation/html.d.ts +23 -0
- package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/html.js +219 -0
- package/dist/site-system/rules/navigation/html.js.map +1 -0
- package/dist/site-system/rules/navigation/index.d.ts +37 -0
- package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/index.js +163 -0
- package/dist/site-system/rules/navigation/index.js.map +1 -0
- package/dist/site-system/rules/navigation/record.d.ts +8 -0
- package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/record.js +39 -0
- package/dist/site-system/rules/navigation/record.js.map +1 -0
- package/dist/site-system/rules/sections/exif.d.ts +22 -0
- package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
- package/dist/site-system/rules/sections/exif.js +218 -0
- package/dist/site-system/rules/sections/exif.js.map +1 -0
- package/dist/site-system/rules/sections/images.d.ts +31 -0
- package/dist/site-system/rules/sections/images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/images.js +96 -0
- package/dist/site-system/rules/sections/images.js.map +1 -0
- package/dist/site-system/rules/sections/index.d.ts +20 -0
- package/dist/site-system/rules/sections/index.d.ts.map +1 -0
- package/dist/site-system/rules/sections/index.js +34 -0
- package/dist/site-system/rules/sections/index.js.map +1 -0
- package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
- package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k11-density.js +88 -0
- package/dist/site-system/rules/sections/k11-density.js.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.js +70 -0
- package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.js +186 -0
- package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.js +41 -0
- package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.js +92 -0
- package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
- package/dist/site-system/rules/shell/banned-wording.d.ts +24 -0
- package/dist/site-system/rules/shell/banned-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/banned-wording.js +212 -0
- package/dist/site-system/rules/shell/banned-wording.js.map +1 -0
- package/dist/site-system/rules/shell/budgets.d.ts +43 -0
- package/dist/site-system/rules/shell/budgets.d.ts.map +1 -0
- package/dist/site-system/rules/shell/budgets.js +297 -0
- package/dist/site-system/rules/shell/budgets.js.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts +17 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.js +86 -0
- package/dist/site-system/rules/shell/cta-labels.js.map +1 -0
- package/dist/site-system/rules/shell/facts.d.ts +33 -0
- package/dist/site-system/rules/shell/facts.d.ts.map +1 -0
- package/dist/site-system/rules/shell/facts.js +217 -0
- package/dist/site-system/rules/shell/facts.js.map +1 -0
- package/dist/site-system/rules/shell/headings.d.ts +5 -0
- package/dist/site-system/rules/shell/headings.d.ts.map +1 -0
- package/dist/site-system/rules/shell/headings.js +23 -0
- package/dist/site-system/rules/shell/headings.js.map +1 -0
- package/dist/site-system/rules/shell/hero-location.d.ts +4 -0
- package/dist/site-system/rules/shell/hero-location.d.ts.map +1 -0
- package/dist/site-system/rules/shell/hero-location.js +45 -0
- package/dist/site-system/rules/shell/hero-location.js.map +1 -0
- package/dist/site-system/rules/shell/html.d.ts +53 -0
- package/dist/site-system/rules/shell/html.d.ts.map +1 -0
- package/dist/site-system/rules/shell/html.js +341 -0
- package/dist/site-system/rules/shell/html.js.map +1 -0
- package/dist/site-system/rules/shell/index.d.ts +18 -0
- package/dist/site-system/rules/shell/index.d.ts.map +1 -0
- package/dist/site-system/rules/shell/index.js +65 -0
- package/dist/site-system/rules/shell/index.js.map +1 -0
- package/dist/site-system/rules/shell/licence.d.ts +4 -0
- package/dist/site-system/rules/shell/licence.d.ts.map +1 -0
- package/dist/site-system/rules/shell/licence.js +60 -0
- package/dist/site-system/rules/shell/licence.js.map +1 -0
- package/dist/site-system/rules/shell/location-wording.d.ts +13 -0
- package/dist/site-system/rules/shell/location-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/location-wording.js +15 -0
- package/dist/site-system/rules/shell/location-wording.js.map +1 -0
- package/dist/site-system/rules/shell/navigation.d.ts +5 -0
- package/dist/site-system/rules/shell/navigation.d.ts.map +1 -0
- package/dist/site-system/rules/shell/navigation.js +272 -0
- package/dist/site-system/rules/shell/navigation.js.map +1 -0
- package/dist/site-system/rules/shell/overlays.d.ts +4 -0
- package/dist/site-system/rules/shell/overlays.d.ts.map +1 -0
- package/dist/site-system/rules/shell/overlays.js +93 -0
- package/dist/site-system/rules/shell/overlays.js.map +1 -0
- package/dist/site-system/rules/shell/page.d.ts +51 -0
- package/dist/site-system/rules/shell/page.d.ts.map +1 -0
- package/dist/site-system/rules/shell/page.js +256 -0
- package/dist/site-system/rules/shell/page.js.map +1 -0
- package/dist/site-system/rules/shell/phone.d.ts +13 -0
- package/dist/site-system/rules/shell/phone.d.ts.map +1 -0
- package/dist/site-system/rules/shell/phone.js +99 -0
- package/dist/site-system/rules/shell/phone.js.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts +7 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.js +81 -0
- package/dist/site-system/rules/shell/proof-numbers.js.map +1 -0
- package/dist/site-system/rules/shell/types.d.ts +80 -0
- package/dist/site-system/rules/shell/types.d.ts.map +1 -0
- package/dist/site-system/rules/shell/types.js +17 -0
- package/dist/site-system/rules/shell/types.js.map +1 -0
- package/dist/site-system/runners.d.ts +37 -0
- package/dist/site-system/runners.d.ts.map +1 -0
- package/dist/site-system/runners.js +48 -0
- package/dist/site-system/runners.js.map +1 -0
- package/dist/site-system/samples.d.ts +43 -0
- package/dist/site-system/samples.d.ts.map +1 -0
- package/dist/site-system/samples.js +99 -0
- package/dist/site-system/samples.js.map +1 -0
- package/dist/site-system/steps.d.ts +66 -0
- package/dist/site-system/steps.d.ts.map +1 -0
- package/dist/site-system/steps.js +45 -0
- package/dist/site-system/steps.js.map +1 -0
- package/dist/site-system/streamed-html.d.ts +9 -0
- package/dist/site-system/streamed-html.d.ts.map +1 -0
- package/dist/site-system/streamed-html.js +43 -0
- package/dist/site-system/streamed-html.js.map +1 -0
- package/dist/site-system-e2e.d.ts +6 -0
- package/dist/site-system-e2e.d.ts.map +1 -0
- package/dist/site-system-e2e.js +54 -0
- package/dist/site-system-e2e.js.map +1 -0
- package/package.json +22 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c11-contact-route.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c11-contact-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAmB,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAgB,MAAM,uBAAuB,CAAC;AAErF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,2BAA2B;IAC3B,uBAAuB;CACf,CAAC;AACX,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAC1D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,mCAAmC,EAAE,8BAA8B,CAAC,CAAC,CAAC;AACvG,MAAM,aAAa,GAAG,gDAAgD,CAAC;AAEvE,SAAS,iBAAiB,CAAC,IAAY;IACrC,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,IAAI,UAAU,CAAC,IAAI,GAAG,SAAS,CAAC;YAAE,OAAO,IAAI,GAAG,SAAS,CAAC;IAC9F,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,SAAS,EAAE,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,QAAQ,SAAS,EAAE,CAAC,CAAC;IAC5H,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC;AAQD,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;IAClH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,aAAa,CAAC,MAAoB;IACzC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC,eAAe,CAAC;YAAE,SAAS;QACnG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC;YAAE,SAAS;QAClE,MAAM,QAAQ,GAAG,SAAS,CAAC,YAAY,EAAE,aAAa,CAAC;QACvD,IAAI,QAAQ,IAAI,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5C,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,QAAQ;gBAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QACrH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAoB;IAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,MAAM,KAAK,GAAG,CAAC,IAAa,EAAE,EAAE;QAC9B,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpD,IAAI,QAAQ,KAAK,SAAS,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,IAAI,GAAG,IAAI,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,IAAI;YAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC,CAAC;IACF,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,MAAoB,EAAE,WAAmB;IAC7D,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACjD,IAAI,CAAC,CAAC,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC,eAAe,CAAC;YAAE,SAAS;QACxK,MAAM,SAAS,GAAG,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC;QACjD,IAAI,IAAwB,CAAC;QAC7B,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;aAC1E,IAAI,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;;YACpF,SAAS;QACd,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrD,IAAI,QAAQ;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iHAAiH;AACjH,SAAS,YAAY,CAAC,MAAoB;IACxC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAY,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,KAAK,KAAK,KAAK;YAAE,OAAO,kBAAkB,CAAC;QAC/C,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,IAAI,KAAK,CAAC,UAAU,KAAK,KAAK;YAAE,OAAO,qDAAqD,CAAC;IAClJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,WAAmB,EAAE,QAA+B,EAAE;IAC7E,MAAM,SAAS,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IACxI,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,KAAK,MAAM,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/H,OAAO,OAAO;YACZ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,gIAAgI,CAAC,CAAC;YAClL,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,uFAAuF,CAAC,CAAC,CAAC;IACtH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;IAE7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IACjH,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACpH,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,gDAAgD,GAAG,CAAC,GAAG,OAAO,KAAK,sEAAsE,CAAC,CAAC,CAAC;IACzK,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,2JAA2J,CAAC,CAAC,CAAC;IACnM,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,KAAK,yIAAyI,EAAE,KAAK,CAAC,CAAC,CAAC;AACjL,CAAC","sourcesContent":["/**\n * C11 (static half): the contact route keeps the shared handler's protections (Site System v2 §13.2).\n *\n * `POST /api/contact` must come from the shared handler (`createBrandlabContactRouteHandler`, or\n * `createBrandlabContactRuntime` during the transition, the same evidence as brandlab-verify's\n * contact-provider-route rule). The shared handler strips the `bl_test` marker before delivery and reporting, so a test\n * enquiry is never delivered as a real one, and it rate-limits abuse per IP and per email address. A route that\n * writes its own handler has neither, and a route that passes `rateLimit: false` (or turns off both the per-IP and\n * per-address limits) has switched the second one off.\n *\n * The route file is read from the syntax tree, never run; one hop of local imports is followed because the handler is\n * often created in a `src/config/contact.ts` the route re-exports.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport ts from \"typescript\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, type ParsedPage } from \"../enquiry/common.js\";\nimport { isQuoteForm } from \"../enquiry/c2-quote-form.js\";\nimport { descendants } from \"../enquiry/html.js\";\nimport { LiteralReader, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\n\nexport const C11_ROUTE_CANDIDATES = [\n \"src/app/api/contact/route\",\n \"app/api/contact/route\",\n] as const;\nconst EXTENSIONS = [\".ts\", \".tsx\", \".mts\", \".js\", \".mjs\"];\nconst SHARED_HANDLERS = new Set([\"createBrandlabContactRouteHandler\", \"createBrandlabContactRuntime\"]);\nconst SHARED_MODULE = /^@brandlab-digital\\/site-next\\/contact(?:\\/|$)/;\n\nfunction fileWithExtension(base: string): string | undefined {\n for (const extension of EXTENSIONS) if (existsSync(base + extension)) return base + extension;\n for (const extension of EXTENSIONS) if (existsSync(join(base, `index${extension}`))) return join(base, `index${extension}`);\n return existsSync(base) && /\\.[a-z]+$/.test(base) ? base : undefined;\n}\n\ninterface ModuleSource {\n readonly file: string;\n readonly text: string;\n readonly source: ts.SourceFile;\n}\n\nfunction readModule(file: string): ModuleSource | undefined {\n try {\n const text = readFileSync(file, \"utf8\");\n return { file, text, source: ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) };\n } catch {\n return undefined;\n }\n}\n\n/** The names a module imports from the shared contact entry points, by local name. */\nfunction sharedImports(module: ModuleSource): Map<string, string> {\n const names = new Map<string, string>();\n for (const statement of module.source.statements) {\n if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;\n if (!SHARED_MODULE.test(statement.moduleSpecifier.text)) continue;\n const bindings = statement.importClause?.namedBindings;\n if (bindings && ts.isNamedImports(bindings)) {\n for (const element of bindings.elements) names.set(element.name.text, (element.propertyName ?? element.name).text);\n }\n }\n return names;\n}\n\nfunction usesSharedHandler(module: ModuleSource): boolean {\n const imported = sharedImports(module);\n let used = false;\n const visit = (node: ts.Node) => {\n if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {\n const original = imported.get(node.expression.text);\n if (original !== undefined && SHARED_HANDLERS.has(original)) used = true;\n }\n if (!used) ts.forEachChild(node, visit);\n };\n visit(module.source);\n return used;\n}\n\nfunction localImports(module: ModuleSource, projectRoot: string): ModuleSource[] {\n const found: ModuleSource[] = [];\n for (const statement of module.source.statements) {\n if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;\n const specifier = statement.moduleSpecifier.text;\n let base: string | undefined;\n if (specifier.startsWith(\".\")) base = resolve(dirname(module.file), specifier);\n else if (specifier.startsWith(\"@/\")) base = join(projectRoot, \"src\", specifier.slice(2));\n else continue;\n const file = fileWithExtension(base);\n const imported = file ? readModule(file) : undefined;\n if (imported) found.push(imported);\n }\n return found;\n}\n\n/** What a `rateLimit` option turns off: \"all\" for `false` (or both limits off), null when the limits stay on. */\nfunction rateLimitOff(module: ModuleSource): string | null {\n const reader = new LiteralReader(module.text, module.file);\n for (const expression of reader.propertyValues(\"rateLimit\")) {\n const value: Literal = reader.evaluate(expression);\n if (value === false) return \"rateLimit: false\";\n if (isLiteralObject(value) && value.perIp === false && value.perAddress === false) return \"both the per-IP and per-address limits set to false\";\n }\n return null;\n}\n\nexport function checkC11(projectRoot: string, pages: readonly ParsedPage[] = []): SiteSystemFinding[] {\n const routeBase = C11_ROUTE_CANDIDATES.map((base) => join(projectRoot, base)).map(fileWithExtension).find((file) => file !== undefined);\n if (!routeBase) {\n const hasForm = pages.some((page) => descendants(page.root).some((element) => element.tag === \"form\" && isQuoteForm(element)));\n return hasForm\n ? [fail(\"C11\", C11_ROUTE_CANDIDATES[0] + \".ts\", \"The site has a quote form but no /api/contact route, so enquiries have no shared handler, test-marker handling or rate limits.\")]\n : [notApplicable(\"C11\", \"None of the sampled pages has a quote form and the project has no /api/contact route.\")];\n }\n const route = readModule(routeBase);\n if (!route) return [];\n const shown = routeBase.slice(projectRoot.length).replace(/^\\/+/, \"\");\n const modules = [route, ...localImports(route, projectRoot)];\n\n const off = modules.map((module) => ({ module, off: rateLimitOff(module) })).find((entry) => entry.off !== null);\n if (off) {\n const where = off.module.file === routeBase ? shown : off.module.file.slice(projectRoot.length).replace(/^\\/+/, \"\");\n return [fail(\"C11\", where, `The contact route turns the rate limits off (${off.off} in ${where}); /api/contact must stay rate-limited per IP and per email address.`)];\n }\n if (!modules.some(usesSharedHandler)) {\n return [fail(\"C11\", shown, `${shown} does not create its handler with the shared contact handler, so the bl_test marker is not stripped before delivery and /api/contact is not rate-limited.`)];\n }\n return [pass(\"C11\", `${shown} uses the shared contact handler: the bl_test marker is stripped before delivery and reporting, and /api/contact keeps its rate limits.`, shown)];\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C7: the thank-you page (Site System v2 §13.2, CONV §14 item 10).
|
|
3
|
+
*
|
|
4
|
+
* After an accepted enquiry the visitor lands on a page that says when they will hear back, offers a `tel:` link for
|
|
5
|
+
* anything urgent, and keeps itself out of search (`noindex, follow`) so it never ranks and never counts as a lead.
|
|
6
|
+
* The library's `ThankYou` block and `thankYouMetadata()` do all three. A site with a quote form but no thank-you page
|
|
7
|
+
* among the sampled pages fails: the form has nowhere to send an accepted enquiry.
|
|
8
|
+
*/
|
|
9
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
10
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
11
|
+
import { type HtmlElement } from "../enquiry/html.js";
|
|
12
|
+
/** A sentence that says how soon the business replies: "within one business day", "in 1 to 2 hours", "within the hour". */
|
|
13
|
+
export declare const C7_REPLY_TIME: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* The robots directives of a page, from every `<meta name="robots">` tag. A `googlebot` tag alone does not count: it
|
|
16
|
+
* leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.
|
|
17
|
+
*/
|
|
18
|
+
export declare function robotsDirectives(root: HtmlElement): string[];
|
|
19
|
+
export declare function checkC7(pages: readonly ParsedPage[], missing?: readonly string[]): SiteSystemFinding[];
|
|
20
|
+
//# sourceMappingURL=c7-thank-you.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c7-thank-you.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAyC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE9F,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMjF,2HAA2H;AAC3H,eAAO,MAAM,aAAa,QASzB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAK5D;AAcD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,GAAE,SAAS,MAAM,EAAO,GAAG,iBAAiB,EAAE,CA+B1G"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fail, notApplicable, plural, resultOf } from "../enquiry/common.js";
|
|
2
|
+
import { isQuoteForm } from "../enquiry/c2-quote-form.js";
|
|
3
|
+
import { attr, descendants, textOf } from "../enquiry/html.js";
|
|
4
|
+
import { byHook, isThankYouPath, mainOf } from "../library/common.js";
|
|
5
|
+
const NUMBER = String.raw `(?:(?:a\s+)?(?:few|couple(?:\s+of)?|half(?:\s+an)?)|one|two|three|four|five|six|seven|eight|nine|ten|twelve|an?|\d+)`;
|
|
6
|
+
const UNIT = String.raw `(?:minutes?|mins?|hours?|hrs?|business\s+days?|working\s+days?|days?)`;
|
|
7
|
+
/** A sentence that says how soon the business replies: "within one business day", "in 1 to 2 hours", "within the hour". */
|
|
8
|
+
export const C7_REPLY_TIME = new RegExp([
|
|
9
|
+
String.raw `\b(?:within|in|inside|under)\s+(?:the\s+)?(?:next\s+)?${NUMBER}(?:\s*(?:-|–|to|or)\s*${NUMBER})?\s+${UNIT}\b`,
|
|
10
|
+
String.raw `\b(?:within|in|inside|under)\s+(?:the\s+)?(?:next\s+)?(?:hour|day)\b`,
|
|
11
|
+
String.raw `\b(?:within|during|in)\s+(?:our\s+)?(?:normal\s+)?(?:business|working|office)\s+hours\b`,
|
|
12
|
+
String.raw `\b(?:same|next)(?:[-\s]+business)?[-\s]+day\b`,
|
|
13
|
+
String.raw `\bby\s+(?:the\s+)?end\s+of\s+(?:the\s+)?(?:business\s+)?day\b`,
|
|
14
|
+
].join("|"), "i");
|
|
15
|
+
/**
|
|
16
|
+
* The robots directives of a page, from every `<meta name="robots">` tag. A `googlebot` tag alone does not count: it
|
|
17
|
+
* leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.
|
|
18
|
+
*/
|
|
19
|
+
export function robotsDirectives(root) {
|
|
20
|
+
return descendants(root)
|
|
21
|
+
.filter((element) => element.tag === "meta" && (attr(element, "name") ?? "").toLowerCase() === "robots")
|
|
22
|
+
.flatMap((element) => (attr(element, "content") ?? "").toLowerCase().split(/[,\s]+/))
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function hasQuoteForm(page) {
|
|
26
|
+
return descendants(page.root).some((element) => element.tag === "form" && isQuoteForm(element));
|
|
27
|
+
}
|
|
28
|
+
function telLinks(scope) {
|
|
29
|
+
return descendants(scope).filter((element) => element.tag === "a" && (attr(element, "href") ?? "").trim().toLowerCase().startsWith("tel:"));
|
|
30
|
+
}
|
|
31
|
+
function isThankYouPage(page) {
|
|
32
|
+
return isThankYouPath(page.path) || byHook(page.root, "thank-you").length > 0;
|
|
33
|
+
}
|
|
34
|
+
export function checkC7(pages, missing = []) {
|
|
35
|
+
const thankYouPages = pages.filter(isThankYouPage);
|
|
36
|
+
if (thankYouPages.length === 0) {
|
|
37
|
+
const named = missing.find((path) => isThankYouPath(path));
|
|
38
|
+
if (named !== undefined) {
|
|
39
|
+
return [fail("C7", named, `The thank-you page ${named} could not be read, so its reply time, call link and noindex were not checked.`)];
|
|
40
|
+
}
|
|
41
|
+
if (!pages.some(hasQuoteForm))
|
|
42
|
+
return [notApplicable("C7", "None of the sampled pages has a quote form or a thank-you page.")];
|
|
43
|
+
return [fail("C7", undefined, "The site has a quote form but no thank-you page for an accepted enquiry to open.")];
|
|
44
|
+
}
|
|
45
|
+
const failures = [];
|
|
46
|
+
for (const page of thankYouPages) {
|
|
47
|
+
const block = byHook(page.root, "thank-you")[0];
|
|
48
|
+
const scope = block ?? mainOf(page.root);
|
|
49
|
+
const reply = descendants(scope).find((element) => attr(element, "data-slot") === "reply");
|
|
50
|
+
const replyText = reply ? textOf(reply) : textOf(scope);
|
|
51
|
+
if (!C7_REPLY_TIME.test(replyText)) {
|
|
52
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} does not say when the business will reply (for example "within one business day").`));
|
|
53
|
+
}
|
|
54
|
+
if (telLinks(scope).length === 0) {
|
|
55
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} has no tel: link for anything urgent.`));
|
|
56
|
+
}
|
|
57
|
+
const directives = robotsDirectives(page.root);
|
|
58
|
+
if (!directives.includes("noindex") && !directives.includes("none")) {
|
|
59
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} is not noindex; it carries robots "${directives.join(", ") || "none"}" instead of "noindex, follow".`));
|
|
60
|
+
}
|
|
61
|
+
else if (directives.includes("nofollow") || directives.includes("none")) {
|
|
62
|
+
failures.push(fail("C7", page.path, `The thank-you page ${page.path} is noindex but also nofollow; its links should still be followed ("noindex, follow").`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return resultOf("C7", failures, `Checked ${plural(thankYouPages.length, "thank-you page")}: each gives a reply time and a tel: link and is noindex, follow.`);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=c7-thank-you.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c7-thank-you.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c7-thank-you.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * C7: the thank-you page (Site System v2 §13.2, CONV §14 item 10).\n *\n * After an accepted enquiry the visitor lands on a page that says when they will hear back, offers a `tel:` link for\n * anything urgent, and keeps itself out of search (`noindex, follow`) so it never ranks and never counts as a lead.\n * The library's `ThankYou` block and `thankYouMetadata()` do all three. A site with a quote form but no thank-you page\n * among the sampled pages fails: the form has nowhere to send an accepted enquiry.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, plural, resultOf, type ParsedPage } from \"../enquiry/common.js\";\nimport { isQuoteForm } from \"../enquiry/c2-quote-form.js\";\nimport { attr, descendants, textOf, type HtmlElement } from \"../enquiry/html.js\";\nimport { byHook, isThankYouPath, mainOf } from \"../library/common.js\";\n\nconst NUMBER = String.raw`(?:(?:a\\s+)?(?:few|couple(?:\\s+of)?|half(?:\\s+an)?)|one|two|three|four|five|six|seven|eight|nine|ten|twelve|an?|\\d+)`;\nconst UNIT = String.raw`(?:minutes?|mins?|hours?|hrs?|business\\s+days?|working\\s+days?|days?)`;\n\n/** A sentence that says how soon the business replies: \"within one business day\", \"in 1 to 2 hours\", \"within the hour\". */\nexport const C7_REPLY_TIME = new RegExp(\n [\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?${NUMBER}(?:\\s*(?:-|–|to|or)\\s*${NUMBER})?\\s+${UNIT}\\b`,\n String.raw`\\b(?:within|in|inside|under)\\s+(?:the\\s+)?(?:next\\s+)?(?:hour|day)\\b`,\n String.raw`\\b(?:within|during|in)\\s+(?:our\\s+)?(?:normal\\s+)?(?:business|working|office)\\s+hours\\b`,\n String.raw`\\b(?:same|next)(?:[-\\s]+business)?[-\\s]+day\\b`,\n String.raw`\\bby\\s+(?:the\\s+)?end\\s+of\\s+(?:the\\s+)?(?:business\\s+)?day\\b`,\n ].join(\"|\"),\n \"i\",\n);\n\n/**\n * The robots directives of a page, from every `<meta name=\"robots\">` tag. A `googlebot` tag alone does not count: it\n * leaves Bing and every other crawler free to index the page, which is why `thankYouMetadata()` writes both.\n */\nexport function robotsDirectives(root: HtmlElement): string[] {\n return descendants(root)\n .filter((element) => element.tag === \"meta\" && (attr(element, \"name\") ?? \"\").toLowerCase() === \"robots\")\n .flatMap((element) => (attr(element, \"content\") ?? \"\").toLowerCase().split(/[,\\s]+/))\n .filter(Boolean);\n}\n\nfunction hasQuoteForm(page: ParsedPage): boolean {\n return descendants(page.root).some((element) => element.tag === \"form\" && isQuoteForm(element));\n}\n\nfunction telLinks(scope: HtmlElement): HtmlElement[] {\n return descendants(scope).filter((element) => element.tag === \"a\" && (attr(element, \"href\") ?? \"\").trim().toLowerCase().startsWith(\"tel:\"));\n}\n\nfunction isThankYouPage(page: ParsedPage): boolean {\n return isThankYouPath(page.path) || byHook(page.root, \"thank-you\").length > 0;\n}\n\nexport function checkC7(pages: readonly ParsedPage[], missing: readonly string[] = []): SiteSystemFinding[] {\n const thankYouPages = pages.filter(isThankYouPage);\n if (thankYouPages.length === 0) {\n const named = missing.find((path) => isThankYouPath(path));\n if (named !== undefined) {\n return [fail(\"C7\", named, `The thank-you page ${named} could not be read, so its reply time, call link and noindex were not checked.`)];\n }\n if (!pages.some(hasQuoteForm)) return [notApplicable(\"C7\", \"None of the sampled pages has a quote form or a thank-you page.\")];\n return [fail(\"C7\", undefined, \"The site has a quote form but no thank-you page for an accepted enquiry to open.\")];\n }\n\n const failures: SiteSystemFinding[] = [];\n for (const page of thankYouPages) {\n const block = byHook(page.root, \"thank-you\")[0];\n const scope = block ?? mainOf(page.root);\n const reply = descendants(scope).find((element) => attr(element, \"data-slot\") === \"reply\");\n const replyText = reply ? textOf(reply) : textOf(scope);\n if (!C7_REPLY_TIME.test(replyText)) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} does not say when the business will reply (for example \"within one business day\").`));\n }\n if (telLinks(scope).length === 0) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} has no tel: link for anything urgent.`));\n }\n const directives = robotsDirectives(page.root);\n if (!directives.includes(\"noindex\") && !directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is not noindex; it carries robots \"${directives.join(\", \") || \"none\"}\" instead of \"noindex, follow\".`));\n } else if (directives.includes(\"nofollow\") || directives.includes(\"none\")) {\n failures.push(fail(\"C7\", page.path, `The thank-you page ${page.path} is noindex but also nofollow; its links should still be followed (\"noindex, follow\").`));\n }\n }\n return resultOf(\"C7\", failures, `Checked ${plural(thankYouPages.length, \"thank-you page\")}: each gives a reply time and a tel: link and is noindex, follow.`);\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C8: the forgiving form (Site System v2 §13.2, CONV §4.2).
|
|
3
|
+
*
|
|
4
|
+
* People type their phone number every way there is: "0412 345 678", "(02) 9123 4567", "+61 412 345 678", "13 14 15",
|
|
5
|
+
* "1300 123 456", "1800 123 456". The phone input must be `type="tel"` (or carry `inputmode="tel"`), must not carry a
|
|
6
|
+
* `pattern`, `maxlength` or `minlength` that turns any of those away, and every field names its `autocomplete` token so
|
|
7
|
+
* a phone can fill the form in one tap: `name`, `address-level2` for the suburb, `tel`, `email`. The photo input never
|
|
8
|
+
* carries `capture`, which would force the camera and hide the photo library.
|
|
9
|
+
*/
|
|
10
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
11
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
12
|
+
import { type HtmlElement } from "../enquiry/html.js";
|
|
13
|
+
/** Phone numbers a real visitor types; a pattern, minimum or maximum length must accept every one. */
|
|
14
|
+
export declare const C8_PHONE_SAMPLES: readonly ["0412 345 678", "0412345678", "04 1234 5678", "(02) 9123 4567", "02 9123 4567", "02-9123-4567", "+61 412 345 678", "+61412345678", "+61 2 9123 4567", "13 14 15", "131415", "1300 123 456", "1300123456", "1800 123 456", "1800123456"];
|
|
15
|
+
/** What is wrong with the phone input, as short fragments. */
|
|
16
|
+
export declare function phoneInputProblems(input: HtmlElement): string[];
|
|
17
|
+
export declare function formProblems(form: HtmlElement): string[];
|
|
18
|
+
export declare function checkC8(pages: readonly ParsedPage[]): SiteSystemFinding[];
|
|
19
|
+
//# sourceMappingURL=c8-forgiving-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c8-forgiving-form.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c8-forgiving-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEpF,OAAO,EAAyC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAG7F,sGAAsG;AACtG,eAAO,MAAM,gBAAgB,mPAgBnB,CAAC;AAuCX,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,CAqB/D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAoBxD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,iBAAiB,EAAE,CAczE"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { notApplicable, pass, plural } from "../enquiry/common.js";
|
|
2
|
+
import { isQuoteForm } from "../enquiry/c2-quote-form.js";
|
|
3
|
+
import { attr, descendants, isHiddenFromPeople } from "../enquiry/html.js";
|
|
4
|
+
import { PageProblems } from "../library/common.js";
|
|
5
|
+
/** Phone numbers a real visitor types; a pattern, minimum or maximum length must accept every one. */
|
|
6
|
+
export const C8_PHONE_SAMPLES = [
|
|
7
|
+
"0412 345 678",
|
|
8
|
+
"0412345678",
|
|
9
|
+
"04 1234 5678",
|
|
10
|
+
"(02) 9123 4567",
|
|
11
|
+
"02 9123 4567",
|
|
12
|
+
"02-9123-4567",
|
|
13
|
+
"+61 412 345 678",
|
|
14
|
+
"+61412345678",
|
|
15
|
+
"+61 2 9123 4567",
|
|
16
|
+
"13 14 15",
|
|
17
|
+
"131415",
|
|
18
|
+
"1300 123 456",
|
|
19
|
+
"1300123456",
|
|
20
|
+
"1800 123 456",
|
|
21
|
+
"1800123456",
|
|
22
|
+
];
|
|
23
|
+
const AUTOCOMPLETE_TOKENS = {
|
|
24
|
+
name: ["name", "given-name"],
|
|
25
|
+
suburb: ["address-level2"],
|
|
26
|
+
phone: ["tel", "tel-national"],
|
|
27
|
+
email: ["email"],
|
|
28
|
+
};
|
|
29
|
+
function token(value) {
|
|
30
|
+
return (value ?? "").trim().toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
/** Which of the four text fields an input is, from its type, autocomplete token, name and id. */
|
|
33
|
+
function fieldOf(input) {
|
|
34
|
+
const type = token(attr(input, "type"));
|
|
35
|
+
const autocomplete = token(attr(input, "autocomplete")).split(/\s+/);
|
|
36
|
+
const identity = `${token(attr(input, "name"))} ${token(attr(input, "id"))}`;
|
|
37
|
+
if (type === "tel" || token(attr(input, "inputmode")) === "tel" || autocomplete.some((part) => part.startsWith("tel")) || /phone|mobile|\btel\b/.test(identity))
|
|
38
|
+
return "phone";
|
|
39
|
+
if (type === "email" || autocomplete.includes("email") || /e-?mail/.test(identity))
|
|
40
|
+
return "email";
|
|
41
|
+
if (autocomplete.includes("address-level2") || /suburb|locality|town|city/.test(identity))
|
|
42
|
+
return "suburb";
|
|
43
|
+
if (autocomplete.some((part) => part === "name" || part === "given-name" || part === "family-name") || /(?:^|[\s_-])(?:full|first|your)?[_-]?name\b/.test(identity))
|
|
44
|
+
return "name";
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** The pattern as the browser applies it: the whole value must match. Null when it is not a usable expression. */
|
|
48
|
+
function wholeValuePattern(pattern) {
|
|
49
|
+
for (const flags of ["v", "u"]) {
|
|
50
|
+
try {
|
|
51
|
+
return new RegExp(`^(?:${pattern})$`, flags);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// try the next flag set
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** What is wrong with the phone input, as short fragments. */
|
|
60
|
+
export function phoneInputProblems(input) {
|
|
61
|
+
const problems = [];
|
|
62
|
+
const type = token(attr(input, "type"));
|
|
63
|
+
if (type !== "tel" && token(attr(input, "inputmode")) !== "tel") {
|
|
64
|
+
problems.push(`the phone input is type="${type || "text"}" without inputmode="tel", so phones show the wrong keyboard`);
|
|
65
|
+
}
|
|
66
|
+
const pattern = attr(input, "pattern");
|
|
67
|
+
if (pattern !== undefined && pattern !== "") {
|
|
68
|
+
const expression = wholeValuePattern(pattern);
|
|
69
|
+
const rejected = expression ? C8_PHONE_SAMPLES.filter((sample) => !expression.test(sample)) : [];
|
|
70
|
+
if (rejected.length > 0) {
|
|
71
|
+
problems.push(`the phone pattern turns away ${rejected.slice(0, 3).map((sample) => `"${sample}"`).join(", ")}${rejected.length > 3 ? ` and ${rejected.length - 3} more` : ""}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const longest = Math.max(...C8_PHONE_SAMPLES.map((sample) => sample.length));
|
|
75
|
+
const shortest = Math.min(...C8_PHONE_SAMPLES.map((sample) => sample.length));
|
|
76
|
+
const maxlength = Number.parseInt(attr(input, "maxlength") ?? "", 10);
|
|
77
|
+
if (Number.isFinite(maxlength) && maxlength < longest)
|
|
78
|
+
problems.push(`maxlength="${maxlength}" is too short for a number typed with +61 and spaces`);
|
|
79
|
+
const minlength = Number.parseInt(attr(input, "minlength") ?? "", 10);
|
|
80
|
+
if (Number.isFinite(minlength) && minlength > shortest)
|
|
81
|
+
problems.push(`minlength="${minlength}" turns away a 13 number such as "131415"`);
|
|
82
|
+
return problems;
|
|
83
|
+
}
|
|
84
|
+
export function formProblems(form) {
|
|
85
|
+
const problems = [];
|
|
86
|
+
const inputs = descendants(form).filter((element) => element.tag === "input" && !isHiddenFromPeople(element) && attr(element, "tabindex") !== "-1");
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const input of inputs) {
|
|
89
|
+
if (token(attr(input, "type")) === "file")
|
|
90
|
+
continue;
|
|
91
|
+
const field = fieldOf(input);
|
|
92
|
+
if (!field)
|
|
93
|
+
continue;
|
|
94
|
+
seen.add(field);
|
|
95
|
+
const autocomplete = token(attr(input, "autocomplete")).split(/\s+/).filter(Boolean);
|
|
96
|
+
if (!autocomplete.some((part) => AUTOCOMPLETE_TOKENS[field].includes(part))) {
|
|
97
|
+
problems.push(`the ${field} input has no autocomplete="${AUTOCOMPLETE_TOKENS[field][0]}"`);
|
|
98
|
+
}
|
|
99
|
+
if (field === "phone")
|
|
100
|
+
problems.push(...phoneInputProblems(input));
|
|
101
|
+
}
|
|
102
|
+
if (!seen.has("phone"))
|
|
103
|
+
problems.push("the form has no phone input to check");
|
|
104
|
+
for (const file of descendants(form).filter((element) => element.tag === "input" && token(attr(element, "type")) === "file")) {
|
|
105
|
+
if (attr(file, "capture") !== undefined)
|
|
106
|
+
problems.push("the photo input has a capture attribute, which forces the camera and hides the photo library");
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
export function checkC8(pages) {
|
|
111
|
+
const problems = new PageProblems();
|
|
112
|
+
let forms = 0;
|
|
113
|
+
const formPages = new Set();
|
|
114
|
+
for (const page of pages) {
|
|
115
|
+
for (const form of descendants(page.root).filter((element) => element.tag === "form" && isQuoteForm(element))) {
|
|
116
|
+
forms += 1;
|
|
117
|
+
formPages.add(page.path);
|
|
118
|
+
for (const problem of formProblems(form))
|
|
119
|
+
problems.add(page.path, `The quote form is not forgiving: ${problem}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (forms === 0)
|
|
123
|
+
return [notApplicable("C8", "None of the sampled pages has a quote form.")];
|
|
124
|
+
if (problems.size > 0)
|
|
125
|
+
return problems.findings("C8");
|
|
126
|
+
return [pass("C8", `Checked ${plural(forms, "quote form")} on ${plural(formPages.size, "page")}: each takes a phone number in any format and names its autocomplete tokens, and the photo input has no capture.`)];
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=c8-forgiving-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c8-forgiving-form.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/c8-forgiving-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * C8: the forgiving form (Site System v2 §13.2, CONV §4.2).\n *\n * People type their phone number every way there is: \"0412 345 678\", \"(02) 9123 4567\", \"+61 412 345 678\", \"13 14 15\",\n * \"1300 123 456\", \"1800 123 456\". The phone input must be `type=\"tel\"` (or carry `inputmode=\"tel\"`), must not carry a\n * `pattern`, `maxlength` or `minlength` that turns any of those away, and every field names its `autocomplete` token so\n * a phone can fill the form in one tap: `name`, `address-level2` for the suburb, `tel`, `email`. The photo input never\n * carries `capture`, which would force the camera and hide the photo library.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { isQuoteForm } from \"../enquiry/c2-quote-form.js\";\nimport { attr, descendants, isHiddenFromPeople, type HtmlElement } from \"../enquiry/html.js\";\nimport { PageProblems } from \"../library/common.js\";\n\n/** Phone numbers a real visitor types; a pattern, minimum or maximum length must accept every one. */\nexport const C8_PHONE_SAMPLES = [\n \"0412 345 678\",\n \"0412345678\",\n \"04 1234 5678\",\n \"(02) 9123 4567\",\n \"02 9123 4567\",\n \"02-9123-4567\",\n \"+61 412 345 678\",\n \"+61412345678\",\n \"+61 2 9123 4567\",\n \"13 14 15\",\n \"131415\",\n \"1300 123 456\",\n \"1300123456\",\n \"1800 123 456\",\n \"1800123456\",\n] as const;\n\nconst AUTOCOMPLETE_TOKENS = {\n name: [\"name\", \"given-name\"],\n suburb: [\"address-level2\"],\n phone: [\"tel\", \"tel-national\"],\n email: [\"email\"],\n} as const;\n\ntype Field = keyof typeof AUTOCOMPLETE_TOKENS;\n\nfunction token(value: string | undefined): string {\n return (value ?? \"\").trim().toLowerCase();\n}\n\n/** Which of the four text fields an input is, from its type, autocomplete token, name and id. */\nfunction fieldOf(input: HtmlElement): Field | null {\n const type = token(attr(input, \"type\"));\n const autocomplete = token(attr(input, \"autocomplete\")).split(/\\s+/);\n const identity = `${token(attr(input, \"name\"))} ${token(attr(input, \"id\"))}`;\n if (type === \"tel\" || token(attr(input, \"inputmode\")) === \"tel\" || autocomplete.some((part) => part.startsWith(\"tel\")) || /phone|mobile|\\btel\\b/.test(identity)) return \"phone\";\n if (type === \"email\" || autocomplete.includes(\"email\") || /e-?mail/.test(identity)) return \"email\";\n if (autocomplete.includes(\"address-level2\") || /suburb|locality|town|city/.test(identity)) return \"suburb\";\n if (autocomplete.some((part) => part === \"name\" || part === \"given-name\" || part === \"family-name\") || /(?:^|[\\s_-])(?:full|first|your)?[_-]?name\\b/.test(identity)) return \"name\";\n return null;\n}\n\n/** The pattern as the browser applies it: the whole value must match. Null when it is not a usable expression. */\nfunction wholeValuePattern(pattern: string): RegExp | null {\n for (const flags of [\"v\", \"u\"]) {\n try {\n return new RegExp(`^(?:${pattern})$`, flags);\n } catch {\n // try the next flag set\n }\n }\n return null;\n}\n\n/** What is wrong with the phone input, as short fragments. */\nexport function phoneInputProblems(input: HtmlElement): string[] {\n const problems: string[] = [];\n const type = token(attr(input, \"type\"));\n if (type !== \"tel\" && token(attr(input, \"inputmode\")) !== \"tel\") {\n problems.push(`the phone input is type=\"${type || \"text\"}\" without inputmode=\"tel\", so phones show the wrong keyboard`);\n }\n const pattern = attr(input, \"pattern\");\n if (pattern !== undefined && pattern !== \"\") {\n const expression = wholeValuePattern(pattern);\n const rejected = expression ? C8_PHONE_SAMPLES.filter((sample) => !expression.test(sample)) : [];\n if (rejected.length > 0) {\n problems.push(`the phone pattern turns away ${rejected.slice(0, 3).map((sample) => `\"${sample}\"`).join(\", \")}${rejected.length > 3 ? ` and ${rejected.length - 3} more` : \"\"}`);\n }\n }\n const longest = Math.max(...C8_PHONE_SAMPLES.map((sample) => sample.length));\n const shortest = Math.min(...C8_PHONE_SAMPLES.map((sample) => sample.length));\n const maxlength = Number.parseInt(attr(input, \"maxlength\") ?? \"\", 10);\n if (Number.isFinite(maxlength) && maxlength < longest) problems.push(`maxlength=\"${maxlength}\" is too short for a number typed with +61 and spaces`);\n const minlength = Number.parseInt(attr(input, \"minlength\") ?? \"\", 10);\n if (Number.isFinite(minlength) && minlength > shortest) problems.push(`minlength=\"${minlength}\" turns away a 13 number such as \"131415\"`);\n return problems;\n}\n\nexport function formProblems(form: HtmlElement): string[] {\n const problems: string[] = [];\n const inputs = descendants(form).filter((element) => element.tag === \"input\" && !isHiddenFromPeople(element) && attr(element, \"tabindex\") !== \"-1\");\n const seen = new Set<Field>();\n for (const input of inputs) {\n if (token(attr(input, \"type\")) === \"file\") continue;\n const field = fieldOf(input);\n if (!field) continue;\n seen.add(field);\n const autocomplete = token(attr(input, \"autocomplete\")).split(/\\s+/).filter(Boolean);\n if (!autocomplete.some((part) => (AUTOCOMPLETE_TOKENS[field] as readonly string[]).includes(part))) {\n problems.push(`the ${field} input has no autocomplete=\"${AUTOCOMPLETE_TOKENS[field][0]}\"`);\n }\n if (field === \"phone\") problems.push(...phoneInputProblems(input));\n }\n if (!seen.has(\"phone\")) problems.push(\"the form has no phone input to check\");\n for (const file of descendants(form).filter((element) => element.tag === \"input\" && token(attr(element, \"type\")) === \"file\")) {\n if (attr(file, \"capture\") !== undefined) problems.push(\"the photo input has a capture attribute, which forces the camera and hides the photo library\");\n }\n return problems;\n}\n\nexport function checkC8(pages: readonly ParsedPage[]): SiteSystemFinding[] {\n const problems = new PageProblems();\n let forms = 0;\n const formPages = new Set<string>();\n for (const page of pages) {\n for (const form of descendants(page.root).filter((element) => element.tag === \"form\" && isQuoteForm(element))) {\n forms += 1;\n formPages.add(page.path);\n for (const problem of formProblems(form)) problems.add(page.path, `The quote form is not forgiving: ${problem}`);\n }\n }\n if (forms === 0) return [notApplicable(\"C8\", \"None of the sampled pages has a quote form.\")];\n if (problems.size > 0) return problems.findings(\"C8\");\n return [pass(\"C8\", `Checked ${plural(forms, \"quote form\")} on ${plural(formPages.size, \"page\")}: each takes a phone number in any format and names its autocomplete tokens, and the photo input has no capture.`)];\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form rule module (release 3.10.0): C7 the thank-you page, C8 the forgiving form and C11 the contact route's
|
|
3
|
+
* protections. C7 and C8 read the sampled pages' server HTML; C11 reads the project's `/api/contact` route.
|
|
4
|
+
*/
|
|
5
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
6
|
+
import type { SiteSystemRenderedInput } from "../../runners.js";
|
|
7
|
+
export { C11_ROUTE_CANDIDATES, checkC11 } from "./c11-contact-route.js";
|
|
8
|
+
export { C7_REPLY_TIME, checkC7, robotsDirectives } from "./c7-thank-you.js";
|
|
9
|
+
export { C8_PHONE_SAMPLES, checkC8, formProblems, phoneInputProblems } from "./c8-forgiving-form.js";
|
|
10
|
+
/** Runs C7, C8 and C11. */
|
|
11
|
+
export declare function runFormRules(input: Pick<SiteSystemRenderedInput, "baseUrl" | "pages" | "projectRoot"> & {
|
|
12
|
+
readonly missing?: readonly string[];
|
|
13
|
+
}): Promise<readonly SiteSystemFinding[]>;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAKhE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAErG,2BAA2B;AAC3B,wBAAsB,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,GAAG,OAAO,GAAG,aAAa,CAAC,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAG9L"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parsePages } from "../enquiry/common.js";
|
|
2
|
+
import { checkC11 } from "./c11-contact-route.js";
|
|
3
|
+
import { checkC7 } from "./c7-thank-you.js";
|
|
4
|
+
import { checkC8 } from "./c8-forgiving-form.js";
|
|
5
|
+
export { C11_ROUTE_CANDIDATES, checkC11 } from "./c11-contact-route.js";
|
|
6
|
+
export { C7_REPLY_TIME, checkC7, robotsDirectives } from "./c7-thank-you.js";
|
|
7
|
+
export { C8_PHONE_SAMPLES, checkC8, formProblems, phoneInputProblems } from "./c8-forgiving-form.js";
|
|
8
|
+
/** Runs C7, C8 and C11. */
|
|
9
|
+
export async function runFormRules(input) {
|
|
10
|
+
const pages = parsePages(input.pages);
|
|
11
|
+
return [...checkC7(pages, input.missing ?? []), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/form/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAErG,2BAA2B;AAC3B,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAoH;IACrJ,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;AAC5G,CAAC","sourcesContent":["/**\n * The form rule module (release 3.10.0): C7 the thank-you page, C8 the forgiving form and C11 the contact route's\n * protections. C7 and C8 read the sampled pages' server HTML; C11 reads the project's `/api/contact` route.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { parsePages } from \"../enquiry/common.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { checkC11 } from \"./c11-contact-route.js\";\nimport { checkC7 } from \"./c7-thank-you.js\";\nimport { checkC8 } from \"./c8-forgiving-form.js\";\n\nexport { C11_ROUTE_CANDIDATES, checkC11 } from \"./c11-contact-route.js\";\nexport { C7_REPLY_TIME, checkC7, robotsDirectives } from \"./c7-thank-you.js\";\nexport { C8_PHONE_SAMPLES, checkC8, formProblems, phoneInputProblems } from \"./c8-forgiving-form.js\";\n\n/** Runs C7, C8 and C11. */\nexport async function runFormRules(input: Pick<SiteSystemRenderedInput, \"baseUrl\" | \"pages\" | \"projectRoot\"> & { readonly missing?: readonly string[] }): Promise<readonly SiteSystemFinding[]> {\n const pages = parsePages(input.pages);\n return [...checkC7(pages, input.missing ?? []), ...checkC8(pages), ...checkC11(input.projectRoot, pages)];\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers the 3.9.0 to 3.11.0 rule modules (library, form, sections) share: the same problem found on many pages is
|
|
3
|
+
* one finding that names a few of them, and a page is found by the last segment of its path so a site mounted below a
|
|
4
|
+
* prefix (the component catalogue) reads like a site mounted at the root.
|
|
5
|
+
*/
|
|
6
|
+
import type { SiteSystemCheckId, SiteSystemFinding } from "../../checks.js";
|
|
7
|
+
import { type HtmlElement } from "../enquiry/html.js";
|
|
8
|
+
/** "/a", "/a and /b", "/a, /b and /c", or "/a, /b, /c and 4 other pages". */
|
|
9
|
+
export declare function pagesPhrase(paths: readonly string[]): string;
|
|
10
|
+
/** Collects `problem` (a clause with no subject) against the pages it was found on. */
|
|
11
|
+
export declare class PageProblems {
|
|
12
|
+
private readonly found;
|
|
13
|
+
add(path: string, problem: string): void;
|
|
14
|
+
get size(): number;
|
|
15
|
+
/** One failing finding per distinct problem: "{problem} on {pages}." The target is the first page. */
|
|
16
|
+
findings(checkId: SiteSystemCheckId): SiteSystemFinding[];
|
|
17
|
+
}
|
|
18
|
+
/** The path's last segment, lower case: "/aa-tree/contact" and "/contact" both give "contact"; home gives "". */
|
|
19
|
+
export declare function lastSegment(path: string): string;
|
|
20
|
+
export declare function isContactPath(path: string): boolean;
|
|
21
|
+
/** The contact page: a contact path, or any page that holds the library's contact section. */
|
|
22
|
+
export declare function isContactPage(page: {
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly root: HtmlElement;
|
|
25
|
+
}): boolean;
|
|
26
|
+
export declare function isThankYouPath(path: string): boolean;
|
|
27
|
+
/** The first element below `node` for which `test` holds. */
|
|
28
|
+
export declare function findElement(node: HtmlElement, test: (element: HtmlElement) => boolean): HtmlElement | undefined;
|
|
29
|
+
export declare function hookOf(element: HtmlElement): string | undefined;
|
|
30
|
+
/** Elements carrying `data-bl="name"`. */
|
|
31
|
+
export declare function byHook(root: HtmlElement, name: string): HtmlElement[];
|
|
32
|
+
/** The `<main>` element, or the whole document when the page has none. */
|
|
33
|
+
export declare function mainOf(root: HtmlElement): HtmlElement;
|
|
34
|
+
/** The first `n` items of a list, then "and N more": for messages that name files or images. */
|
|
35
|
+
export declare function namedList(items: readonly string[], shown?: number): string;
|
|
36
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/library/common.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKzE,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAO5D;AAED,uFAAuF;AACvF,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IAErD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IASxC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sGAAsG;IACtG,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,EAAE;CAG1D;AAED,iHAAiH;AACjH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAAG,OAAO,CAElG;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,GAAG,WAAW,GAAG,SAAS,CAE/G;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED,0CAA0C;AAC1C,wBAAgB,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAErE;AAED,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAErD;AAED,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAI,GAAG,MAAM,CAGrE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fail } from "../enquiry/common.js";
|
|
2
|
+
import { attr, descendants } from "../enquiry/html.js";
|
|
3
|
+
/** How many page paths a message names before it says "and N other pages". */
|
|
4
|
+
const NAMED_PAGES = 3;
|
|
5
|
+
/** "/a", "/a and /b", "/a, /b and /c", or "/a, /b, /c and 4 other pages". */
|
|
6
|
+
export function pagesPhrase(paths) {
|
|
7
|
+
const unique = [...new Set(paths)];
|
|
8
|
+
if (unique.length <= NAMED_PAGES) {
|
|
9
|
+
return unique.length <= 1 ? (unique[0] ?? "the site") : `${unique.slice(0, -1).join(", ")} and ${unique[unique.length - 1]}`;
|
|
10
|
+
}
|
|
11
|
+
const rest = unique.length - NAMED_PAGES;
|
|
12
|
+
return `${unique.slice(0, NAMED_PAGES).join(", ")} and ${rest} other ${rest === 1 ? "page" : "pages"}`;
|
|
13
|
+
}
|
|
14
|
+
/** Collects `problem` (a clause with no subject) against the pages it was found on. */
|
|
15
|
+
export class PageProblems {
|
|
16
|
+
found = new Map();
|
|
17
|
+
add(path, problem) {
|
|
18
|
+
const paths = this.found.get(problem);
|
|
19
|
+
if (paths) {
|
|
20
|
+
if (!paths.includes(path))
|
|
21
|
+
paths.push(path);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.found.set(problem, [path]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get size() {
|
|
28
|
+
return this.found.size;
|
|
29
|
+
}
|
|
30
|
+
/** One failing finding per distinct problem: "{problem} on {pages}." The target is the first page. */
|
|
31
|
+
findings(checkId) {
|
|
32
|
+
return [...this.found].map(([problem, paths]) => fail(checkId, paths[0], `${problem} on ${pagesPhrase(paths)}.`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The path's last segment, lower case: "/aa-tree/contact" and "/contact" both give "contact"; home gives "". */
|
|
36
|
+
export function lastSegment(path) {
|
|
37
|
+
const parts = path.split("/").filter(Boolean);
|
|
38
|
+
return (parts[parts.length - 1] ?? "").toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
export function isContactPath(path) {
|
|
41
|
+
return ["contact", "contact-us", "get-in-touch"].includes(lastSegment(path));
|
|
42
|
+
}
|
|
43
|
+
/** The contact page: a contact path, or any page that holds the library's contact section. */
|
|
44
|
+
export function isContactPage(page) {
|
|
45
|
+
return isContactPath(page.path) || descendants(page.root).some((element) => attr(element, "data-bl-section") === "contact");
|
|
46
|
+
}
|
|
47
|
+
export function isThankYouPath(path) {
|
|
48
|
+
return /^(?:thank-you|thanks|thankyou|enquiry-sent|message-sent)$/.test(lastSegment(path));
|
|
49
|
+
}
|
|
50
|
+
/** The first element below `node` for which `test` holds. */
|
|
51
|
+
export function findElement(node, test) {
|
|
52
|
+
return descendants(node).find(test);
|
|
53
|
+
}
|
|
54
|
+
export function hookOf(element) {
|
|
55
|
+
return attr(element, "data-bl");
|
|
56
|
+
}
|
|
57
|
+
/** Elements carrying `data-bl="name"`. */
|
|
58
|
+
export function byHook(root, name) {
|
|
59
|
+
return descendants(root).filter((element) => attr(element, "data-bl") === name);
|
|
60
|
+
}
|
|
61
|
+
/** The `<main>` element, or the whole document when the page has none. */
|
|
62
|
+
export function mainOf(root) {
|
|
63
|
+
return descendants(root).find((element) => element.tag === "main") ?? root;
|
|
64
|
+
}
|
|
65
|
+
/** The first `n` items of a list, then "and N more": for messages that name files or images. */
|
|
66
|
+
export function namedList(items, shown = 5) {
|
|
67
|
+
const head = items.slice(0, shown).join(", ");
|
|
68
|
+
return items.length > shown ? `${head} and ${items.length - shown} more` : head;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../../../src/site-system/rules/library/common.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Helpers the 3.9.0 to 3.11.0 rule modules (library, form, sections) share: the same problem found on many pages is\n * one finding that names a few of them, and a page is found by the last segment of its path so a site mounted below a\n * prefix (the component catalogue) reads like a site mounted at the root.\n */\nimport type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport { fail } from \"../enquiry/common.js\";\nimport { attr, descendants, type HtmlElement } from \"../enquiry/html.js\";\n\n/** How many page paths a message names before it says \"and N other pages\". */\nconst NAMED_PAGES = 3;\n\n/** \"/a\", \"/a and /b\", \"/a, /b and /c\", or \"/a, /b, /c and 4 other pages\". */\nexport function pagesPhrase(paths: readonly string[]): string {\n const unique = [...new Set(paths)];\n if (unique.length <= NAMED_PAGES) {\n return unique.length <= 1 ? (unique[0] ?? \"the site\") : `${unique.slice(0, -1).join(\", \")} and ${unique[unique.length - 1]}`;\n }\n const rest = unique.length - NAMED_PAGES;\n return `${unique.slice(0, NAMED_PAGES).join(\", \")} and ${rest} other ${rest === 1 ? \"page\" : \"pages\"}`;\n}\n\n/** Collects `problem` (a clause with no subject) against the pages it was found on. */\nexport class PageProblems {\n private readonly found = new Map<string, string[]>();\n\n add(path: string, problem: string): void {\n const paths = this.found.get(problem);\n if (paths) {\n if (!paths.includes(path)) paths.push(path);\n } else {\n this.found.set(problem, [path]);\n }\n }\n\n get size(): number {\n return this.found.size;\n }\n\n /** One failing finding per distinct problem: \"{problem} on {pages}.\" The target is the first page. */\n findings(checkId: SiteSystemCheckId): SiteSystemFinding[] {\n return [...this.found].map(([problem, paths]) => fail(checkId, paths[0], `${problem} on ${pagesPhrase(paths)}.`));\n }\n}\n\n/** The path's last segment, lower case: \"/aa-tree/contact\" and \"/contact\" both give \"contact\"; home gives \"\". */\nexport function lastSegment(path: string): string {\n const parts = path.split(\"/\").filter(Boolean);\n return (parts[parts.length - 1] ?? \"\").toLowerCase();\n}\n\nexport function isContactPath(path: string): boolean {\n return [\"contact\", \"contact-us\", \"get-in-touch\"].includes(lastSegment(path));\n}\n\n/** The contact page: a contact path, or any page that holds the library's contact section. */\nexport function isContactPage(page: { readonly path: string; readonly root: HtmlElement }): boolean {\n return isContactPath(page.path) || descendants(page.root).some((element) => attr(element, \"data-bl-section\") === \"contact\");\n}\n\nexport function isThankYouPath(path: string): boolean {\n return /^(?:thank-you|thanks|thankyou|enquiry-sent|message-sent)$/.test(lastSegment(path));\n}\n\n/** The first element below `node` for which `test` holds. */\nexport function findElement(node: HtmlElement, test: (element: HtmlElement) => boolean): HtmlElement | undefined {\n return descendants(node).find(test);\n}\n\nexport function hookOf(element: HtmlElement): string | undefined {\n return attr(element, \"data-bl\");\n}\n\n/** Elements carrying `data-bl=\"name\"`. */\nexport function byHook(root: HtmlElement, name: string): HtmlElement[] {\n return descendants(root).filter((element) => attr(element, \"data-bl\") === name);\n}\n\n/** The `<main>` element, or the whole document when the page has none. */\nexport function mainOf(root: HtmlElement): HtmlElement {\n return descendants(root).find((element) => element.tag === \"main\") ?? root;\n}\n\n/** The first `n` items of a list, then \"and N more\": for messages that name files or images. */\nexport function namedList(items: readonly string[], shown = 5): string {\n const head = items.slice(0, shown).join(\", \");\n return items.length > shown ? `${head} and ${items.length - shown} more` : head;\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library rule module (release 3.9.0): K1, shared components in use. It reads the declared tier from
|
|
3
|
+
* brandlab.config.ts so the enquiry-tier parts (quote form, reviews band, CTA band, footer) are required only where
|
|
4
|
+
* the tier covers them; a config it cannot read leaves the shell parts, which every tier requires.
|
|
5
|
+
*/
|
|
6
|
+
import type { SiteSystemFinding, SiteSystemTier } from "../../checks.js";
|
|
7
|
+
import { type EnquiryRuleInput } from "../enquiry/common.js";
|
|
8
|
+
export { K1_SHELL_PARTS, checkK1 } from "./k1-shared-components.js";
|
|
9
|
+
export { PageProblems, isContactPath, isThankYouPath, lastSegment, pagesPhrase } from "./common.js";
|
|
10
|
+
export interface LibraryRuleOptions {
|
|
11
|
+
/** Replaces the tier read from brandlab.config.ts (tests). */
|
|
12
|
+
readonly tier?: SiteSystemTier;
|
|
13
|
+
}
|
|
14
|
+
/** Runs K1 over the sampled pages. */
|
|
15
|
+
export declare function runLibraryRules(input: EnquiryRuleInput, options?: LibraryRuleOptions): Promise<readonly SiteSystemFinding[]>;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/library/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpG,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC;CAChC;AAUD,sCAAsC;AACtC,wBAAsB,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAGtI"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readSiteSystemProjectConfig } from "../../config.js";
|
|
2
|
+
import { parsePages } from "../enquiry/common.js";
|
|
3
|
+
import { checkK1 } from "./k1-shared-components.js";
|
|
4
|
+
export { K1_SHELL_PARTS, checkK1 } from "./k1-shared-components.js";
|
|
5
|
+
export { PageProblems, isContactPath, isThankYouPath, lastSegment, pagesPhrase } from "./common.js";
|
|
6
|
+
function declaredTier(projectRoot) {
|
|
7
|
+
try {
|
|
8
|
+
return readSiteSystemProjectConfig(projectRoot).siteSystem?.tier ?? "shell";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "shell";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Runs K1 over the sampled pages. */
|
|
15
|
+
export async function runLibraryRules(input, options = {}) {
|
|
16
|
+
const tier = options.tier ?? declaredTier(input.projectRoot);
|
|
17
|
+
return checkK1(parsePages(input.pages), tier);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/library/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAyB,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAOpG,SAAS,YAAY,CAAC,WAAmB;IACvC,IAAI,CAAC;QACH,OAAO,2BAA2B,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,IAAI,IAAI,OAAO,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,sCAAsC;AACtC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAuB,EAAE,UAA8B,EAAE;IAC7F,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC7D,OAAO,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC","sourcesContent":["/**\n * The library rule module (release 3.9.0): K1, shared components in use. It reads the declared tier from\n * brandlab.config.ts so the enquiry-tier parts (quote form, reviews band, CTA band, footer) are required only where\n * the tier covers them; a config it cannot read leaves the shell parts, which every tier requires.\n */\nimport type { SiteSystemFinding, SiteSystemTier } from \"../../checks.js\";\nimport { readSiteSystemProjectConfig } from \"../../config.js\";\nimport { parsePages, type EnquiryRuleInput } from \"../enquiry/common.js\";\nimport { checkK1 } from \"./k1-shared-components.js\";\n\nexport { K1_SHELL_PARTS, checkK1 } from \"./k1-shared-components.js\";\nexport { PageProblems, isContactPath, isThankYouPath, lastSegment, pagesPhrase } from \"./common.js\";\n\nexport interface LibraryRuleOptions {\n /** Replaces the tier read from brandlab.config.ts (tests). */\n readonly tier?: SiteSystemTier;\n}\n\nfunction declaredTier(projectRoot: string): SiteSystemTier {\n try {\n return readSiteSystemProjectConfig(projectRoot).siteSystem?.tier ?? \"shell\";\n } catch {\n return \"shell\";\n }\n}\n\n/** Runs K1 over the sampled pages. */\nexport async function runLibraryRules(input: EnquiryRuleInput, options: LibraryRuleOptions = {}): Promise<readonly SiteSystemFinding[]> {\n const tier = options.tier ?? declaredTier(input.projectRoot);\n return checkK1(parsePages(input.pages), tier);\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K1: shared components in use where the declared tier covers them (Site System v2 §13.2).
|
|
3
|
+
*
|
|
4
|
+
* The library components mark their root with `data-bl="<name>"` in the server HTML, so use is read from the rendered
|
|
5
|
+
* pages. The shell parts (header, drawer, call bar, back-to-top) are all in the server HTML: the call bar and
|
|
6
|
+
* back-to-top are client islands, but they render hidden and `inert` on the server (`data-state="hidden"`), so a page
|
|
7
|
+
* that lacks them has not mounted the library's shell. From the enquiry tier the quote form, reviews band, CTA band
|
|
8
|
+
* and footer must also be the library's wherever the page has such a part. The lint rule catches a literal `tel:`
|
|
9
|
+
* href in source; this rule is its rendered counterpart: no `tel:` link outside an element with
|
|
10
|
+
* `data-bl="phone-link"`, so a hand-written call link cannot hide behind a wrapper the lint cannot see through.
|
|
11
|
+
*/
|
|
12
|
+
import type { SiteSystemFinding, SiteSystemTier } from "../../checks.js";
|
|
13
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
14
|
+
/** The shell parts every page carries, by the tier that requires them. */
|
|
15
|
+
export declare const K1_SHELL_PARTS: readonly [{
|
|
16
|
+
readonly hook: "header";
|
|
17
|
+
readonly name: "header";
|
|
18
|
+
}, {
|
|
19
|
+
readonly hook: "drawer";
|
|
20
|
+
readonly name: "mobile drawer";
|
|
21
|
+
}, {
|
|
22
|
+
readonly hook: "callbar";
|
|
23
|
+
readonly name: "call bar";
|
|
24
|
+
}, {
|
|
25
|
+
readonly hook: "back-to-top";
|
|
26
|
+
readonly name: "back-to-top control";
|
|
27
|
+
}];
|
|
28
|
+
export declare function checkK1(pages: readonly ParsedPage[], tier?: SiteSystemTier): SiteSystemFinding[];
|
|
29
|
+
//# sourceMappingURL=k1-shared-components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k1-shared-components.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/library/k1-shared-components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKpF,0EAA0E;AAC1E,eAAO,MAAM,cAAc;;;;;;;;;;;;EAKjB,CAAC;AA8CX,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,GAAE,cAAwB,GAAG,iBAAiB,EAAE,CA8CzG"}
|