@brandlab-digital/conformance 3.7.0 → 3.8.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/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 +12 -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/shell/banned-wording.d.ts +24 -0
- package/dist/site-system/rules/shell/banned-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/banned-wording.js +212 -0
- package/dist/site-system/rules/shell/banned-wording.js.map +1 -0
- package/dist/site-system/rules/shell/budgets.d.ts +43 -0
- package/dist/site-system/rules/shell/budgets.d.ts.map +1 -0
- package/dist/site-system/rules/shell/budgets.js +297 -0
- package/dist/site-system/rules/shell/budgets.js.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts +17 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.js +86 -0
- package/dist/site-system/rules/shell/cta-labels.js.map +1 -0
- package/dist/site-system/rules/shell/facts.d.ts +33 -0
- package/dist/site-system/rules/shell/facts.d.ts.map +1 -0
- package/dist/site-system/rules/shell/facts.js +217 -0
- package/dist/site-system/rules/shell/facts.js.map +1 -0
- package/dist/site-system/rules/shell/headings.d.ts +5 -0
- package/dist/site-system/rules/shell/headings.d.ts.map +1 -0
- package/dist/site-system/rules/shell/headings.js +23 -0
- package/dist/site-system/rules/shell/headings.js.map +1 -0
- package/dist/site-system/rules/shell/hero-location.d.ts +4 -0
- package/dist/site-system/rules/shell/hero-location.d.ts.map +1 -0
- package/dist/site-system/rules/shell/hero-location.js +45 -0
- package/dist/site-system/rules/shell/hero-location.js.map +1 -0
- package/dist/site-system/rules/shell/html.d.ts +53 -0
- package/dist/site-system/rules/shell/html.d.ts.map +1 -0
- package/dist/site-system/rules/shell/html.js +341 -0
- package/dist/site-system/rules/shell/html.js.map +1 -0
- package/dist/site-system/rules/shell/index.d.ts +18 -0
- package/dist/site-system/rules/shell/index.d.ts.map +1 -0
- package/dist/site-system/rules/shell/index.js +65 -0
- package/dist/site-system/rules/shell/index.js.map +1 -0
- package/dist/site-system/rules/shell/licence.d.ts +4 -0
- package/dist/site-system/rules/shell/licence.d.ts.map +1 -0
- package/dist/site-system/rules/shell/licence.js +60 -0
- package/dist/site-system/rules/shell/licence.js.map +1 -0
- package/dist/site-system/rules/shell/location-wording.d.ts +13 -0
- package/dist/site-system/rules/shell/location-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/location-wording.js +15 -0
- package/dist/site-system/rules/shell/location-wording.js.map +1 -0
- package/dist/site-system/rules/shell/navigation.d.ts +5 -0
- package/dist/site-system/rules/shell/navigation.d.ts.map +1 -0
- package/dist/site-system/rules/shell/navigation.js +272 -0
- package/dist/site-system/rules/shell/navigation.js.map +1 -0
- package/dist/site-system/rules/shell/overlays.d.ts +4 -0
- package/dist/site-system/rules/shell/overlays.d.ts.map +1 -0
- package/dist/site-system/rules/shell/overlays.js +93 -0
- package/dist/site-system/rules/shell/overlays.js.map +1 -0
- package/dist/site-system/rules/shell/page.d.ts +51 -0
- package/dist/site-system/rules/shell/page.d.ts.map +1 -0
- package/dist/site-system/rules/shell/page.js +256 -0
- package/dist/site-system/rules/shell/page.js.map +1 -0
- package/dist/site-system/rules/shell/phone.d.ts +13 -0
- package/dist/site-system/rules/shell/phone.d.ts.map +1 -0
- package/dist/site-system/rules/shell/phone.js +99 -0
- package/dist/site-system/rules/shell/phone.js.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts +7 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.js +81 -0
- package/dist/site-system/rules/shell/proof-numbers.js.map +1 -0
- package/dist/site-system/rules/shell/types.d.ts +80 -0
- package/dist/site-system/rules/shell/types.d.ts.map +1 -0
- package/dist/site-system/rules/shell/types.js +17 -0
- package/dist/site-system/rules/shell/types.js.map +1 -0
- package/dist/site-system/runners.d.ts +37 -0
- package/dist/site-system/runners.d.ts.map +1 -0
- package/dist/site-system/runners.js +48 -0
- package/dist/site-system/runners.js.map +1 -0
- package/dist/site-system/samples.d.ts +43 -0
- package/dist/site-system/samples.d.ts.map +1 -0
- package/dist/site-system/samples.js +99 -0
- package/dist/site-system/samples.js.map +1 -0
- package/dist/site-system/steps.d.ts +66 -0
- package/dist/site-system/steps.d.ts.map +1 -0
- package/dist/site-system/steps.js +45 -0
- package/dist/site-system/steps.js.map +1 -0
- package/dist/site-system/streamed-html.d.ts +9 -0
- package/dist/site-system/streamed-html.d.ts.map +1 -0
- package/dist/site-system/streamed-html.js +43 -0
- package/dist/site-system/streamed-html.js.map +1 -0
- package/dist/site-system-e2e.d.ts +6 -0
- package/dist/site-system-e2e.d.ts.map +1 -0
- package/dist/site-system-e2e.js +54 -0
- package/dist/site-system-e2e.js.map +1 -0
- package/package.json +22 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlays.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/overlays.ts"],"names":[],"mappings":"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","sourcesContent":["import { classTokens, findAll, selfAndAncestors, type HtmlElement } from \"./html.js\";\nimport type { ShellPage } from \"./page.js\";\nimport { failFinding, passFinding, sampledPages, type ShellFinding } from \"./types.js\";\n\n/**\n * C9: no interstitial overlay in the first screen. The server HTML holds what shows on load, so an open modal dialog,\n * an open popup or a fixed full-screen cover in it covers the first screen. A closed drawer or dialog (`hidden`,\n * `inert`, `data-state=\"closed\"`, a `<dialog>` without `open`) does not.\n */\n\nconst OVERLAY_NAME = /(?:^|[\\s_-])(?:pop-?ups?|modals?|interstitials?|lightbox|splash)(?:$|[\\s_-])|popup|modal|interstitial|lightbox/i;\nconst OPEN_CLASSES = new Set([\"open\", \"opened\", \"is-open\", \"show\", \"shown\", \"is-shown\", \"visible\", \"is-visible\", \"active\", \"is-active\", \"in\", \"displayed\"]);\nconst HIDDEN_CLASSES = new Set([\"hidden\", \"invisible\", \"sr-only\", \"closed\", \"is-closed\", \"d-none\", \"collapse\"]);\n/** Utility classes that move a panel off screen or make it unclickable until it opens. */\nconst OFF_SCREEN_CLASS = /^(?:-?translate-[xy]-full|opacity-0|pointer-events-none|scale-0)$/;\nconst FULL_SCREEN_STYLE = /position\\s*:\\s*(?:fixed|sticky)/i;\n\nfunction styleHides(style: string): boolean {\n return /display\\s*:\\s*none|visibility\\s*:\\s*hidden|opacity\\s*:\\s*0(?:\\.0+)?\\s*(?:;|$)/i.test(style);\n}\n\n/** Whether the element, or an ancestor, is hidden or inert in the server HTML. */\nfunction isHiddenOnLoad(element: HtmlElement): boolean {\n for (const candidate of selfAndAncestors(element)) {\n const { attrs } = candidate;\n if (attrs.hidden !== undefined || attrs.inert !== undefined) return true;\n if (attrs[\"aria-hidden\"] === \"true\" && candidate === element) return true;\n if (/^(?:closed|hidden)$/.test(attrs[\"data-state\"] ?? \"\")) return true;\n if (attrs[\"data-open\"] === \"false\" || (attrs[\"aria-expanded\"] === \"false\" && candidate === element)) return true;\n if (candidate.tag === \"dialog\" && attrs.open === undefined) return true;\n if (styleHides(attrs.style ?? \"\")) return true;\n if (classTokens(candidate).some((token) => HIDDEN_CLASSES.has(token) || OFF_SCREEN_CLASS.test(token))) return true;\n }\n return false;\n}\n\nfunction opensOnLoad(element: HtmlElement): boolean {\n const { attrs } = element;\n return attrs.open !== undefined || attrs[\"data-state\"] === \"open\" || attrs[\"data-open\"] === \"true\" || attrs[\"aria-hidden\"] === \"false\" ||\n attrs[\"data-visible\"] === \"true\" || classTokens(element).some((token) => OPEN_CLASSES.has(token));\n}\n\nfunction namedLikeOverlay(element: HtmlElement): boolean {\n return [attrsOf(element, \"id\"), attrsOf(element, \"class\"), attrsOf(element, \"data-bl\"), attrsOf(element, \"data-slot\"), attrsOf(element, \"aria-label\")]\n .some((value) => OVERLAY_NAME.test(value));\n}\n\nfunction attrsOf(element: HtmlElement, name: string): string {\n return element.attrs[name] ?? \"\";\n}\n\n/** Tailwind-style utilities that make a full-screen fixed cover. */\nfunction coversScreen(element: HtmlElement): boolean {\n const tokens = new Set(classTokens(element));\n const fixed = tokens.has(\"fixed\") || tokens.has(\"sticky\");\n const inset = tokens.has(\"inset-0\") || (tokens.has(\"top-0\") && tokens.has(\"left-0\") && tokens.has(\"right-0\") && tokens.has(\"bottom-0\"));\n const sized = (tokens.has(\"w-screen\") || tokens.has(\"w-full\") || tokens.has(\"w-dvw\")) && (tokens.has(\"h-screen\") || tokens.has(\"h-full\") || tokens.has(\"h-dvh\") || tokens.has(\"min-h-screen\"));\n if (fixed && (inset || sized)) return true;\n const style = element.attrs.style ?? \"\";\n if (!FULL_SCREEN_STYLE.test(style)) return false;\n const insetZero = /inset\\s*:\\s*0(?:px)?\\b/i.test(style) || (/top\\s*:\\s*0/i.test(style) && /left\\s*:\\s*0/i.test(style) && /right\\s*:\\s*0/i.test(style) && /bottom\\s*:\\s*0/i.test(style));\n const full = /width\\s*:\\s*100(?:vw|dvw|%)/i.test(style) && /(?:min-)?height\\s*:\\s*100(?:vh|dvh|%)/i.test(style);\n return insetZero || full;\n}\n\nfunction describe(element: HtmlElement): string {\n const name = element.attrs[\"aria-label\"] || element.attrs.id || element.attrs[\"data-bl\"] || classTokens(element)[0] || element.tag;\n return `${element.tag} \"${name}\"`;\n}\n\nexport function checkNoInterstitial(pages: readonly ShellPage[]): ShellFinding[] {\n const findings: ShellFinding[] = [];\n for (const page of pages) {\n const reasons = new Map<HtmlElement, string>();\n for (const element of findAll(page.body, () => true)) {\n if (isHiddenOnLoad(element)) continue;\n if (element.attrs[\"aria-modal\"] === \"true\") reasons.set(element, \"a modal dialog that is open on load\");\n else if (element.tag === \"dialog\" && element.attrs.open !== undefined) reasons.set(element, \"a dialog that is open on load\");\n else if (namedLikeOverlay(element) && opensOnLoad(element) && !(element.tag === \"button\" || element.tag === \"a\")) reasons.set(element, \"a popup that is open on load\");\n else if (coversScreen(element)) reasons.set(element, \"a fixed full-screen cover\");\n }\n // Report the outermost cover only: its children are part of it.\n const outer = [...reasons.entries()].filter(([element]) => ![...selfAndAncestors(element)].slice(1).some((ancestor) => reasons.has(ancestor)));\n for (const [element, reason] of outer.slice(0, 3)) {\n findings.push(failFinding(\"C9\", page.path, `The page ${page.path} loads with ${reason} (${describe(element)}), which covers the first screen.`));\n }\n }\n return findings.length > 0 ? findings : [passFinding(\"C9\", `No interstitial overlay is open on load on ${sampledPages(pages.length)}.`)];\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type HtmlDocument, type HtmlElement } from "./html.js";
|
|
2
|
+
/** The kind of page, which decides where some wording is allowed (the footer and contact page may say "based in"). */
|
|
3
|
+
export type ShellPageFamily = "home" | "contact" | "location" | "legal" | "utility" | "other";
|
|
4
|
+
export interface TextSegment {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
/** The element that holds this run of text. */
|
|
8
|
+
element: HtmlElement;
|
|
9
|
+
}
|
|
10
|
+
/** A run of visible text between block boundaries, with the element behind each stretch of it. */
|
|
11
|
+
export interface TextUnit {
|
|
12
|
+
text: string;
|
|
13
|
+
segments: readonly TextSegment[];
|
|
14
|
+
}
|
|
15
|
+
export interface ShellPage {
|
|
16
|
+
path: string;
|
|
17
|
+
html: string;
|
|
18
|
+
doc: HtmlDocument;
|
|
19
|
+
body: HtmlElement;
|
|
20
|
+
family: ShellPageFamily;
|
|
21
|
+
noindex: boolean;
|
|
22
|
+
/** Visible text, one unit per block. */
|
|
23
|
+
units: readonly TextUnit[];
|
|
24
|
+
/** Text that renders from attributes: alt, aria-label, title, placeholder and button values. */
|
|
25
|
+
attributeCopy: readonly TextUnit[];
|
|
26
|
+
/** The title and the description and social meta tags. */
|
|
27
|
+
metadata: readonly string[];
|
|
28
|
+
/** Copy-like strings inside JSON-LD (schema). */
|
|
29
|
+
schemaCopy: readonly string[];
|
|
30
|
+
/** The `telephone` values of the page's own LocalBusiness JSON-LD, as written. */
|
|
31
|
+
schemaTelephones: readonly string[];
|
|
32
|
+
headers: readonly HtmlElement[];
|
|
33
|
+
footers: readonly HtmlElement[];
|
|
34
|
+
h1s: readonly HtmlElement[];
|
|
35
|
+
hero: HtmlElement | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function normalisePagePath(path: string): string;
|
|
38
|
+
/** Parse one sampled page and gather what the rules read from it. */
|
|
39
|
+
export declare function prepareShellPage(page: {
|
|
40
|
+
path: string;
|
|
41
|
+
html: string;
|
|
42
|
+
}): ShellPage;
|
|
43
|
+
/** The element behind the character at `index` in a unit. */
|
|
44
|
+
export declare function elementAt(unit: TextUnit, index: number): HtmlElement;
|
|
45
|
+
/** Whether the element is inside any of the regions. */
|
|
46
|
+
export declare function inAny(element: HtmlElement, regions: readonly HtmlElement[]): boolean;
|
|
47
|
+
/** A short, single-line excerpt of a match for a message. */
|
|
48
|
+
export declare function excerpt(text: string, index: number, length: number, margin?: number): string;
|
|
49
|
+
/** The element and every ancestor satisfy `predicate` somewhere. */
|
|
50
|
+
export declare function anyAncestor(element: HtmlElement, predicate: (candidate: HtmlElement) => boolean): boolean;
|
|
51
|
+
//# sourceMappingURL=page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAEqB,KAAK,YAAY,EAAE,KAAK,WAAW,EAC9D,MAAM,WAAW,CAAC;AAEnB,sHAAsH;AACtH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAE9F,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,YAAY,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,gGAAgG;IAChG,aAAa,EAAE,SAAS,QAAQ,EAAE,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,kFAAkF;IAClF,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,GAAG,EAAE,SAAS,WAAW,EAAE,CAAC;IAC5B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1B;AAWD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAItD;AAyKD,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAuBhF;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAGpE;AAED,wDAAwD;AACxD,wBAAgB,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAEpF;AAED,6DAA6D;AAC7D,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,CAMxF;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,GAAG,OAAO,CAGzG"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { BLOCK_TAGS, NON_TEXT_TAGS, closest, collapseWhitespace, descendants, findAll, findFirst, isWithin, parseHtml, selfAndAncestors, textOf, } from "./html.js";
|
|
2
|
+
const LOCATION_SEGMENTS = new Set([
|
|
3
|
+
"areas", "area", "locations", "location", "service-areas", "service-area", "suburbs", "suburb", "regions", "region",
|
|
4
|
+
]);
|
|
5
|
+
const LEGAL_SEGMENT = /^(?:privacy|privacy-policy|terms|terms-and-conditions|terms-of-use|cookies?|cookie-policy|legal|disclaimer|accessibility|sitemap)$/;
|
|
6
|
+
const UTILITY_SEGMENT = /^(?:thank-you|thankyou|thanks|404|not-found|offline|error)$/;
|
|
7
|
+
const FAMILY_VALUES = new Set(["home", "contact", "location", "legal", "utility", "other"]);
|
|
8
|
+
export function normalisePagePath(path) {
|
|
9
|
+
const clean = (path.split(/[?#]/)[0] ?? "").trim() || "/";
|
|
10
|
+
const lower = clean.startsWith("/") ? clean : `/${clean}`;
|
|
11
|
+
return lower.length > 1 ? lower.replace(/\/+$/, "") || "/" : lower;
|
|
12
|
+
}
|
|
13
|
+
function pageFamily(path, doc) {
|
|
14
|
+
const declared = [findFirst(doc.root, (element) => element.tag === "html"), findFirst(doc.root, (element) => element.tag === "body"), findFirst(doc.root, (element) => element.tag === "main")]
|
|
15
|
+
.map((element) => element?.attrs["data-bl-page"] ?? element?.attrs["data-bl-family"] ?? element?.attrs["data-page-family"])
|
|
16
|
+
.find((value) => value !== undefined);
|
|
17
|
+
if (declared && FAMILY_VALUES.has(declared))
|
|
18
|
+
return declared;
|
|
19
|
+
const segments = normalisePagePath(path).toLowerCase().split("/").filter(Boolean);
|
|
20
|
+
if (segments.length === 0)
|
|
21
|
+
return "home";
|
|
22
|
+
if (segments.some((segment) => /^(?:contact|contact-us)$/.test(segment)))
|
|
23
|
+
return "contact";
|
|
24
|
+
if (segments.some((segment) => LEGAL_SEGMENT.test(segment)))
|
|
25
|
+
return "legal";
|
|
26
|
+
if (segments.some((segment) => UTILITY_SEGMENT.test(segment)))
|
|
27
|
+
return "utility";
|
|
28
|
+
if (LOCATION_SEGMENTS.has(segments[0]))
|
|
29
|
+
return "location";
|
|
30
|
+
return "other";
|
|
31
|
+
}
|
|
32
|
+
const SECTIONING = new Set(["main", "article", "section", "aside"]);
|
|
33
|
+
function isPageHeader(element) {
|
|
34
|
+
if (element.attrs["data-bl"] === "header" || element.attrs.role === "banner")
|
|
35
|
+
return true;
|
|
36
|
+
if (element.tag !== "header")
|
|
37
|
+
return false;
|
|
38
|
+
return !closest(element.parent ?? element, (ancestor) => SECTIONING.has(ancestor.tag) || ancestor.attrs.role === "main");
|
|
39
|
+
}
|
|
40
|
+
function isPageFooter(element) {
|
|
41
|
+
if (element.attrs["data-bl"] === "footer" || element.attrs.role === "contentinfo")
|
|
42
|
+
return true;
|
|
43
|
+
if (element.tag !== "footer")
|
|
44
|
+
return false;
|
|
45
|
+
return !closest(element.parent ?? element, (ancestor) => SECTIONING.has(ancestor.tag) || ancestor.attrs.role === "main");
|
|
46
|
+
}
|
|
47
|
+
function findHero(body, h1s) {
|
|
48
|
+
const declared = findFirst(body, (element) => element.attrs["data-bl-section"] === "hero");
|
|
49
|
+
if (declared)
|
|
50
|
+
return declared;
|
|
51
|
+
const h1 = h1s[0];
|
|
52
|
+
if (!h1)
|
|
53
|
+
return null;
|
|
54
|
+
const section = closest(h1, (element) => element.tag === "section" || element.attrs["data-bl-section"] !== undefined);
|
|
55
|
+
if (section)
|
|
56
|
+
return section;
|
|
57
|
+
const main = closest(h1, (element) => element.tag === "main");
|
|
58
|
+
if (main) {
|
|
59
|
+
let child = h1;
|
|
60
|
+
while (child.parent && child.parent !== main)
|
|
61
|
+
child = child.parent;
|
|
62
|
+
if (child.parent === main)
|
|
63
|
+
return child;
|
|
64
|
+
}
|
|
65
|
+
return h1.parent ?? h1;
|
|
66
|
+
}
|
|
67
|
+
function collectUnits(root) {
|
|
68
|
+
const units = [];
|
|
69
|
+
let text = "";
|
|
70
|
+
let segments = [];
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (text.trim() !== "")
|
|
73
|
+
units.push({ text, segments });
|
|
74
|
+
text = "";
|
|
75
|
+
segments = [];
|
|
76
|
+
};
|
|
77
|
+
const visit = (element, depth) => {
|
|
78
|
+
if (NON_TEXT_TAGS.has(element.tag) || depth > 1500)
|
|
79
|
+
return;
|
|
80
|
+
const block = BLOCK_TAGS.has(element.tag);
|
|
81
|
+
if (block)
|
|
82
|
+
flush();
|
|
83
|
+
for (const child of element.children) {
|
|
84
|
+
if (child.kind === "text") {
|
|
85
|
+
segments.push({ start: text.length, end: text.length + child.text.length, element });
|
|
86
|
+
text += child.text;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
visit(child, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (block)
|
|
93
|
+
flush();
|
|
94
|
+
};
|
|
95
|
+
visit(root, 0);
|
|
96
|
+
flush();
|
|
97
|
+
return units;
|
|
98
|
+
}
|
|
99
|
+
const COPY_ATTRIBUTES = ["alt", "title", "aria-label", "aria-description", "placeholder"];
|
|
100
|
+
function collectAttributeCopy(root) {
|
|
101
|
+
const units = [];
|
|
102
|
+
for (const element of descendants(root)) {
|
|
103
|
+
if (closest(element, (candidate) => NON_TEXT_TAGS.has(candidate.tag)))
|
|
104
|
+
continue;
|
|
105
|
+
const values = COPY_ATTRIBUTES.map((name) => element.attrs[name]);
|
|
106
|
+
if (element.tag === "input" && /^(?:submit|button)$/i.test(element.attrs.type ?? ""))
|
|
107
|
+
values.push(element.attrs.value);
|
|
108
|
+
for (const value of values) {
|
|
109
|
+
const text = value === undefined ? "" : collapseWhitespace(value);
|
|
110
|
+
if (text !== "")
|
|
111
|
+
units.push({ text, segments: [{ start: 0, end: text.length, element }] });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return units;
|
|
115
|
+
}
|
|
116
|
+
function collectMetadata(doc) {
|
|
117
|
+
const values = [];
|
|
118
|
+
const title = findFirst(doc.root, (element) => element.tag === "title");
|
|
119
|
+
if (title)
|
|
120
|
+
values.push(textOf(title, new Set()));
|
|
121
|
+
for (const meta of findAll(doc.root, (element) => element.tag === "meta")) {
|
|
122
|
+
const key = (meta.attrs.name ?? meta.attrs.property ?? "").toLowerCase();
|
|
123
|
+
if (key === "description" || /^(?:og|twitter):(?:title|description|site_name)$/.test(key))
|
|
124
|
+
values.push(collapseWhitespace(meta.attrs.content ?? ""));
|
|
125
|
+
}
|
|
126
|
+
return values.filter((value) => value !== "");
|
|
127
|
+
}
|
|
128
|
+
const NON_COPY_JSON_KEYS = /^(?:@context|@id|@type|url|image|logo|sameAs|telephone|email|priceRange|openingHours|postalCode|addressCountry|identifier|taxID|vatID|sku|mpn|ratingValue|reviewCount|ratingCount)$/;
|
|
129
|
+
function collectSchemaCopy(doc) {
|
|
130
|
+
const values = [];
|
|
131
|
+
const visit = (value, key, depth) => {
|
|
132
|
+
if (depth > 12)
|
|
133
|
+
return;
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (key !== null && NON_COPY_JSON_KEYS.test(key))
|
|
136
|
+
return;
|
|
137
|
+
const text = collapseWhitespace(value);
|
|
138
|
+
if (/\s/.test(text))
|
|
139
|
+
values.push(text);
|
|
140
|
+
}
|
|
141
|
+
else if (Array.isArray(value)) {
|
|
142
|
+
for (const item of value)
|
|
143
|
+
visit(item, key, depth + 1);
|
|
144
|
+
}
|
|
145
|
+
else if (value && typeof value === "object") {
|
|
146
|
+
for (const [childKey, child] of Object.entries(value))
|
|
147
|
+
visit(child, childKey, depth + 1);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
for (const script of findAll(doc.root, (element) => element.tag === "script" && /ld\+json/i.test(element.attrs.type ?? ""))) {
|
|
151
|
+
const source = script.children.map((child) => (child.kind === "text" ? child.text : "")).join("");
|
|
152
|
+
try {
|
|
153
|
+
visit(JSON.parse(source), null, 0);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Malformed schema is another check's concern; it holds no copy to read here.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return values;
|
|
160
|
+
}
|
|
161
|
+
/** The schema.org types the shared LocalBusiness builder emits (site-core `BrandlabLocalBusinessSchemaType`). */
|
|
162
|
+
const LOCAL_BUSINESS_TYPES = new Set([
|
|
163
|
+
"LocalBusiness", "ProfessionalService", "HomeAndConstructionBusiness", "Electrician", "GeneralContractor", "Plumber",
|
|
164
|
+
"RoofingContractor",
|
|
165
|
+
]);
|
|
166
|
+
function collectSchemaTelephones(doc) {
|
|
167
|
+
const telephones = [];
|
|
168
|
+
const visit = (value, depth) => {
|
|
169
|
+
if (depth > 12 || !value || typeof value !== "object")
|
|
170
|
+
return;
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
for (const item of value)
|
|
173
|
+
visit(item, depth + 1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const node = value;
|
|
177
|
+
const type = node["@type"];
|
|
178
|
+
const types = Array.isArray(type) ? type : [type];
|
|
179
|
+
if (types.some((name) => typeof name === "string" && LOCAL_BUSINESS_TYPES.has(name))) {
|
|
180
|
+
const telephone = node.telephone;
|
|
181
|
+
for (const entry of Array.isArray(telephone) ? telephone : [telephone]) {
|
|
182
|
+
if (typeof entry === "string" && entry.trim() !== "")
|
|
183
|
+
telephones.push(entry.trim());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const child of Object.values(node))
|
|
187
|
+
visit(child, depth + 1);
|
|
188
|
+
};
|
|
189
|
+
for (const script of findAll(doc.root, (element) => element.tag === "script" && /ld\+json/i.test(element.attrs.type ?? ""))) {
|
|
190
|
+
const source = script.children.map((child) => (child.kind === "text" ? child.text : "")).join("");
|
|
191
|
+
try {
|
|
192
|
+
visit(JSON.parse(source), 0);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Malformed schema is another check's concern.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return telephones;
|
|
199
|
+
}
|
|
200
|
+
function hasNoindex(doc) {
|
|
201
|
+
return findAll(doc.root, (element) => element.tag === "meta" && (element.attrs.name ?? "").toLowerCase() === "robots")
|
|
202
|
+
.some((meta) => /\bnoindex\b/i.test(meta.attrs.content ?? ""));
|
|
203
|
+
}
|
|
204
|
+
/** Parse one sampled page and gather what the rules read from it. */
|
|
205
|
+
export function prepareShellPage(page) {
|
|
206
|
+
const doc = parseHtml(page.html);
|
|
207
|
+
const body = findFirst(doc.root, (element) => element.tag === "body") ?? doc.root;
|
|
208
|
+
const h1s = findAll(body, (element) => (element.tag === "h1" || (element.attrs.role === "heading" && element.attrs["aria-level"] === "1")) &&
|
|
209
|
+
!closest(element, (ancestor) => ancestor.tag === "template" || ancestor.tag === "noscript" || ancestor.tag === "svg"));
|
|
210
|
+
return {
|
|
211
|
+
path: normalisePagePath(page.path),
|
|
212
|
+
html: page.html,
|
|
213
|
+
doc,
|
|
214
|
+
body,
|
|
215
|
+
family: pageFamily(page.path, doc),
|
|
216
|
+
noindex: hasNoindex(doc),
|
|
217
|
+
units: collectUnits(body),
|
|
218
|
+
attributeCopy: collectAttributeCopy(body),
|
|
219
|
+
metadata: collectMetadata(doc),
|
|
220
|
+
schemaCopy: collectSchemaCopy(doc),
|
|
221
|
+
schemaTelephones: collectSchemaTelephones(doc),
|
|
222
|
+
headers: findAll(body, isPageHeader),
|
|
223
|
+
footers: findAll(body, isPageFooter),
|
|
224
|
+
h1s,
|
|
225
|
+
hero: findHero(body, h1s),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** The element behind the character at `index` in a unit. */
|
|
229
|
+
export function elementAt(unit, index) {
|
|
230
|
+
for (const segment of unit.segments)
|
|
231
|
+
if (index >= segment.start && index < segment.end)
|
|
232
|
+
return segment.element;
|
|
233
|
+
return unit.segments[unit.segments.length - 1].element;
|
|
234
|
+
}
|
|
235
|
+
/** Whether the element is inside any of the regions. */
|
|
236
|
+
export function inAny(element, regions) {
|
|
237
|
+
return regions.some((region) => isWithin(element, region));
|
|
238
|
+
}
|
|
239
|
+
/** A short, single-line excerpt of a match for a message. */
|
|
240
|
+
export function excerpt(text, index, length, margin = 24) {
|
|
241
|
+
let start = Math.max(0, index - margin);
|
|
242
|
+
let end = Math.min(text.length, index + length + margin);
|
|
243
|
+
while (start > 0 && start < index && !/\s/.test(text[start - 1]))
|
|
244
|
+
start += 1;
|
|
245
|
+
while (end < text.length && end > index + length && !/\s/.test(text[end]))
|
|
246
|
+
end -= 1;
|
|
247
|
+
return `${start > 0 ? "…" : ""}${collapseWhitespace(text.slice(start, end))}${end < text.length ? "…" : ""}`;
|
|
248
|
+
}
|
|
249
|
+
/** The element and every ancestor satisfy `predicate` somewhere. */
|
|
250
|
+
export function anyAncestor(element, predicate) {
|
|
251
|
+
for (const candidate of selfAndAncestors(element))
|
|
252
|
+
if (predicate(candidate))
|
|
253
|
+
return true;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/page.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BLOCK_TAGS, NON_TEXT_TAGS, closest, collapseWhitespace, descendants, findAll, findFirst, isWithin, parseHtml,\n selfAndAncestors, textOf, type HtmlDocument, type HtmlElement,\n} from \"./html.js\";\n\n/** The kind of page, which decides where some wording is allowed (the footer and contact page may say \"based in\"). */\nexport type ShellPageFamily = \"home\" | \"contact\" | \"location\" | \"legal\" | \"utility\" | \"other\";\n\nexport interface TextSegment {\n start: number;\n end: number;\n /** The element that holds this run of text. */\n element: HtmlElement;\n}\n\n/** A run of visible text between block boundaries, with the element behind each stretch of it. */\nexport interface TextUnit {\n text: string;\n segments: readonly TextSegment[];\n}\n\nexport interface ShellPage {\n path: string;\n html: string;\n doc: HtmlDocument;\n body: HtmlElement;\n family: ShellPageFamily;\n noindex: boolean;\n /** Visible text, one unit per block. */\n units: readonly TextUnit[];\n /** Text that renders from attributes: alt, aria-label, title, placeholder and button values. */\n attributeCopy: readonly TextUnit[];\n /** The title and the description and social meta tags. */\n metadata: readonly string[];\n /** Copy-like strings inside JSON-LD (schema). */\n schemaCopy: readonly string[];\n /** The `telephone` values of the page's own LocalBusiness JSON-LD, as written. */\n schemaTelephones: readonly string[];\n headers: readonly HtmlElement[];\n footers: readonly HtmlElement[];\n h1s: readonly HtmlElement[];\n hero: HtmlElement | null;\n}\n\n\n\nconst LOCATION_SEGMENTS = new Set([\n \"areas\", \"area\", \"locations\", \"location\", \"service-areas\", \"service-area\", \"suburbs\", \"suburb\", \"regions\", \"region\",\n]);\nconst LEGAL_SEGMENT = /^(?:privacy|privacy-policy|terms|terms-and-conditions|terms-of-use|cookies?|cookie-policy|legal|disclaimer|accessibility|sitemap)$/;\nconst UTILITY_SEGMENT = /^(?:thank-you|thankyou|thanks|404|not-found|offline|error)$/;\nconst FAMILY_VALUES = new Set<ShellPageFamily>([\"home\", \"contact\", \"location\", \"legal\", \"utility\", \"other\"]);\n\nexport function normalisePagePath(path: string): string {\n const clean = (path.split(/[?#]/)[0] ?? \"\").trim() || \"/\";\n const lower = clean.startsWith(\"/\") ? clean : `/${clean}`;\n return lower.length > 1 ? lower.replace(/\\/+$/, \"\") || \"/\" : lower;\n}\n\nfunction pageFamily(path: string, doc: HtmlDocument): ShellPageFamily {\n const declared = [findFirst(doc.root, (element) => element.tag === \"html\"), findFirst(doc.root, (element) => element.tag === \"body\"), findFirst(doc.root, (element) => element.tag === \"main\")]\n .map((element) => element?.attrs[\"data-bl-page\"] ?? element?.attrs[\"data-bl-family\"] ?? element?.attrs[\"data-page-family\"])\n .find((value) => value !== undefined);\n if (declared && FAMILY_VALUES.has(declared as ShellPageFamily)) return declared as ShellPageFamily;\n const segments = normalisePagePath(path).toLowerCase().split(\"/\").filter(Boolean);\n if (segments.length === 0) return \"home\";\n if (segments.some((segment) => /^(?:contact|contact-us)$/.test(segment))) return \"contact\";\n if (segments.some((segment) => LEGAL_SEGMENT.test(segment))) return \"legal\";\n if (segments.some((segment) => UTILITY_SEGMENT.test(segment))) return \"utility\";\n if (LOCATION_SEGMENTS.has(segments[0]!)) return \"location\";\n return \"other\";\n}\n\nconst SECTIONING = new Set([\"main\", \"article\", \"section\", \"aside\"]);\n\nfunction isPageHeader(element: HtmlElement): boolean {\n if (element.attrs[\"data-bl\"] === \"header\" || element.attrs.role === \"banner\") return true;\n if (element.tag !== \"header\") return false;\n return !closest(element.parent ?? element, (ancestor) => SECTIONING.has(ancestor.tag) || ancestor.attrs.role === \"main\");\n}\n\nfunction isPageFooter(element: HtmlElement): boolean {\n if (element.attrs[\"data-bl\"] === \"footer\" || element.attrs.role === \"contentinfo\") return true;\n if (element.tag !== \"footer\") return false;\n return !closest(element.parent ?? element, (ancestor) => SECTIONING.has(ancestor.tag) || ancestor.attrs.role === \"main\");\n}\n\nfunction findHero(body: HtmlElement, h1s: readonly HtmlElement[]): HtmlElement | null {\n const declared = findFirst(body, (element) => element.attrs[\"data-bl-section\"] === \"hero\");\n if (declared) return declared;\n const h1 = h1s[0];\n if (!h1) return null;\n const section = closest(h1, (element) => element.tag === \"section\" || element.attrs[\"data-bl-section\"] !== undefined);\n if (section) return section;\n const main = closest(h1, (element) => element.tag === \"main\");\n if (main) {\n let child: HtmlElement = h1;\n while (child.parent && child.parent !== main) child = child.parent;\n if (child.parent === main) return child;\n }\n return h1.parent ?? h1;\n}\n\nfunction collectUnits(root: HtmlElement): TextUnit[] {\n const units: TextUnit[] = [];\n let text = \"\";\n let segments: TextSegment[] = [];\n const flush = () => {\n if (text.trim() !== \"\") units.push({ text, segments });\n text = \"\";\n segments = [];\n };\n const visit = (element: HtmlElement, depth: number) => {\n if (NON_TEXT_TAGS.has(element.tag) || depth > 1500) return;\n const block = BLOCK_TAGS.has(element.tag);\n if (block) flush();\n for (const child of element.children) {\n if (child.kind === \"text\") {\n segments.push({ start: text.length, end: text.length + child.text.length, element });\n text += child.text;\n } else {\n visit(child, depth + 1);\n }\n }\n if (block) flush();\n };\n visit(root, 0);\n flush();\n return units;\n}\n\nconst COPY_ATTRIBUTES = [\"alt\", \"title\", \"aria-label\", \"aria-description\", \"placeholder\"] as const;\n\nfunction collectAttributeCopy(root: HtmlElement): TextUnit[] {\n const units: TextUnit[] = [];\n for (const element of descendants(root)) {\n if (closest(element, (candidate) => NON_TEXT_TAGS.has(candidate.tag))) continue;\n const values = COPY_ATTRIBUTES.map((name) => element.attrs[name]);\n if (element.tag === \"input\" && /^(?:submit|button)$/i.test(element.attrs.type ?? \"\")) values.push(element.attrs.value);\n for (const value of values) {\n const text = value === undefined ? \"\" : collapseWhitespace(value);\n if (text !== \"\") units.push({ text, segments: [{ start: 0, end: text.length, element }] });\n }\n }\n return units;\n}\n\nfunction collectMetadata(doc: HtmlDocument): string[] {\n const values: string[] = [];\n const title = findFirst(doc.root, (element) => element.tag === \"title\");\n if (title) values.push(textOf(title, new Set()));\n for (const meta of findAll(doc.root, (element) => element.tag === \"meta\")) {\n const key = (meta.attrs.name ?? meta.attrs.property ?? \"\").toLowerCase();\n if (key === \"description\" || /^(?:og|twitter):(?:title|description|site_name)$/.test(key)) values.push(collapseWhitespace(meta.attrs.content ?? \"\"));\n }\n return values.filter((value) => value !== \"\");\n}\n\nconst NON_COPY_JSON_KEYS = /^(?:@context|@id|@type|url|image|logo|sameAs|telephone|email|priceRange|openingHours|postalCode|addressCountry|identifier|taxID|vatID|sku|mpn|ratingValue|reviewCount|ratingCount)$/;\n\nfunction collectSchemaCopy(doc: HtmlDocument): string[] {\n const values: string[] = [];\n const visit = (value: unknown, key: string | null, depth: number) => {\n if (depth > 12) return;\n if (typeof value === \"string\") {\n if (key !== null && NON_COPY_JSON_KEYS.test(key)) return;\n const text = collapseWhitespace(value);\n if (/\\s/.test(text)) values.push(text);\n } else if (Array.isArray(value)) {\n for (const item of value) visit(item, key, depth + 1);\n } else if (value && typeof value === \"object\") {\n for (const [childKey, child] of Object.entries(value)) visit(child, childKey, depth + 1);\n }\n };\n for (const script of findAll(doc.root, (element) => element.tag === \"script\" && /ld\\+json/i.test(element.attrs.type ?? \"\"))) {\n const source = script.children.map((child) => (child.kind === \"text\" ? child.text : \"\")).join(\"\");\n try {\n visit(JSON.parse(source), null, 0);\n } catch {\n // Malformed schema is another check's concern; it holds no copy to read here.\n }\n }\n return values;\n}\n\n/** The schema.org types the shared LocalBusiness builder emits (site-core `BrandlabLocalBusinessSchemaType`). */\nconst LOCAL_BUSINESS_TYPES = new Set([\n \"LocalBusiness\", \"ProfessionalService\", \"HomeAndConstructionBusiness\", \"Electrician\", \"GeneralContractor\", \"Plumber\",\n \"RoofingContractor\",\n]);\n\nfunction collectSchemaTelephones(doc: HtmlDocument): string[] {\n const telephones: string[] = [];\n const visit = (value: unknown, depth: number) => {\n if (depth > 12 || !value || typeof value !== \"object\") return;\n if (Array.isArray(value)) {\n for (const item of value) visit(item, depth + 1);\n return;\n }\n const node = value as Record<string, unknown>;\n const type = node[\"@type\"];\n const types = Array.isArray(type) ? type : [type];\n if (types.some((name) => typeof name === \"string\" && LOCAL_BUSINESS_TYPES.has(name))) {\n const telephone = node.telephone;\n for (const entry of Array.isArray(telephone) ? telephone : [telephone]) {\n if (typeof entry === \"string\" && entry.trim() !== \"\") telephones.push(entry.trim());\n }\n }\n for (const child of Object.values(node)) visit(child, depth + 1);\n };\n for (const script of findAll(doc.root, (element) => element.tag === \"script\" && /ld\\+json/i.test(element.attrs.type ?? \"\"))) {\n const source = script.children.map((child) => (child.kind === \"text\" ? child.text : \"\")).join(\"\");\n try {\n visit(JSON.parse(source), 0);\n } catch {\n // Malformed schema is another check's concern.\n }\n }\n return telephones;\n}\n\nfunction hasNoindex(doc: HtmlDocument): boolean {\n return findAll(doc.root, (element) => element.tag === \"meta\" && (element.attrs.name ?? \"\").toLowerCase() === \"robots\")\n .some((meta) => /\\bnoindex\\b/i.test(meta.attrs.content ?? \"\"));\n}\n\n/** Parse one sampled page and gather what the rules read from it. */\nexport function prepareShellPage(page: { path: string; html: string }): ShellPage {\n const doc = parseHtml(page.html);\n const body = findFirst(doc.root, (element) => element.tag === \"body\") ?? doc.root;\n const h1s = findAll(body, (element) =>\n (element.tag === \"h1\" || (element.attrs.role === \"heading\" && element.attrs[\"aria-level\"] === \"1\")) &&\n !closest(element, (ancestor) => ancestor.tag === \"template\" || ancestor.tag === \"noscript\" || ancestor.tag === \"svg\"));\n return {\n path: normalisePagePath(page.path),\n html: page.html,\n doc,\n body,\n family: pageFamily(page.path, doc),\n noindex: hasNoindex(doc),\n units: collectUnits(body),\n attributeCopy: collectAttributeCopy(body),\n metadata: collectMetadata(doc),\n schemaCopy: collectSchemaCopy(doc),\n schemaTelephones: collectSchemaTelephones(doc),\n headers: findAll(body, isPageHeader),\n footers: findAll(body, isPageFooter),\n h1s,\n hero: findHero(body, h1s),\n };\n}\n\n/** The element behind the character at `index` in a unit. */\nexport function elementAt(unit: TextUnit, index: number): HtmlElement {\n for (const segment of unit.segments) if (index >= segment.start && index < segment.end) return segment.element;\n return unit.segments[unit.segments.length - 1]!.element;\n}\n\n/** Whether the element is inside any of the regions. */\nexport function inAny(element: HtmlElement, regions: readonly HtmlElement[]): boolean {\n return regions.some((region) => isWithin(element, region));\n}\n\n/** A short, single-line excerpt of a match for a message. */\nexport function excerpt(text: string, index: number, length: number, margin = 24): string {\n let start = Math.max(0, index - margin);\n let end = Math.min(text.length, index + length + margin);\n while (start > 0 && start < index && !/\\s/.test(text[start - 1]!)) start += 1;\n while (end < text.length && end > index + length && !/\\s/.test(text[end]!)) end -= 1;\n return `${start > 0 ? \"…\" : \"\"}${collapseWhitespace(text.slice(start, end))}${end < text.length ? \"…\" : \"\"}`;\n}\n\n/** The element and every ancestor satisfy `predicate` somewhere. */\nexport function anyAncestor(element: HtmlElement, predicate: (candidate: HtmlElement) => boolean): boolean {\n for (const candidate of selfAndAncestors(element)) if (predicate(candidate)) return true;\n return false;\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ShellPage } from "./page.js";
|
|
2
|
+
import { type ShellFinding, type ShellProjectFacts } from "./types.js";
|
|
3
|
+
/** Why a dialled number is not valid, or null when it is. */
|
|
4
|
+
export declare function telProblem(number: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* The number a business writes ("+61255500100", "02 5550 0100", "1300 555 010", "tel:+61255500100") as the dialled
|
|
7
|
+
* number a `tel:` link should carry. Text that is not an Australian number comes back as typed, without `tel:`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function dialledFromBusinessNumber(written: string): string;
|
|
10
|
+
/** The same normalisation through the shared `telHrefFor` for an E.164 value. */
|
|
11
|
+
export declare function dialledFromE164(e164: string): string;
|
|
12
|
+
export declare function checkTelLinks(pages: readonly ShellPage[], facts: ShellProjectFacts | null, _factsReason: string, recordedPhones?: readonly string[]): ShellFinding[];
|
|
13
|
+
//# sourceMappingURL=phone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phone.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/phone.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAkD,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA0BvH,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,iBAAiB,GAAG,IAAI,EAC/B,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,SAAS,MAAM,EAAO,GACrC,YAAY,EAAE,CAuChB"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { findAll } from "./html.js";
|
|
2
|
+
import { telHrefFor } from "./facts.js";
|
|
3
|
+
import { failFinding, notApplicableFinding, passFinding } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* C10: every `tel:` href is Australian E.164 (`+61…`), or the national digits for a 13, 1300 or 1800 number, which
|
|
6
|
+
* cannot be dialled in international form, and equals the business phone. The business phone is the `telephone` in the
|
|
7
|
+
* page's own LocalBusiness JSON-LD; a page with none falls back to the evidence file. A project can record further
|
|
8
|
+
* numbers (`siteSystem.phones`), such as a second line. With no phone to compare against, the format is still checked
|
|
9
|
+
* and the comparison is reported as not applicable.
|
|
10
|
+
*/
|
|
11
|
+
const GEOGRAPHIC_E164 = /^\+61[23478]\d{8}$/;
|
|
12
|
+
const NON_GEOGRAPHIC_NATIONAL = /^(?:1[38]00\d{6}|13(?!00)\d{4})$/;
|
|
13
|
+
const NON_GEOGRAPHIC_INTERNATIONAL = /^\+61(?:1[38]00\d{6}|13(?!00)\d{4})$/;
|
|
14
|
+
/** The dialled number in a `tel:` href, without parameters such as `;ext=`. */
|
|
15
|
+
function dialledNumber(href) {
|
|
16
|
+
const raw = href.trim().replace(/^tel:/i, "");
|
|
17
|
+
let decoded = raw;
|
|
18
|
+
try {
|
|
19
|
+
decoded = decodeURIComponent(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
decoded = raw;
|
|
23
|
+
}
|
|
24
|
+
return (decoded.split(";")[0] ?? "").trim();
|
|
25
|
+
}
|
|
26
|
+
/** Why a dialled number is not valid, or null when it is. */
|
|
27
|
+
export function telProblem(number) {
|
|
28
|
+
if (GEOGRAPHIC_E164.test(number) || NON_GEOGRAPHIC_NATIONAL.test(number))
|
|
29
|
+
return null;
|
|
30
|
+
if (NON_GEOGRAPHIC_INTERNATIONAL.test(number))
|
|
31
|
+
return "a 13, 1300 or 1800 number cannot be dialled in international form, so it needs its national digits";
|
|
32
|
+
if (/[^+\d]/.test(number))
|
|
33
|
+
return "it has spaces or punctuation, so it needs digits only";
|
|
34
|
+
if (/^\+?0/.test(number))
|
|
35
|
+
return "it starts with 0, so it needs +61 and no leading 0";
|
|
36
|
+
return "it is not an Australian number in +61 format";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The number a business writes ("+61255500100", "02 5550 0100", "1300 555 010", "tel:+61255500100") as the dialled
|
|
40
|
+
* number a `tel:` link should carry. Text that is not an Australian number comes back as typed, without `tel:`.
|
|
41
|
+
*/
|
|
42
|
+
export function dialledFromBusinessNumber(written) {
|
|
43
|
+
const compact = written.trim().replace(/^tel:/i, "").replace(/[\s().-]/g, "");
|
|
44
|
+
if (/^\+61/.test(compact))
|
|
45
|
+
return NON_GEOGRAPHIC_INTERNATIONAL.test(compact) ? compact.slice(3) : compact;
|
|
46
|
+
if (/^0[23478]\d{8}$/.test(compact))
|
|
47
|
+
return `+61${compact.slice(1)}`;
|
|
48
|
+
return compact;
|
|
49
|
+
}
|
|
50
|
+
/** The same normalisation through the shared `telHrefFor` for an E.164 value. */
|
|
51
|
+
export function dialledFromE164(e164) {
|
|
52
|
+
return telHrefFor(e164).replace(/^tel:/i, "");
|
|
53
|
+
}
|
|
54
|
+
export function checkTelLinks(pages, facts, _factsReason, recordedPhones = []) {
|
|
55
|
+
const findings = [];
|
|
56
|
+
const evidence = facts?.phone ? dialledFromE164(facts.phone.e164) : undefined;
|
|
57
|
+
const recorded = recordedPhones.map(dialledFromBusinessNumber);
|
|
58
|
+
let links = 0;
|
|
59
|
+
let compared = 0;
|
|
60
|
+
for (const page of pages) {
|
|
61
|
+
const anchors = findAll(page.body, (element) => (element.tag === "a" || element.tag === "area") && /^\s*tel:/i.test(element.attrs.href ?? ""));
|
|
62
|
+
links += anchors.length;
|
|
63
|
+
const fromSchema = page.schemaTelephones.map(dialledFromBusinessNumber);
|
|
64
|
+
const expected = fromSchema.length > 0 ? fromSchema : evidence !== undefined ? [evidence] : [];
|
|
65
|
+
const source = fromSchema.length > 0 ? "the page's own LocalBusiness data" : "the evidence file";
|
|
66
|
+
const allowed = new Set([...expected, ...recorded]);
|
|
67
|
+
const formatProblems = new Map();
|
|
68
|
+
const mismatches = new Set();
|
|
69
|
+
for (const anchor of anchors) {
|
|
70
|
+
const href = anchor.attrs.href.trim();
|
|
71
|
+
const number = dialledNumber(href);
|
|
72
|
+
const problem = telProblem(number);
|
|
73
|
+
if (problem)
|
|
74
|
+
formatProblems.set(href, problem);
|
|
75
|
+
else if (expected.length > 0) {
|
|
76
|
+
compared += 1;
|
|
77
|
+
if (!allowed.has(number))
|
|
78
|
+
mismatches.add(href);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const [href, problem] of formatProblems) {
|
|
82
|
+
findings.push(failFinding("C10", page.path, `The phone link ${href} on ${page.path} is not valid: ${problem}.`));
|
|
83
|
+
}
|
|
84
|
+
if (mismatches.size > 0) {
|
|
85
|
+
const shown = [...expected].map((number) => `tel:${number}`).join(" or ");
|
|
86
|
+
findings.push(failFinding("C10", page.path, `The phone link${mismatches.size === 1 ? "" : "s"} ${[...mismatches].join(", ")} on ${page.path} ${mismatches.size === 1 ? "is" : "are"} not the business phone in ${source} (${shown}).`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (findings.length > 0)
|
|
90
|
+
return findings;
|
|
91
|
+
if (links === 0)
|
|
92
|
+
return [notApplicableFinding("C10", "The sampled pages have no tel: links to check.")];
|
|
93
|
+
// Valid tel: links with no business phone to compare them with (no LocalBusiness telephone, and the facts are missing,
|
|
94
|
+
// unreadable or hold no phone): the check applies but could not run, so it returns no finding rather than pass.
|
|
95
|
+
if (compared === 0)
|
|
96
|
+
return [];
|
|
97
|
+
return [passFinding("C10", `${links === 1 ? "The one tel: link" : `All ${links} tel: links`} on ${pages.length} page${pages.length === 1 ? "" : "s"} ${links === 1 ? "is" : "are"} valid and ${links === 1 ? "matches" : "match"} the business phone.`)];
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=phone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phone.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/phone.ts"],"names":[],"mappings":"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","sourcesContent":["import { findAll } from \"./html.js\";\nimport type { ShellPage } from \"./page.js\";\nimport { telHrefFor } from \"./facts.js\";\nimport { failFinding, notApplicableFinding, passFinding, type ShellFinding, type ShellProjectFacts } from \"./types.js\";\n\n/**\n * C10: every `tel:` href is Australian E.164 (`+61…`), or the national digits for a 13, 1300 or 1800 number, which\n * cannot be dialled in international form, and equals the business phone. The business phone is the `telephone` in the\n * page's own LocalBusiness JSON-LD; a page with none falls back to the evidence file. A project can record further\n * numbers (`siteSystem.phones`), such as a second line. With no phone to compare against, the format is still checked\n * and the comparison is reported as not applicable.\n */\n\nconst GEOGRAPHIC_E164 = /^\\+61[23478]\\d{8}$/;\nconst NON_GEOGRAPHIC_NATIONAL = /^(?:1[38]00\\d{6}|13(?!00)\\d{4})$/;\nconst NON_GEOGRAPHIC_INTERNATIONAL = /^\\+61(?:1[38]00\\d{6}|13(?!00)\\d{4})$/;\n\n/** The dialled number in a `tel:` href, without parameters such as `;ext=`. */\nfunction dialledNumber(href: string): string {\n const raw = href.trim().replace(/^tel:/i, \"\");\n let decoded = raw;\n try {\n decoded = decodeURIComponent(raw);\n } catch {\n decoded = raw;\n }\n return (decoded.split(\";\")[0] ?? \"\").trim();\n}\n\n/** Why a dialled number is not valid, or null when it is. */\nexport function telProblem(number: string): string | null {\n if (GEOGRAPHIC_E164.test(number) || NON_GEOGRAPHIC_NATIONAL.test(number)) return null;\n if (NON_GEOGRAPHIC_INTERNATIONAL.test(number)) return \"a 13, 1300 or 1800 number cannot be dialled in international form, so it needs its national digits\";\n if (/[^+\\d]/.test(number)) return \"it has spaces or punctuation, so it needs digits only\";\n if (/^\\+?0/.test(number)) return \"it starts with 0, so it needs +61 and no leading 0\";\n return \"it is not an Australian number in +61 format\";\n}\n\n/**\n * The number a business writes (\"+61255500100\", \"02 5550 0100\", \"1300 555 010\", \"tel:+61255500100\") as the dialled\n * number a `tel:` link should carry. Text that is not an Australian number comes back as typed, without `tel:`.\n */\nexport function dialledFromBusinessNumber(written: string): string {\n const compact = written.trim().replace(/^tel:/i, \"\").replace(/[\\s().-]/g, \"\");\n if (/^\\+61/.test(compact)) return NON_GEOGRAPHIC_INTERNATIONAL.test(compact) ? compact.slice(3) : compact;\n if (/^0[23478]\\d{8}$/.test(compact)) return `+61${compact.slice(1)}`;\n return compact;\n}\n\n/** The same normalisation through the shared `telHrefFor` for an E.164 value. */\nexport function dialledFromE164(e164: string): string {\n return telHrefFor(e164).replace(/^tel:/i, \"\");\n}\n\nexport function checkTelLinks(\n pages: readonly ShellPage[],\n facts: ShellProjectFacts | null,\n _factsReason: string,\n recordedPhones: readonly string[] = [],\n): ShellFinding[] {\n const findings: ShellFinding[] = [];\n const evidence = facts?.phone ? dialledFromE164(facts.phone.e164) : undefined;\n const recorded = recordedPhones.map(dialledFromBusinessNumber);\n let links = 0;\n let compared = 0;\n for (const page of pages) {\n const anchors = findAll(page.body, (element) => (element.tag === \"a\" || element.tag === \"area\") && /^\\s*tel:/i.test(element.attrs.href ?? \"\"));\n links += anchors.length;\n const fromSchema = page.schemaTelephones.map(dialledFromBusinessNumber);\n const expected = fromSchema.length > 0 ? fromSchema : evidence !== undefined ? [evidence] : [];\n const source = fromSchema.length > 0 ? \"the page's own LocalBusiness data\" : \"the evidence file\";\n const allowed = new Set([...expected, ...recorded]);\n const formatProblems = new Map<string, string>();\n const mismatches = new Set<string>();\n for (const anchor of anchors) {\n const href = anchor.attrs.href!.trim();\n const number = dialledNumber(href);\n const problem = telProblem(number);\n if (problem) formatProblems.set(href, problem);\n else if (expected.length > 0) {\n compared += 1;\n if (!allowed.has(number)) mismatches.add(href);\n }\n }\n for (const [href, problem] of formatProblems) {\n findings.push(failFinding(\"C10\", page.path, `The phone link ${href} on ${page.path} is not valid: ${problem}.`));\n }\n if (mismatches.size > 0) {\n const shown = [...expected].map((number) => `tel:${number}`).join(\" or \");\n findings.push(failFinding(\"C10\", page.path, `The phone link${mismatches.size === 1 ? \"\" : \"s\"} ${[...mismatches].join(\", \")} on ${page.path} ${mismatches.size === 1 ? \"is\" : \"are\"} not the business phone in ${source} (${shown}).`));\n }\n }\n if (findings.length > 0) return findings;\n if (links === 0) return [notApplicableFinding(\"C10\", \"The sampled pages have no tel: links to check.\")];\n // Valid tel: links with no business phone to compare them with (no LocalBusiness telephone, and the facts are missing,\n // unreadable or hold no phone): the check applies but could not run, so it returns no finding rather than pass.\n if (compared === 0) return [];\n return [passFinding(\"C10\", `${links === 1 ? \"The one tel: link\" : `All ${links} tel: links`} on ${pages.length} page${pages.length === 1 ? \"\" : \"s\"} ${links === 1 ? \"is\" : \"are\"} valid and ${links === 1 ? \"matches\" : \"match\"} the business phone.`)];\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type HtmlElement } from "./html.js";
|
|
2
|
+
import { type ShellPage } from "./page.js";
|
|
3
|
+
import { type ShellFinding } from "./types.js";
|
|
4
|
+
/** Whether the element is rendered from the feed, or is a customer's own words. */
|
|
5
|
+
export declare function isFeedRendered(element: HtmlElement): boolean;
|
|
6
|
+
export declare function checkProofNumbers(pages: readonly ShellPage[]): ShellFinding[];
|
|
7
|
+
//# sourceMappingURL=proof-numbers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof-numbers.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/proof-numbers.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AA6BvF,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAS5D;AAoBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,YAAY,EAAE,CAkB7E"}
|