@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T6: every image has a media.ts entry with provenance (Site System v2 §13.2, F13).
|
|
3
|
+
*
|
|
4
|
+
* `src/data/media.ts` declares the manifest with `defineMedia({ id: { src, width, height, alt, provenance, rights … } })`.
|
|
5
|
+
* Every image file under `public/` and every image the built pages show (a `next/image` URL is read back to the file it
|
|
6
|
+
* optimises) must be the `src` of an entry, and every entry names its provenance (`genuine`, `stock` or
|
|
7
|
+
* `illustrative-ai`) and its rights. Logos, icons, SVG art and third-party profile photos (review avatars) are not
|
|
8
|
+
* photographs the business has to vouch for, so they need no entry.
|
|
9
|
+
*
|
|
10
|
+
* The manifest is read from the syntax tree, never run. A manifest the reader cannot resolve to plain data (a spread or
|
|
11
|
+
* a computed value) reports nothing rather than guessing.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { fail, notApplicable, pass, plural } from "../enquiry/common.js";
|
|
16
|
+
import { LiteralReader, UNRESOLVED, isFullyResolved, isLiteralObject } from "../enquiry/literal.js";
|
|
17
|
+
import { PageProblems, namedList } from "../library/common.js";
|
|
18
|
+
import { MEDIA_EXEMPT_NAME, imagesIn, needsMediaEntry, publicImages } from "./images.js";
|
|
19
|
+
export const T6_MEDIA_PATH = "src/data/media.ts";
|
|
20
|
+
export const T6_PROVENANCES = ["genuine", "stock", "illustrative-ai"];
|
|
21
|
+
/** "missing": no media.ts; "not-declared": no defineMedia call in it; null: unreadable; else the manifest. */
|
|
22
|
+
export function readMediaManifest(projectRoot) {
|
|
23
|
+
const file = join(projectRoot, T6_MEDIA_PATH);
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return "missing";
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = readFileSync(file, "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const reader = new LiteralReader(text, T6_MEDIA_PATH);
|
|
34
|
+
const [argument] = reader.callArguments("defineMedia");
|
|
35
|
+
if (!argument)
|
|
36
|
+
return "not-declared";
|
|
37
|
+
const value = reader.evaluate(argument);
|
|
38
|
+
if (!isLiteralObject(value))
|
|
39
|
+
return null;
|
|
40
|
+
const sources = new Map();
|
|
41
|
+
const entryProblems = [];
|
|
42
|
+
for (const [id, entry] of Object.entries(value)) {
|
|
43
|
+
if (!isLiteralObject(entry) || !isFullyResolved(entry.src) || typeof entry.src !== "string")
|
|
44
|
+
return null;
|
|
45
|
+
sources.set(id, entry.src);
|
|
46
|
+
if (!T6_PROVENANCES.includes(entry.provenance)) {
|
|
47
|
+
entryProblems.push(`media.${id} has no provenance (genuine, stock or illustrative-ai)`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof entry.rights !== "string" || entry.rights.trim() === "") {
|
|
50
|
+
if (entry.rights !== UNRESOLVED)
|
|
51
|
+
entryProblems.push(`media.${id} does not say who holds the rights`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { sources, entryProblems };
|
|
55
|
+
}
|
|
56
|
+
/** The part of an https URL entry that matches a page: the whole URL. */
|
|
57
|
+
function covered(sources, file) {
|
|
58
|
+
return sources.has(file);
|
|
59
|
+
}
|
|
60
|
+
export function checkT6(pages, projectRoot) {
|
|
61
|
+
const onDisk = publicImages(projectRoot).filter((image) => !/\.svg$/i.test(image.web) && !MEDIA_EXEMPT_NAME.test(image.web));
|
|
62
|
+
const shown = new Map();
|
|
63
|
+
for (const page of pages) {
|
|
64
|
+
for (const image of imagesIn(page.root)) {
|
|
65
|
+
if (needsMediaEntry(image))
|
|
66
|
+
shown.set(image.file, [...(shown.get(image.file) ?? []), page]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const manifest = readMediaManifest(projectRoot);
|
|
70
|
+
if (manifest === null)
|
|
71
|
+
return [];
|
|
72
|
+
if (manifest === "missing" || manifest === "not-declared") {
|
|
73
|
+
if (onDisk.length === 0 && shown.size === 0)
|
|
74
|
+
return [notApplicable("T6", "The site has no images to record in a media manifest.")];
|
|
75
|
+
const reason = manifest === "missing" ? `${T6_MEDIA_PATH} does not exist` : `${T6_MEDIA_PATH} does not declare the manifest with defineMedia`;
|
|
76
|
+
const count = onDisk.length > 0 ? plural(onDisk.length, "image file") + " in public/" : plural(shown.size, "image") + " on the pages";
|
|
77
|
+
return [fail("T6", T6_MEDIA_PATH, `${reason}, but the site has ${count}; every image needs a media.ts entry with provenance.`)];
|
|
78
|
+
}
|
|
79
|
+
const sources = new Set(manifest.sources.values());
|
|
80
|
+
const failures = manifest.entryProblems.map((problem) => fail("T6", T6_MEDIA_PATH, `${T6_MEDIA_PATH}: ${problem}.`));
|
|
81
|
+
const unlisted = onDisk.filter((image) => !covered(sources, image.web)).map((image) => image.web);
|
|
82
|
+
if (unlisted.length > 0) {
|
|
83
|
+
failures.push(fail("T6", "public", `${plural(unlisted.length, "image")} in public/ ${unlisted.length === 1 ? "has" : "have"} no entry in ${T6_MEDIA_PATH}: ${namedList(unlisted)}.`));
|
|
84
|
+
}
|
|
85
|
+
const problems = new PageProblems();
|
|
86
|
+
for (const page of pages) {
|
|
87
|
+
const missing = [...shown].filter(([file, owners]) => owners.includes(page) && !covered(sources, file)).map(([file]) => file);
|
|
88
|
+
if (missing.length > 0)
|
|
89
|
+
problems.add(page.path, `${plural(missing.length, "image")} shown with no entry in ${T6_MEDIA_PATH} (${namedList(missing, 3)})`);
|
|
90
|
+
}
|
|
91
|
+
failures.push(...problems.findings("T6"));
|
|
92
|
+
if (failures.length > 0)
|
|
93
|
+
return failures;
|
|
94
|
+
return [pass("T6", `Checked ${plural(onDisk.length, "image file")} in public/ and ${plural(shown.size, "image")} on the pages: each has an entry in ${T6_MEDIA_PATH} with provenance.`)];
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=t6-media-entries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t6-media-entries.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAgB,MAAM,uBAAuB,CAAC;AAClH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzF,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAC;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,iBAAiB,CAAU,CAAC;AAS/E,8GAA8G;AAC9G,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACvD,IAAI,CAAC,QAAQ;QAAE,OAAO,cAAc,CAAC;IACrC,MAAM,KAAK,GAAY,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACzG,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAE,cAAqC,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YACvE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,wDAAwD,CAAC,CAAC;QAC1F,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnE,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU;gBAAE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,oCAAoC,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,SAAS,OAAO,CAAC,OAA4B,EAAE,IAAY;IACzD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAA4B,EAAE,WAAmB;IACvE,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7H,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,IAAI,eAAe,CAAC,KAAK,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAChD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;QAC1D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,uDAAuD,CAAC,CAAC,CAAC;QACnI,MAAM,MAAM,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,aAAa,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,iDAAiD,CAAC;QAC9I,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,eAAe,CAAC;QACtI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,sBAAsB,KAAK,uDAAuD,CAAC,CAAC,CAAC;IAClI,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAwB,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,aAAa,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC;IAC1I,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClG,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,eAAe,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,gBAAgB,aAAa,KAAK,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACxL,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QAC9H,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,2BAA2B,aAAa,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3J,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,mBAAmB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,uCAAuC,aAAa,mBAAmB,CAAC,CAAC,CAAC;AAC3L,CAAC","sourcesContent":["/**\n * T6: every image has a media.ts entry with provenance (Site System v2 §13.2, F13).\n *\n * `src/data/media.ts` declares the manifest with `defineMedia({ id: { src, width, height, alt, provenance, rights … } })`.\n * Every image file under `public/` and every image the built pages show (a `next/image` URL is read back to the file it\n * optimises) must be the `src` of an entry, and every entry names its provenance (`genuine`, `stock` or\n * `illustrative-ai`) and its rights. Logos, icons, SVG art and third-party profile photos (review avatars) are not\n * photographs the business has to vouch for, so they need no entry.\n *\n * The manifest is read from the syntax tree, never run. A manifest the reader cannot resolve to plain data (a spread or\n * a computed value) reports nothing rather than guessing.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { LiteralReader, UNRESOLVED, isFullyResolved, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\nimport { PageProblems, namedList } from \"../library/common.js\";\nimport { MEDIA_EXEMPT_NAME, imagesIn, needsMediaEntry, publicImages } from \"./images.js\";\n\nexport const T6_MEDIA_PATH = \"src/data/media.ts\";\nexport const T6_PROVENANCES = [\"genuine\", \"stock\", \"illustrative-ai\"] as const;\n\nexport interface MediaManifest {\n /** Media id to its `src`. */\n readonly sources: ReadonlyMap<string, string>;\n /** Problems with entries, one plain fragment each (\"media.hero has no provenance\"). */\n readonly entryProblems: readonly string[];\n}\n\n/** \"missing\": no media.ts; \"not-declared\": no defineMedia call in it; null: unreadable; else the manifest. */\nexport function readMediaManifest(projectRoot: string): MediaManifest | \"missing\" | \"not-declared\" | null {\n const file = join(projectRoot, T6_MEDIA_PATH);\n if (!existsSync(file)) return \"missing\";\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return null;\n }\n const reader = new LiteralReader(text, T6_MEDIA_PATH);\n const [argument] = reader.callArguments(\"defineMedia\");\n if (!argument) return \"not-declared\";\n const value: Literal = reader.evaluate(argument);\n if (!isLiteralObject(value)) return null;\n const sources = new Map<string, string>();\n const entryProblems: string[] = [];\n for (const [id, entry] of Object.entries(value)) {\n if (!isLiteralObject(entry) || !isFullyResolved(entry.src) || typeof entry.src !== \"string\") return null;\n sources.set(id, entry.src);\n if (!(T6_PROVENANCES as readonly unknown[]).includes(entry.provenance)) {\n entryProblems.push(`media.${id} has no provenance (genuine, stock or illustrative-ai)`);\n }\n if (typeof entry.rights !== \"string\" || entry.rights.trim() === \"\") {\n if (entry.rights !== UNRESOLVED) entryProblems.push(`media.${id} does not say who holds the rights`);\n }\n }\n return { sources, entryProblems };\n}\n\n/** The part of an https URL entry that matches a page: the whole URL. */\nfunction covered(sources: ReadonlySet<string>, file: string): boolean {\n return sources.has(file);\n}\n\nexport function checkT6(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n const onDisk = publicImages(projectRoot).filter((image) => !/\\.svg$/i.test(image.web) && !MEDIA_EXEMPT_NAME.test(image.web));\n const shown = new Map<string, ParsedPage[]>();\n for (const page of pages) {\n for (const image of imagesIn(page.root)) {\n if (needsMediaEntry(image)) shown.set(image.file, [...(shown.get(image.file) ?? []), page]);\n }\n }\n\n const manifest = readMediaManifest(projectRoot);\n if (manifest === null) return [];\n if (manifest === \"missing\" || manifest === \"not-declared\") {\n if (onDisk.length === 0 && shown.size === 0) return [notApplicable(\"T6\", \"The site has no images to record in a media manifest.\")];\n const reason = manifest === \"missing\" ? `${T6_MEDIA_PATH} does not exist` : `${T6_MEDIA_PATH} does not declare the manifest with defineMedia`;\n const count = onDisk.length > 0 ? plural(onDisk.length, \"image file\") + \" in public/\" : plural(shown.size, \"image\") + \" on the pages\";\n return [fail(\"T6\", T6_MEDIA_PATH, `${reason}, but the site has ${count}; every image needs a media.ts entry with provenance.`)];\n }\n\n const sources = new Set(manifest.sources.values());\n const failures: SiteSystemFinding[] = manifest.entryProblems.map((problem) => fail(\"T6\", T6_MEDIA_PATH, `${T6_MEDIA_PATH}: ${problem}.`));\n const unlisted = onDisk.filter((image) => !covered(sources, image.web)).map((image) => image.web);\n if (unlisted.length > 0) {\n failures.push(fail(\"T6\", \"public\", `${plural(unlisted.length, \"image\")} in public/ ${unlisted.length === 1 ? \"has\" : \"have\"} no entry in ${T6_MEDIA_PATH}: ${namedList(unlisted)}.`));\n }\n const problems = new PageProblems();\n for (const page of pages) {\n const missing = [...shown].filter(([file, owners]) => owners.includes(page) && !covered(sources, file)).map(([file]) => file);\n if (missing.length > 0) problems.add(page.path, `${plural(missing.length, \"image\")} shown with no entry in ${T6_MEDIA_PATH} (${namedList(missing, 3)})`);\n }\n failures.push(...problems.findings(\"T6\"));\n if (failures.length > 0) return failures;\n return [pass(\"T6\", `Checked ${plural(onDisk.length, \"image file\")} in public/ and ${plural(shown.size, \"image\")} on the pages: each has an entry in ${T6_MEDIA_PATH} with provenance.`)];\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T7: no image is the main image of two pages (Site System v2 §13.2, F13).
|
|
3
|
+
*
|
|
4
|
+
* The main image of a page is the first image in its hero or page header. A page that has neither opens with its first
|
|
5
|
+
* image above the first H2. Two pages that open with the same photograph read as one page to a visitor and to a search
|
|
6
|
+
* engine, so each page gets its own.
|
|
7
|
+
*/
|
|
8
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
9
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
10
|
+
/** The file of the page's main image, or null when the page has none. */
|
|
11
|
+
export declare function mainImageOf(page: ParsedPage): string | null;
|
|
12
|
+
export declare function checkT7(pages: readonly ParsedPage[]): SiteSystemFinding[];
|
|
13
|
+
//# sourceMappingURL=t7-main-images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t7-main-images.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t7-main-images.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAkE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAWvH,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAU3D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,iBAAiB,EAAE,CAezE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { fail, notApplicable, pass, plural, sectionId, topLevelSections } from "../enquiry/common.js";
|
|
2
|
+
import { descendants } from "../enquiry/html.js";
|
|
3
|
+
import { mainOf } from "../library/common.js";
|
|
4
|
+
import { imageOf, imagesIn, isAvatar, MEDIA_EXEMPT_NAME } from "./images.js";
|
|
5
|
+
const LEAD_SECTIONS = new Set(["hero", "page-header"]);
|
|
6
|
+
function isContent(image) {
|
|
7
|
+
return !/\.svg$/i.test(image.file) && !MEDIA_EXEMPT_NAME.test(image.file) && !isAvatar(image.element);
|
|
8
|
+
}
|
|
9
|
+
/** The file of the page's main image, or null when the page has none. */
|
|
10
|
+
export function mainImageOf(page) {
|
|
11
|
+
const lead = topLevelSections(page.root).find((section) => LEAD_SECTIONS.has(sectionId(section)));
|
|
12
|
+
if (lead)
|
|
13
|
+
return imagesIn(lead).find(isContent)?.file ?? null;
|
|
14
|
+
// No hero or page header: the first image before the first H2 of the main content.
|
|
15
|
+
for (const element of descendants(mainOf(page.root))) {
|
|
16
|
+
if (element.tag === "h2")
|
|
17
|
+
return null;
|
|
18
|
+
const image = imageOf(element);
|
|
19
|
+
if (image && isContent(image))
|
|
20
|
+
return image.file;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function checkT7(pages) {
|
|
25
|
+
const owners = new Map();
|
|
26
|
+
let measured = 0;
|
|
27
|
+
for (const page of pages) {
|
|
28
|
+
const file = mainImageOf(page);
|
|
29
|
+
if (!file)
|
|
30
|
+
continue;
|
|
31
|
+
measured += 1;
|
|
32
|
+
owners.set(file, [...(owners.get(file) ?? []), page.path]);
|
|
33
|
+
}
|
|
34
|
+
if (measured === 0)
|
|
35
|
+
return [notApplicable("T7", "None of the sampled pages opens with an image in its hero or page header.")];
|
|
36
|
+
const shared = [...owners].filter(([, paths]) => paths.length > 1);
|
|
37
|
+
if (shared.length === 0)
|
|
38
|
+
return [pass("T7", `Checked the main image of ${plural(measured, "page")}: no two pages open with the same image.`)];
|
|
39
|
+
return shared.map(([file, paths]) => fail("T7", paths[0], `${file} is the main image of ${paths.length} pages (${paths.join(", ")}); each page opens with its own image.`));
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=t7-main-images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t7-main-images.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t7-main-images.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * T7: no image is the main image of two pages (Site System v2 §13.2, F13).\n *\n * The main image of a page is the first image in its hero or page header. A page that has neither opens with its first\n * image above the first H2. Two pages that open with the same photograph read as one page to a visitor and to a search\n * engine, so each page gets its own.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, sectionId, topLevelSections, type ParsedPage } from \"../enquiry/common.js\";\nimport { descendants } from \"../enquiry/html.js\";\nimport { mainOf } from \"../library/common.js\";\nimport { imageOf, imagesIn, isAvatar, MEDIA_EXEMPT_NAME, type PageImage } from \"./images.js\";\n\nconst LEAD_SECTIONS = new Set([\"hero\", \"page-header\"]);\n\nfunction isContent(image: PageImage): boolean {\n return !/\\.svg$/i.test(image.file) && !MEDIA_EXEMPT_NAME.test(image.file) && !isAvatar(image.element);\n}\n\n/** The file of the page's main image, or null when the page has none. */\nexport function mainImageOf(page: ParsedPage): string | null {\n const lead = topLevelSections(page.root).find((section) => LEAD_SECTIONS.has(sectionId(section)));\n if (lead) return imagesIn(lead).find(isContent)?.file ?? null;\n // No hero or page header: the first image before the first H2 of the main content.\n for (const element of descendants(mainOf(page.root))) {\n if (element.tag === \"h2\") return null;\n const image = imageOf(element);\n if (image && isContent(image)) return image.file;\n }\n return null;\n}\n\nexport function checkT7(pages: readonly ParsedPage[]): SiteSystemFinding[] {\n const owners = new Map<string, string[]>();\n let measured = 0;\n for (const page of pages) {\n const file = mainImageOf(page);\n if (!file) continue;\n measured += 1;\n owners.set(file, [...(owners.get(file) ?? []), page.path]);\n }\n if (measured === 0) return [notApplicable(\"T7\", \"None of the sampled pages opens with an image in its hero or page header.\")];\n const shared = [...owners].filter(([, paths]) => paths.length > 1);\n if (shared.length === 0) return [pass(\"T7\", `Checked the main image of ${plural(measured, \"page\")}: no two pages open with the same image.`)];\n return shared.map(([file, paths]) =>\n fail(\"T7\", paths[0], `${file} is the main image of ${paths.length} pages (${paths.join(\", \")}); each page opens with its own image.`),\n );\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T8: how prices are shown (Site System v2 §13.2, NSW Fair Trading: show the full price).
|
|
3
|
+
*
|
|
4
|
+
* - every price table row carries the GST flag ("incl. GST" or "excl. GST"), on the row or in the table's caption,
|
|
5
|
+
* header or section lead;
|
|
6
|
+
* - no "+ GST" price anywhere, and no "from $X" teaser in a card, a link or a menu: the full price is shown, never a
|
|
7
|
+
* lower figure to draw people in;
|
|
8
|
+
* - one price band per job across the whole site: the same job never appears with two different bands.
|
|
9
|
+
*/
|
|
10
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
11
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
12
|
+
export declare function checkT8(pages: readonly ParsedPage[]): SiteSystemFinding[];
|
|
13
|
+
//# sourceMappingURL=t8-prices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t8-prices.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t8-prices.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAuD1F,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,iBAAiB,EAAE,CAwDzE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { dollarFigures, fail, pass, plural } from "../enquiry/common.js";
|
|
2
|
+
import { ancestors, attr, closest, descendants, normaliseText, textOf } from "../enquiry/html.js";
|
|
3
|
+
import { PageProblems, pagesPhrase } from "../library/common.js";
|
|
4
|
+
const PLUS_GST = /\+\s*GST\b|\bplus\s+GST\b/i;
|
|
5
|
+
// "from $400" is a teaser; "from $400 to $900" and "from $400–900" are the full band and pass.
|
|
6
|
+
const FROM_TEASER = /\b(?:from|starting\s+(?:at|from)|starts?\s+(?:at|from)|as\s+low\s+as)\s+(?:only\s+)?\$\s?\d[\d,]*(?![\d,]|(?:\.\d+)?\s*(?:to|-|–|—)\s*\$?\s?\d)/i;
|
|
7
|
+
const GST = /\bGST\b/i;
|
|
8
|
+
const CARD_SLOTS = new Set(["card", "tile", "teaser", "project", "service", "post", "package"]);
|
|
9
|
+
const MENU_HOOKS = new Set(["header", "drawer", "utility", "footer"]);
|
|
10
|
+
const CARD_CLASS = /(?:^|[\s_-])card(?:[\s_-]|$)/i;
|
|
11
|
+
/** An `<article>` is a card only in a list or grid of them; a post wrapped whole in one `<article>` is a page. */
|
|
12
|
+
function isArticleCard(element) {
|
|
13
|
+
return ancestors(element).some((parent) => parent.tag === "ul" || parent.tag === "ol" || ["list", "grid"].includes(attr(parent, "data-slot") ?? "") || /(?:^|[\s_-])grid(?:[\s_-]|$)/i.test(attr(parent, "class") ?? ""));
|
|
14
|
+
}
|
|
15
|
+
/** Cards and menus: a card slot, an `<article>` in a list or grid, a card class, the header, drawer and footer, a `<nav>`, and any link. */
|
|
16
|
+
function isCardOrMenu(element) {
|
|
17
|
+
const slot = attr(element, "data-slot") ?? "";
|
|
18
|
+
const hook = attr(element, "data-bl") ?? "";
|
|
19
|
+
return ((element.tag === "article" && isArticleCard(element)) ||
|
|
20
|
+
element.tag === "nav" ||
|
|
21
|
+
element.tag === "a" ||
|
|
22
|
+
CARD_SLOTS.has(slot) ||
|
|
23
|
+
MENU_HOOKS.has(hook) ||
|
|
24
|
+
CARD_CLASS.test(attr(element, "class") ?? ""));
|
|
25
|
+
}
|
|
26
|
+
function rowJob(row) {
|
|
27
|
+
const labelled = descendants(row).find((cell) => attr(cell, "data-slot") === "job" || (cell.tag === "th" && attr(cell, "scope") === "row"));
|
|
28
|
+
const cell = labelled ?? descendants(row).find((candidate) => candidate.tag === "td" || candidate.tag === "th");
|
|
29
|
+
return cell ? normaliseText(textOf(cell)).toLowerCase() : "";
|
|
30
|
+
}
|
|
31
|
+
/** The price cell's text, without the hidden column labels the library repeats for narrow widths. */
|
|
32
|
+
function rowPriceText(row) {
|
|
33
|
+
return textOf(row, false, (element) => attr(element, "aria-hidden") === "true");
|
|
34
|
+
}
|
|
35
|
+
function sectionOf(element) {
|
|
36
|
+
return closest(element, (candidate) => attr(candidate, "data-bl-section") !== undefined);
|
|
37
|
+
}
|
|
38
|
+
export function checkT8(pages) {
|
|
39
|
+
const problems = new PageProblems();
|
|
40
|
+
const rows = [];
|
|
41
|
+
let tables = 0;
|
|
42
|
+
for (const page of pages) {
|
|
43
|
+
for (const table of descendants(page.root).filter((element) => element.tag === "table")) {
|
|
44
|
+
const dataRows = descendants(table).filter((row) => row.tag === "tr" && descendants(row).some((cell) => cell.tag === "td"));
|
|
45
|
+
const priced = dataRows.filter((row) => dollarFigures(rowPriceText(row)).length > 0);
|
|
46
|
+
if (priced.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
tables += 1;
|
|
49
|
+
const header = descendants(table).filter((element) => element.tag === "caption" || element.tag === "thead").map((element) => textOf(element)).join(" ");
|
|
50
|
+
const lead = sectionOf(table) ? textOf(sectionOf(table)) : "";
|
|
51
|
+
const flaggedOnTable = GST.test(header) || (GST.test(lead) && !PLUS_GST.test(lead));
|
|
52
|
+
for (const row of priced) {
|
|
53
|
+
if (!flaggedOnTable && !GST.test(rowPriceText(row))) {
|
|
54
|
+
problems.add(page.path, `A price table row without the GST flag ("${rowJob(row) || "unnamed job"}")`);
|
|
55
|
+
}
|
|
56
|
+
const figures = dollarFigures(rowPriceText(row)).slice(0, 2);
|
|
57
|
+
rows.push({ job: rowJob(row), band: figures.join("-"), path: page.path });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const text = textOf(page.root);
|
|
61
|
+
if (PLUS_GST.test(text))
|
|
62
|
+
problems.add(page.path, `A "+ GST" price; GST is flagged as "incl. GST" or "excl. GST" beside the full price`);
|
|
63
|
+
const teaser = descendants(page.root).find((element) => isCardOrMenu(element) && element.tag !== "#root" && FROM_TEASER.test(textOf(element)) && !closest(element.parent ?? element, (parent) => parent.tag === "table"));
|
|
64
|
+
if (teaser)
|
|
65
|
+
problems.add(page.path, `A "from $X" teaser in a card, link or menu; show the full price band or none`);
|
|
66
|
+
}
|
|
67
|
+
const bands = new Map();
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
if (!row.job)
|
|
70
|
+
continue;
|
|
71
|
+
const byBand = bands.get(row.job) ?? new Map();
|
|
72
|
+
byBand.set(row.band, [...(byBand.get(row.band) ?? []), row.path]);
|
|
73
|
+
bands.set(row.job, byBand);
|
|
74
|
+
}
|
|
75
|
+
const conflicts = [];
|
|
76
|
+
for (const [job, byBand] of bands) {
|
|
77
|
+
if (byBand.size <= 1)
|
|
78
|
+
continue;
|
|
79
|
+
const money = (figure) => `$${Number(figure).toLocaleString("en-AU")}`;
|
|
80
|
+
const shown = [...byBand].map(([band, paths]) => `${band.split("-").map(money).join(" to ")} on ${pagesPhrase(paths)}`);
|
|
81
|
+
conflicts.push(fail("T8", [...byBand.values()][0][0], `The job "${job}" has ${byBand.size} price bands (${shown.join("; ")}); a job has one band across the site.`));
|
|
82
|
+
}
|
|
83
|
+
const failures = [...problems.findings("T8"), ...conflicts];
|
|
84
|
+
if (failures.length > 0)
|
|
85
|
+
return failures;
|
|
86
|
+
return [
|
|
87
|
+
pass("T8", tables === 0
|
|
88
|
+
? `Checked ${plural(pages.length, "page")}: no price tables, no "+ GST" and no "from $X" teasers.`
|
|
89
|
+
: `Checked ${plural(tables, "price table")} on ${plural(pages.length, "page")}: every row is flagged for GST, each job has one band, and no card or menu teases a "from" price.`),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=t8-prices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t8-prices.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t8-prices.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAoB,MAAM,oBAAoB,CAAC;AACpH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,MAAM,QAAQ,GAAG,4BAA4B,CAAC;AAC9C,+FAA+F;AAC/F,MAAM,WAAW,GAAG,kJAAkJ,CAAC;AACvK,MAAM,GAAG,GAAG,UAAU,CAAC;AACvB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAChG,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEtE,MAAM,UAAU,GAAG,+BAA+B,CAAC;AAEnD,kHAAkH;AAClH,SAAS,aAAa,CAAC,OAAoB;IACzC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAC5B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,IAAI,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAC1L,CAAC;AACJ,CAAC;AAED,4IAA4I;AAC5I,SAAS,YAAY,CAAC,OAAoB;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC;IAC5C,OAAO,CACL,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,KAAK,KAAK;QACrB,OAAO,CAAC,GAAG,KAAK,GAAG;QACnB,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAC9C,CAAC;AACJ,CAAC;AAQD,SAAS,MAAM,CAAC,GAAgB;IAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;IAC5I,MAAM,IAAI,GAAG,QAAQ,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,IAAI,IAAI,SAAS,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IAChH,OAAO,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED,qGAAqG;AACrG,SAAS,YAAY,CAAC,GAAgB;IACpC,OAAO,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,KAAK,MAAM,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,SAAS,CAAC,OAAoB;IACrC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,KAAK,SAAS,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAA4B;IAClD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;IACpC,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,CAAC;YACxF,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC;YAC5H,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAClC,MAAM,IAAI,CAAC,CAAC;YACZ,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxJ,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,MAAM,cAAc,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACpF,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBACzB,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBACpD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,4CAA4C,MAAM,CAAC,GAAG,CAAC,IAAI,aAAa,IAAI,CAAC,CAAC;gBACxG,CAAC;gBACD,MAAM,OAAO,GAAG,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7D,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,qFAAqF,CAAC,CAAC;QACxI,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACxC,CAAC,OAAO,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,KAAK,OAAO,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,CAC9K,CAAC;QACF,IAAI,MAAM;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,8EAA8E,CAAC,CAAC;IACtH,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiC,CAAC;IACvD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,GAAG;YAAE,SAAS;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAoB,CAAC;QACjE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,SAAS,GAAwB,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QAClC,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC;YAAE,SAAS;QAC/B,MAAM,KAAK,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxH,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,SAAS,MAAM,CAAC,IAAI,iBAAiB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC,CAAC;IACxK,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC,CAAC;IAC5D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACzC,OAAO;QACL,IAAI,CACF,IAAI,EACJ,MAAM,KAAK,CAAC;YACV,CAAC,CAAC,WAAW,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,yDAAyD;YAClG,CAAC,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,mGAAmG,CACnL;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * T8: how prices are shown (Site System v2 §13.2, NSW Fair Trading: show the full price).\n *\n * - every price table row carries the GST flag (\"incl. GST\" or \"excl. GST\"), on the row or in the table's caption,\n * header or section lead;\n * - no \"+ GST\" price anywhere, and no \"from $X\" teaser in a card, a link or a menu: the full price is shown, never a\n * lower figure to draw people in;\n * - one price band per job across the whole site: the same job never appears with two different bands.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { dollarFigures, fail, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { ancestors, attr, closest, descendants, normaliseText, textOf, type HtmlElement } from \"../enquiry/html.js\";\nimport { PageProblems, pagesPhrase } from \"../library/common.js\";\n\nconst PLUS_GST = /\\+\\s*GST\\b|\\bplus\\s+GST\\b/i;\n// \"from $400\" is a teaser; \"from $400 to $900\" and \"from $400–900\" are the full band and pass.\nconst FROM_TEASER = /\\b(?:from|starting\\s+(?:at|from)|starts?\\s+(?:at|from)|as\\s+low\\s+as)\\s+(?:only\\s+)?\\$\\s?\\d[\\d,]*(?![\\d,]|(?:\\.\\d+)?\\s*(?:to|-|–|—)\\s*\\$?\\s?\\d)/i;\nconst GST = /\\bGST\\b/i;\nconst CARD_SLOTS = new Set([\"card\", \"tile\", \"teaser\", \"project\", \"service\", \"post\", \"package\"]);\nconst MENU_HOOKS = new Set([\"header\", \"drawer\", \"utility\", \"footer\"]);\n\nconst CARD_CLASS = /(?:^|[\\s_-])card(?:[\\s_-]|$)/i;\n\n/** An `<article>` is a card only in a list or grid of them; a post wrapped whole in one `<article>` is a page. */\nfunction isArticleCard(element: HtmlElement): boolean {\n return ancestors(element).some(\n (parent) => parent.tag === \"ul\" || parent.tag === \"ol\" || [\"list\", \"grid\"].includes(attr(parent, \"data-slot\") ?? \"\") || /(?:^|[\\s_-])grid(?:[\\s_-]|$)/i.test(attr(parent, \"class\") ?? \"\"),\n );\n}\n\n/** Cards and menus: a card slot, an `<article>` in a list or grid, a card class, the header, drawer and footer, a `<nav>`, and any link. */\nfunction isCardOrMenu(element: HtmlElement): boolean {\n const slot = attr(element, \"data-slot\") ?? \"\";\n const hook = attr(element, \"data-bl\") ?? \"\";\n return (\n (element.tag === \"article\" && isArticleCard(element)) ||\n element.tag === \"nav\" ||\n element.tag === \"a\" ||\n CARD_SLOTS.has(slot) ||\n MENU_HOOKS.has(hook) ||\n CARD_CLASS.test(attr(element, \"class\") ?? \"\")\n );\n}\n\ninterface PriceRow {\n readonly job: string;\n readonly band: string;\n readonly path: string;\n}\n\nfunction rowJob(row: HtmlElement): string {\n const labelled = descendants(row).find((cell) => attr(cell, \"data-slot\") === \"job\" || (cell.tag === \"th\" && attr(cell, \"scope\") === \"row\"));\n const cell = labelled ?? descendants(row).find((candidate) => candidate.tag === \"td\" || candidate.tag === \"th\");\n return cell ? normaliseText(textOf(cell)).toLowerCase() : \"\";\n}\n\n/** The price cell's text, without the hidden column labels the library repeats for narrow widths. */\nfunction rowPriceText(row: HtmlElement): string {\n return textOf(row, false, (element) => attr(element, \"aria-hidden\") === \"true\");\n}\n\nfunction sectionOf(element: HtmlElement): HtmlElement | null {\n return closest(element, (candidate) => attr(candidate, \"data-bl-section\") !== undefined);\n}\n\nexport function checkT8(pages: readonly ParsedPage[]): SiteSystemFinding[] {\n const problems = new PageProblems();\n const rows: PriceRow[] = [];\n let tables = 0;\n\n for (const page of pages) {\n for (const table of descendants(page.root).filter((element) => element.tag === \"table\")) {\n const dataRows = descendants(table).filter((row) => row.tag === \"tr\" && descendants(row).some((cell) => cell.tag === \"td\"));\n const priced = dataRows.filter((row) => dollarFigures(rowPriceText(row)).length > 0);\n if (priced.length === 0) continue;\n tables += 1;\n const header = descendants(table).filter((element) => element.tag === \"caption\" || element.tag === \"thead\").map((element) => textOf(element)).join(\" \");\n const lead = sectionOf(table) ? textOf(sectionOf(table)!) : \"\";\n const flaggedOnTable = GST.test(header) || (GST.test(lead) && !PLUS_GST.test(lead));\n for (const row of priced) {\n if (!flaggedOnTable && !GST.test(rowPriceText(row))) {\n problems.add(page.path, `A price table row without the GST flag (\"${rowJob(row) || \"unnamed job\"}\")`);\n }\n const figures = dollarFigures(rowPriceText(row)).slice(0, 2);\n rows.push({ job: rowJob(row), band: figures.join(\"-\"), path: page.path });\n }\n }\n\n const text = textOf(page.root);\n if (PLUS_GST.test(text)) problems.add(page.path, `A \"+ GST\" price; GST is flagged as \"incl. GST\" or \"excl. GST\" beside the full price`);\n const teaser = descendants(page.root).find(\n (element) => isCardOrMenu(element) && element.tag !== \"#root\" && FROM_TEASER.test(textOf(element)) && !closest(element.parent ?? element, (parent) => parent.tag === \"table\"),\n );\n if (teaser) problems.add(page.path, `A \"from $X\" teaser in a card, link or menu; show the full price band or none`);\n }\n\n const bands = new Map<string, Map<string, string[]>>();\n for (const row of rows) {\n if (!row.job) continue;\n const byBand = bands.get(row.job) ?? new Map<string, string[]>();\n byBand.set(row.band, [...(byBand.get(row.band) ?? []), row.path]);\n bands.set(row.job, byBand);\n }\n const conflicts: SiteSystemFinding[] = [];\n for (const [job, byBand] of bands) {\n if (byBand.size <= 1) continue;\n const money = (figure: string) => `$${Number(figure).toLocaleString(\"en-AU\")}`;\n const shown = [...byBand].map(([band, paths]) => `${band.split(\"-\").map(money).join(\" to \")} on ${pagesPhrase(paths)}`);\n conflicts.push(fail(\"T8\", [...byBand.values()][0]![0], `The job \"${job}\" has ${byBand.size} price bands (${shown.join(\"; \")}); a job has one band across the site.`));\n }\n\n const failures = [...problems.findings(\"T8\"), ...conflicts];\n if (failures.length > 0) return failures;\n return [\n pass(\n \"T8\",\n tables === 0\n ? `Checked ${plural(pages.length, \"page\")}: no price tables, no \"+ GST\" and no \"from $X\" teasers.`\n : `Checked ${plural(tables, \"price table\")} on ${plural(pages.length, \"page\")}: every row is flagged for GST, each job has one band, and no card or menu teases a \"from\" price.`,\n ),\n ];\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t9-location-metadata.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t9-location-metadata.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAWzD,wBAAgB,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CA+BhE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T9: no image in public/ carries location metadata (Site System v2 §13.2, §7.5).
|
|
3
|
+
*
|
|
4
|
+
* A phone photo records the GPS position of the job and often the suburb. Shipping it publishes the customer's address
|
|
5
|
+
* to anyone who downloads the file. Every image under `public/` is read for EXIF GPS, XMP and IPTC location fields
|
|
6
|
+
* (see exif.ts); the photos are stripped before they are committed (the media step does this for new photos).
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { fail, notApplicable, pass, plural } from "../enquiry/common.js";
|
|
10
|
+
import { locationMetadata } from "./exif.js";
|
|
11
|
+
import { scanPublicImages } from "./images.js";
|
|
12
|
+
const RASTER = /\.(?:jpe?g|png|webp|heic|heif|avif)$/i;
|
|
13
|
+
/** Files larger than this are skipped; a photo this big is not a site image. */
|
|
14
|
+
/** How many images are named one by one before the rest are counted. */
|
|
15
|
+
const MAX_LISTED = 10;
|
|
16
|
+
const MAX_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
export function checkT9(projectRoot) {
|
|
18
|
+
const scan = scanPublicImages(projectRoot);
|
|
19
|
+
const images = scan.images.filter((image) => RASTER.test(image.web));
|
|
20
|
+
if (images.length === 0 && scan.unreadableDirectories === 0)
|
|
21
|
+
return [notApplicable("T9", "The project has no raster images in public/.")];
|
|
22
|
+
const failures = [];
|
|
23
|
+
let read = 0;
|
|
24
|
+
// Files and folders the rule could not read: with nothing flagged, it cannot say the images are clean.
|
|
25
|
+
let skipped = scan.unreadableDirectories;
|
|
26
|
+
for (const image of images) {
|
|
27
|
+
let bytes;
|
|
28
|
+
try {
|
|
29
|
+
if (statSync(image.file).size > MAX_BYTES) {
|
|
30
|
+
skipped += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
bytes = readFileSync(image.file);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
skipped += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
read += 1;
|
|
40
|
+
const { found } = locationMetadata(bytes);
|
|
41
|
+
if (found.length > 0)
|
|
42
|
+
failures.push(fail("T9", `public${image.web}`, `public${image.web} carries location metadata (${found.join("; ")}); strip it before the image ships.`));
|
|
43
|
+
}
|
|
44
|
+
if (failures.length > MAX_LISTED) {
|
|
45
|
+
const rest = failures.length - MAX_LISTED;
|
|
46
|
+
return [...failures.slice(0, MAX_LISTED), fail("T9", "public", `${plural(rest, "more image")} in public/ carry${rest === 1 ? "es" : ""} location metadata too; strip them all before they ship.`)];
|
|
47
|
+
}
|
|
48
|
+
if (failures.length > 0)
|
|
49
|
+
return failures;
|
|
50
|
+
if (skipped > 0 || read === 0)
|
|
51
|
+
return [];
|
|
52
|
+
return [pass("T9", `Read ${plural(read, "image")} in public/: none carries GPS, XMP or IPTC location metadata.`)];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=t9-location-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t9-location-metadata.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t9-location-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,GAAG,uCAAuC,CAAC;AACvD,gFAAgF;AAChF,wEAAwE;AACxE,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnC,MAAM,UAAU,OAAO,CAAC,WAAmB;IACzC,MAAM,IAAI,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACrE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,qBAAqB,KAAK,CAAC;QAAE,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,8CAA8C,CAAC,CAAC,CAAC;IAC1I,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,uGAAuG;IACvG,IAAI,OAAO,GAAG,IAAI,CAAC,qBAAqB,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAiB,CAAC;QACtB,IAAI,CAAC;YACH,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC;gBAC1C,OAAO,IAAI,CAAC,CAAC;gBACb,SAAS;YACX,CAAC;YACD,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,CAAC;QACV,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,KAAK,CAAC,GAAG,+BAA+B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC,CAAC;IAChL,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC;QAC1C,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,oBAAoB,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,0DAA0D,CAAC,CAAC,CAAC;IACrM,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACzC,IAAI,OAAO,GAAG,CAAC,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,+DAA+D,CAAC,CAAC,CAAC;AACpH,CAAC","sourcesContent":["/**\n * T9: no image in public/ carries location metadata (Site System v2 §13.2, §7.5).\n *\n * A phone photo records the GPS position of the job and often the suburb. Shipping it publishes the customer's address\n * to anyone who downloads the file. Every image under `public/` is read for EXIF GPS, XMP and IPTC location fields\n * (see exif.ts); the photos are stripped before they are committed (the media step does this for new photos).\n */\nimport { readFileSync, statSync } from \"node:fs\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural } from \"../enquiry/common.js\";\nimport { locationMetadata } from \"./exif.js\";\nimport { scanPublicImages } from \"./images.js\";\n\nconst RASTER = /\\.(?:jpe?g|png|webp|heic|heif|avif)$/i;\n/** Files larger than this are skipped; a photo this big is not a site image. */\n/** How many images are named one by one before the rest are counted. */\nconst MAX_LISTED = 10;\nconst MAX_BYTES = 64 * 1024 * 1024;\n\nexport function checkT9(projectRoot: string): SiteSystemFinding[] {\n const scan = scanPublicImages(projectRoot);\n const images = scan.images.filter((image) => RASTER.test(image.web));\n if (images.length === 0 && scan.unreadableDirectories === 0) return [notApplicable(\"T9\", \"The project has no raster images in public/.\")];\n const failures: SiteSystemFinding[] = [];\n let read = 0;\n // Files and folders the rule could not read: with nothing flagged, it cannot say the images are clean.\n let skipped = scan.unreadableDirectories;\n for (const image of images) {\n let bytes: Uint8Array;\n try {\n if (statSync(image.file).size > MAX_BYTES) {\n skipped += 1;\n continue;\n }\n bytes = readFileSync(image.file);\n } catch {\n skipped += 1;\n continue;\n }\n read += 1;\n const { found } = locationMetadata(bytes);\n if (found.length > 0) failures.push(fail(\"T9\", `public${image.web}`, `public${image.web} carries location metadata (${found.join(\"; \")}); strip it before the image ships.`));\n }\n if (failures.length > MAX_LISTED) {\n const rest = failures.length - MAX_LISTED;\n return [...failures.slice(0, MAX_LISTED), fail(\"T9\", \"public\", `${plural(rest, \"more image\")} in public/ carry${rest === 1 ? \"es\" : \"\"} location metadata too; strip them all before they ship.`)];\n }\n if (failures.length > 0) return failures;\n if (skipped > 0 || read === 0) return [];\n return [pass(\"T9\", `Read ${plural(read, \"image\")} in public/: none carries GPS, XMP or IPTC location metadata.`)];\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ShellPage } from "./page.js";
|
|
2
|
+
import { type ShellFinding, type ShellProjectFacts } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* E8: the banned list, all three layers (spec 6.3). Layer 1 holds everywhere (helper and reassurance filler, text that
|
|
5
|
+
* explains the site or the quoting process, price sources and menu price teasers, template wording). Layer 2 holds on
|
|
6
|
+
* headers, heroes and location pages and is allowed in the footer, on the contact page and in schema. Layer 3 is the
|
|
7
|
+
* trade claims scan: each pattern needs evidence in the claims register. Until a register reader exists, layer 3 is
|
|
8
|
+
* reported as a warning (status "pass", severity "warning") naming the phrase and page; claims in `recordedClaims`
|
|
9
|
+
* are not reported. A customer's own words (review text and feed blocks) are exempt from every layer.
|
|
10
|
+
* Template wording reuses the existing public-copy patterns.
|
|
11
|
+
*/
|
|
12
|
+
export interface BannedPattern {
|
|
13
|
+
id: string;
|
|
14
|
+
layer: 1 | 2 | 3;
|
|
15
|
+
/** What the match is, completing "shows …". */
|
|
16
|
+
label: string;
|
|
17
|
+
regex: RegExp;
|
|
18
|
+
}
|
|
19
|
+
export declare const BANNED_PATTERNS: readonly BannedPattern[];
|
|
20
|
+
/** E8 over the sampled pages. */
|
|
21
|
+
export declare function checkBannedWording(pages: readonly ShellPage[], facts: ShellProjectFacts | null, recordedClaims?: readonly string[]): ShellFinding[];
|
|
22
|
+
/** Used by tests to confirm which patterns exist for each layer. */
|
|
23
|
+
export declare function bannedPatternIds(layer: 1 | 2 | 3): string[];
|
|
24
|
+
//# sourceMappingURL=banned-wording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banned-wording.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/banned-wording.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,SAAS,EAAiB,MAAM,WAAW,CAAC;AACrF,OAAO,EAA0D,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/H;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAgEnD,CAAC;AA8EF,iCAAiC;AACjC,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,iBAAiB,GAAG,IAAI,EAC/B,cAAc,GAAE,SAAS,MAAM,EAAO,GACrC,YAAY,EAAE,CA0EhB;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAE3D"}
|