@brandlab-digital/conformance 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/current-release.js +5 -5
  2. package/dist/current-release.js.map +1 -1
  3. package/dist/engineering.js +4 -2
  4. package/dist/engineering.js.map +1 -1
  5. package/dist/site-system/checks.d.ts +1 -1
  6. package/dist/site-system/checks.js +1 -1
  7. package/dist/site-system/checks.js.map +1 -1
  8. package/dist/site-system/rule-modules.d.ts.map +1 -1
  9. package/dist/site-system/rule-modules.js +22 -1
  10. package/dist/site-system/rule-modules.js.map +1 -1
  11. package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
  12. package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
  13. package/dist/site-system/rules/form/c11-contact-route.js +132 -0
  14. package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
  15. package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
  16. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
  17. package/dist/site-system/rules/form/c7-thank-you.js +67 -0
  18. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
  19. package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
  20. package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
  21. package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
  22. package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
  23. package/dist/site-system/rules/form/index.d.ts +14 -0
  24. package/dist/site-system/rules/form/index.d.ts.map +1 -0
  25. package/dist/site-system/rules/form/index.js +13 -0
  26. package/dist/site-system/rules/form/index.js.map +1 -0
  27. package/dist/site-system/rules/library/common.d.ts +36 -0
  28. package/dist/site-system/rules/library/common.d.ts.map +1 -0
  29. package/dist/site-system/rules/library/common.js +70 -0
  30. package/dist/site-system/rules/library/common.js.map +1 -0
  31. package/dist/site-system/rules/library/index.d.ts +16 -0
  32. package/dist/site-system/rules/library/index.d.ts.map +1 -0
  33. package/dist/site-system/rules/library/index.js +19 -0
  34. package/dist/site-system/rules/library/index.js.map +1 -0
  35. package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
  36. package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
  37. package/dist/site-system/rules/library/k1-shared-components.js +97 -0
  38. package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
  39. package/dist/site-system/rules/navigation/extract.d.ts +50 -0
  40. package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
  41. package/dist/site-system/rules/navigation/extract.js +220 -0
  42. package/dist/site-system/rules/navigation/extract.js.map +1 -0
  43. package/dist/site-system/rules/navigation/html.d.ts +23 -0
  44. package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
  45. package/dist/site-system/rules/navigation/html.js +219 -0
  46. package/dist/site-system/rules/navigation/html.js.map +1 -0
  47. package/dist/site-system/rules/navigation/index.d.ts +37 -0
  48. package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
  49. package/dist/site-system/rules/navigation/index.js +163 -0
  50. package/dist/site-system/rules/navigation/index.js.map +1 -0
  51. package/dist/site-system/rules/navigation/record.d.ts +8 -0
  52. package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
  53. package/dist/site-system/rules/navigation/record.js +39 -0
  54. package/dist/site-system/rules/navigation/record.js.map +1 -0
  55. package/dist/site-system/rules/sections/exif.d.ts +22 -0
  56. package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
  57. package/dist/site-system/rules/sections/exif.js +218 -0
  58. package/dist/site-system/rules/sections/exif.js.map +1 -0
  59. package/dist/site-system/rules/sections/images.d.ts +31 -0
  60. package/dist/site-system/rules/sections/images.d.ts.map +1 -0
  61. package/dist/site-system/rules/sections/images.js +96 -0
  62. package/dist/site-system/rules/sections/images.js.map +1 -0
  63. package/dist/site-system/rules/sections/index.d.ts +20 -0
  64. package/dist/site-system/rules/sections/index.d.ts.map +1 -0
  65. package/dist/site-system/rules/sections/index.js +34 -0
  66. package/dist/site-system/rules/sections/index.js.map +1 -0
  67. package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
  68. package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
  69. package/dist/site-system/rules/sections/k11-density.js +88 -0
  70. package/dist/site-system/rules/sections/k11-density.js.map +1 -0
  71. package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
  72. package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
  73. package/dist/site-system/rules/sections/k12-contact.js +70 -0
  74. package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
  75. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
  76. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
  77. package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
  78. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
  79. package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
  80. package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
  81. package/dist/site-system/rules/sections/n9-footer.js +186 -0
  82. package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
  83. package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
  84. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
  85. package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
  86. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
  87. package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
  88. package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
  89. package/dist/site-system/rules/sections/t7-main-images.js +41 -0
  90. package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
  91. package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
  92. package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
  93. package/dist/site-system/rules/sections/t8-prices.js +92 -0
  94. package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
  95. package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
  96. package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
  97. package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
  98. package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
  99. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"n9-footer.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/n9-footer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * N9: footer content (Site System v2 §13.2, v1 §6).\n *\n * - Column 1 holds the logo, a short description, the licence numbers (when the business holds licences), social and\n * profile links, and the contact details (a `tel:` link; an email when public).\n * - Then Services, Areas (regions or popular areas) and Company, each under a real heading (an H2 to H6 with text and\n * links beneath it), in that order. A site with no areas to list may leave that column out: it is reported as a\n * warning, not a failure, because the library lets a site leave out a column it has nothing true for.\n * - The bottom bar holds © and the trading name, the ABN and the legal links (privacy and terms).\n * - Never a call-to-action button, \"based in\", opening hours or notes about hours, or a guides column.\n *\n * Library footers are read by their slots (`brand`, `columns`, `column`, `bottom`); any other footer is read by its\n * headings, so a hand-written footer is held to the same content.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { ancestors, attr, descendants, normaliseText, textOf, type HtmlElement } from \"../enquiry/html.js\";\nimport { PageProblems } from \"../library/common.js\";\nimport { loadShellProjectFacts } from \"../shell/facts.js\";\n\nconst SOCIAL_HOSTS = /(?:^|\\.)(?:facebook|instagram|linkedin|youtube|tiktok|x|twitter|pinterest|houzz|hipages|yelp|threads)\\.com$|(?:^|\\.)g\\.page$|(?:^|\\.)goo\\.gl$|(?:^|\\.)youtu\\.be$/i;\nconst PROFILE_PATH = /^(?:\\/maps|\\/search|\\/local)/i;\nconst HEADING = /^h[2-6]$/;\nconst AREAS_HEADING = /\\b(?:areas?|regions?|suburbs?|locations?|councils?|where we (?:work|service|cover)|we (?:service|cover|work in)|coverage)\\b/i;\nconst SERVICES_HEADING = /\\b(?:services?|what we do|our work|trades?|solutions)\\b/i;\nconst COMPANY_HEADING = /\\b(?:company|about|information|business|our firm|our company)\\b/i;\nconst GUIDES_HEADING = /\\b(?:guides?|cost guides?|resources|articles|advice)\\b/i;\nconst CTA_LABEL = /^\\s*(?:get|request|book|schedule|claim|start)\\b.*\\b(?:quote|estimate|inspection|booking|now|online|today)\\b|^\\s*(?:free|instant)\\s+(?:quote|estimate|inspection)|^\\s*enquire\\b|^\\s*call\\s+now\\b/i;\nconst HOURS = [\n /\\b(?:opening|business|office|trading|operating)\\s+hours\\b/i,\n /\\bhours\\s*[:–-]/i,\n /\\bmon(?:day)?\\s*(?:-|–|to|through|thru)\\s*(?:fri(?:day)?|sat(?:urday)?|sun(?:day)?)\\b/i,\n /\\b\\d{1,2}(?::\\d{2})?\\s*(?:am|pm)\\s*(?:-|–|to)\\s*\\d{1,2}(?::\\d{2})?\\s*(?:am|pm)\\b/i,\n /\\b24\\s*\\/\\s*7\\b|\\bopen\\s+(?:24\\s+hours|7\\s+days|seven\\s+days)\\b/i,\n];\n\ntype ColumnKey = \"services\" | \"areas\" | \"company\" | \"guides\" | \"other\";\n\ninterface Column {\n readonly key: ColumnKey;\n readonly heading: string;\n readonly links: number;\n readonly element: HtmlElement;\n}\n\n/** The footer of a page: the library footer, else the page-level `<footer>`. */\nexport function footerOf(root: HtmlElement): HtmlElement | null {\n const all = descendants(root);\n return (\n all.find((element) => attr(element, \"data-bl\") === \"footer\") ??\n all.find((element) => element.tag === \"footer\" && !ancestors(element).some((parent) => [\"article\", \"section\", \"aside\", \"main\", \"blockquote\", \"dialog\"].includes(parent.tag))) ??\n null\n );\n}\n\nfunction classify(heading: string, declared: string | undefined): ColumnKey {\n if (declared === \"services\" || declared === \"areas\" || declared === \"company\" || declared === \"guides\") return declared;\n if (GUIDES_HEADING.test(heading)) return \"guides\";\n if (AREAS_HEADING.test(heading)) return \"areas\";\n if (SERVICES_HEADING.test(heading)) return \"services\";\n if (COMPANY_HEADING.test(heading)) return \"company\";\n return \"other\";\n}\n\nfunction anchors(element: HtmlElement): HtmlElement[] {\n return descendants(element).filter((child) => child.tag === \"a\" && (attr(child, \"href\") ?? \"\") !== \"\");\n}\n\n/** The footer's columns, in order. A column is a heading and the links under it. */\nexport function footerColumns(footer: HtmlElement, bottom: HtmlElement | undefined): Column[] {\n const slotted = descendants(footer).filter((element) => attr(element, \"data-slot\") === \"column\");\n if (slotted.length > 0) {\n return slotted.map((element) => {\n const heading = descendants(element).find((child) => HEADING.test(child.tag));\n const text = heading ? textOf(heading, true, (child) => child.tag === \"button\") : \"\";\n return { key: classify(text, attr(element, \"data-column\")), heading: text, links: anchors(element).length, element };\n });\n }\n const headings = descendants(footer).filter((element) => HEADING.test(element.tag) && !(bottom && ancestors(element).includes(bottom)));\n return headings.map((heading) => {\n let scope: HtmlElement = heading.parent ?? footer;\n while (scope !== footer && descendants(scope).filter((element) => HEADING.test(element.tag)).length > 1) scope = scope.parent ?? footer;\n const text = textOf(heading, true);\n return { key: classify(text, undefined), heading: text, links: anchors(scope).length, element: scope };\n });\n}\n\nfunction hostOf(href: string): { host: string; path: string } | null {\n try {\n const url = new URL(href);\n return { host: url.hostname.toLowerCase(), path: url.pathname };\n } catch {\n return null;\n }\n}\n\nfunction isSocialOrProfile(link: HtmlElement): boolean {\n const target = hostOf(attr(link, \"href\") ?? \"\");\n if (!target) return false;\n if (SOCIAL_HOSTS.test(target.host)) return true;\n return /(?:^|\\.)google\\.[a-z.]+$/.test(target.host) && PROFILE_PATH.test(target.path);\n}\n\nexport interface FooterFacts {\n /** Licence identifiers the business holds; an empty list when none are recorded or the facts could not be read. */\n readonly licences: readonly string[];\n}\n\nfunction compact(value: string): string {\n return value.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/** Every problem and warning with one footer, as clauses with no subject. */\nexport function footerProblems(footer: HtmlElement, facts: FooterFacts): { problems: string[]; warnings: string[] } {\n const problems: string[] = [];\n const warnings: string[] = [];\n const text = textOf(footer, true);\n const bottom = descendants(footer).find((element) => attr(element, \"data-slot\") === \"bottom\");\n const columns = footerColumns(footer, bottom);\n const brand = descendants(footer).find((element) => attr(element, \"data-slot\") === \"brand\");\n const inBottom = (element: HtmlElement) => bottom !== undefined && ancestors(element).includes(bottom);\n\n // Column 1.\n const logo = descendants(footer).some((element) => (element.tag === \"img\" || element.tag === \"svg\") && ancestors(element).some((parent) => parent.tag === \"a\") && !inBottom(element));\n if (!logo) problems.push(\"has no logo linking home in column 1\");\n const describes = brand\n ? descendants(brand).some((element) => attr(element, \"data-slot\") === \"description\" && textOf(element).split(/\\s+/).length >= 6)\n : descendants(footer).some((element) => element.tag === \"p\" && textOf(element).split(/\\s+/).length >= 12 && !inBottom(element) && !columns.some((column) => ancestors(element).includes(column.element)));\n if (!describes) problems.push(\"has no short description of the business in column 1\");\n const footerCompact = compact(text);\n const missingLicences = facts.licences.filter((licence) => !footerCompact.includes(compact(licence)));\n if (missingLicences.length > 0) problems.push(`does not show ${missingLicences.length === 1 ? \"the licence number\" : \"the licence numbers\"} ${missingLicences.join(\", \")}`);\n if (!anchors(footer).some(isSocialOrProfile)) warnings.push(\"has no social or Business Profile links in column 1\");\n if (!anchors(footer).some((link) => (attr(link, \"href\") ?? \"\").trim().toLowerCase().startsWith(\"tel:\"))) problems.push(\"has no tel: contact link in column 1\");\n\n // Columns.\n const present = columns.filter((column) => column.key !== \"other\");\n for (const key of [\"services\", \"company\"] as const) {\n const column = columns.find((candidate) => candidate.key === key);\n if (!column) problems.push(`has no ${key} column under a real heading`);\n else if (column.links === 0) problems.push(`has a ${key} column (\"${column.heading}\") with no links`);\n }\n const areas = columns.find((candidate) => candidate.key === \"areas\");\n if (!areas) warnings.push(\"has no areas column\");\n else if (areas.links === 0) problems.push(`has an areas column (\"${areas.heading}\") with no links`);\n const order = present.map((column) => column.key).filter((key): key is \"services\" | \"areas\" | \"company\" => key !== \"guides\");\n const wanted = ([\"services\", \"areas\", \"company\"] as const).filter((key) => order.includes(key));\n if (order.length === wanted.length && order.some((key, index) => key !== wanted[index])) problems.push(\"runs its columns out of order (services, then areas, then company)\");\n for (const column of columns) {\n if (column.heading === \"\") problems.push(\"has a column with no real heading\");\n }\n const guides = columns.find((column) => column.key === \"guides\");\n if (guides) problems.push(`has a guides column (\"${guides.heading}\")`);\n\n // Bottom bar.\n const legalText = bottom ? textOf(bottom, true) : text;\n if (!/(?:©|\\(c\\)|&copy;)\\s*(?:\\d{4}\\s*)?[A-Za-z]/.test(legalText)) problems.push(\"has no © line with the trading name in the bottom bar\");\n const abn = /\\bABN\\b[\\s:.]*((?:\\d[\\s]*){11})(?!\\d)/i.exec(legalText);\n if (!abn) problems.push(\"shows no ABN in the bottom bar\");\n const legalLinks = anchors(bottom ?? footer).map((link) => `${attr(link, \"href\") ?? \"\"} ${textOf(link)}`.toLowerCase());\n if (!legalLinks.some((value) => value.includes(\"privacy\"))) problems.push(\"has no privacy policy link in the bottom bar\");\n if (!legalLinks.some((value) => /terms|conditions/.test(value))) problems.push(\"has no terms link in the bottom bar\");\n\n // Never.\n const cta = descendants(footer).find(\n (element) =>\n attr(element, \"data-bl\") === \"quote-button\" ||\n attr(element, \"data-bl-cta\") !== undefined ||\n (element.tag === \"button\" && attr(element, \"data-slot\") !== \"toggle\" && attr(element, \"type\") !== \"button\") ||\n ((element.tag === \"a\" || element.tag === \"button\") && attr(element, \"data-bl\") !== \"phone-link\" && CTA_LABEL.test(normaliseText(textOf(element)))),\n );\n if (cta) problems.push(`has a call-to-action button (\"${normaliseText(textOf(cta)) || \"button\"}\")`);\n if (/\\bbased\\s+in\\b/i.test(text)) problems.push('says \"based in\"');\n const hours = HOURS.map((pattern) => pattern.exec(text)).find((match) => match !== null);\n if (hours) problems.push(`shows opening hours (\"${hours[0]}\")`);\n return { problems, warnings };\n}\n\nexport function checkN9(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n if (pages.length === 0) return [notApplicable(\"N9\", \"No page could be read, so there is no footer to check.\")];\n const { facts, reason } = loadShellProjectFacts(projectRoot);\n const footerFacts: FooterFacts = { licences: (facts?.licences ?? []).map((licence) => licence.identifier).filter(Boolean) };\n const problems = new PageProblems();\n const notes = new PageProblems();\n for (const page of pages) {\n const footer = footerOf(page.root);\n if (!footer) {\n problems.add(page.path, \"No footer\");\n continue;\n }\n const found = footerProblems(footer, footerFacts);\n for (const problem of found.problems) problems.add(page.path, `The footer ${problem}`);\n for (const warning of found.warnings) notes.add(page.path, `The footer ${warning}`);\n }\n const warnings = notes.findings(\"N9\").map((finding) => ({ ...finding, status: \"pass\" as const, severity: \"warning\" as const }));\n if (facts === null) {\n warnings.push({ checkId: \"N9\", status: \"pass\", severity: \"warning\", message: `The licence numbers in the footer were not checked: ${reason.charAt(0).toLowerCase()}${reason.slice(1)}` });\n }\n if (problems.size > 0) return [...problems.findings(\"N9\"), ...warnings];\n return [pass(\"N9\", `Checked the footer on ${plural(pages.length, \"page\")}: column 1, the services and company columns, the bottom bar and what a footer never holds are all in order.`), ...warnings];\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ import { type ParsedPage } from "../enquiry/common.js";
3
+ export declare const T6_MEDIA_PATH = "src/data/media.ts";
4
+ export declare const T6_PROVENANCES: readonly ["genuine", "stock", "illustrative-ai"];
5
+ export interface MediaManifest {
6
+ /** Media id to its `src`. */
7
+ readonly sources: ReadonlyMap<string, string>;
8
+ /** Problems with entries, one plain fragment each ("media.hero has no provenance"). */
9
+ readonly entryProblems: readonly string[];
10
+ }
11
+ /** "missing": no media.ts; "not-declared": no defineMedia call in it; null: unreadable; else the manifest. */
12
+ export declare function readMediaManifest(projectRoot: string): MediaManifest | "missing" | "not-declared" | null;
13
+ export declare function checkT6(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[];
14
+ //# sourceMappingURL=t6-media-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"t6-media-entries.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAK1F,eAAO,MAAM,aAAa,sBAAsB,CAAC;AACjD,eAAO,MAAM,cAAc,kDAAmD,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CA2BxG;AAOD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAgC9F"}
@@ -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":"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","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":"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","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,3 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ export declare function checkT9(projectRoot: string): SiteSystemFinding[];
3
+ //# sourceMappingURL=t9-location-metadata.d.ts.map
@@ -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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/conformance",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "typecheck": "tsc --noEmit -p tsconfig.json"
28
28
  },
29
29
  "dependencies": {
30
- "@brandlab-digital/site-core": "3.8.0",
30
+ "@brandlab-digital/site-core": "3.9.0",
31
31
  "typescript": "6.0.3"
32
32
  },
33
33
  "peerDependencies": {