@brandlab-digital/conformance 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.js +4 -2
- package/dist/engineering.js.map +1 -1
- package/dist/public-copy.d.ts +7 -0
- package/dist/public-copy.d.ts.map +1 -1
- package/dist/public-copy.js +8 -3
- package/dist/public-copy.js.map +1 -1
- package/dist/ready-cli.d.ts +38 -0
- package/dist/ready-cli.d.ts.map +1 -0
- package/dist/ready-cli.js +327 -0
- package/dist/ready-cli.js.map +1 -0
- package/dist/site-system/checks.d.ts +389 -0
- package/dist/site-system/checks.d.ts.map +1 -0
- package/dist/site-system/checks.js +96 -0
- package/dist/site-system/checks.js.map +1 -0
- package/dist/site-system/config.d.ts +40 -0
- package/dist/site-system/config.d.ts.map +1 -0
- package/dist/site-system/config.js +214 -0
- package/dist/site-system/config.js.map +1 -0
- package/dist/site-system/index.d.ts +14 -0
- package/dist/site-system/index.d.ts.map +1 -0
- package/dist/site-system/index.js +14 -0
- package/dist/site-system/index.js.map +1 -0
- package/dist/site-system/journeys/access.d.ts +7 -0
- package/dist/site-system/journeys/access.d.ts.map +1 -0
- package/dist/site-system/journeys/access.js +263 -0
- package/dist/site-system/journeys/access.js.map +1 -0
- package/dist/site-system/journeys/browser.d.ts +79 -0
- package/dist/site-system/journeys/browser.d.ts.map +1 -0
- package/dist/site-system/journeys/browser.js +218 -0
- package/dist/site-system/journeys/browser.js.map +1 -0
- package/dist/site-system/journeys/conversion.d.ts +3 -0
- package/dist/site-system/journeys/conversion.d.ts.map +1 -0
- package/dist/site-system/journeys/conversion.js +152 -0
- package/dist/site-system/journeys/conversion.js.map +1 -0
- package/dist/site-system/journeys/menu.d.ts +11 -0
- package/dist/site-system/journeys/menu.d.ts.map +1 -0
- package/dist/site-system/journeys/menu.js +395 -0
- package/dist/site-system/journeys/menu.js.map +1 -0
- package/dist/site-system/journeys/page-helpers.d.ts +146 -0
- package/dist/site-system/journeys/page-helpers.d.ts.map +1 -0
- package/dist/site-system/journeys/page-helpers.js +426 -0
- package/dist/site-system/journeys/page-helpers.js.map +1 -0
- package/dist/site-system/journeys/plan.d.ts +82 -0
- package/dist/site-system/journeys/plan.d.ts.map +1 -0
- package/dist/site-system/journeys/plan.js +112 -0
- package/dist/site-system/journeys/plan.js.map +1 -0
- package/dist/site-system/journeys/shell.d.ts +24 -0
- package/dist/site-system/journeys/shell.d.ts.map +1 -0
- package/dist/site-system/journeys/shell.js +269 -0
- package/dist/site-system/journeys/shell.js.map +1 -0
- package/dist/site-system/playwright-report.d.ts +22 -0
- package/dist/site-system/playwright-report.d.ts.map +1 -0
- package/dist/site-system/playwright-report.js +131 -0
- package/dist/site-system/playwright-report.js.map +1 -0
- package/dist/site-system/records.d.ts +30 -0
- package/dist/site-system/records.d.ts.map +1 -0
- package/dist/site-system/records.js +98 -0
- package/dist/site-system/records.js.map +1 -0
- package/dist/site-system/report.d.ts +116 -0
- package/dist/site-system/report.d.ts.map +1 -0
- package/dist/site-system/report.js +366 -0
- package/dist/site-system/report.js.map +1 -0
- package/dist/site-system/rule-modules.d.ts +8 -0
- package/dist/site-system/rule-modules.d.ts.map +1 -0
- package/dist/site-system/rule-modules.js +33 -0
- package/dist/site-system/rule-modules.js.map +1 -0
- package/dist/site-system/rules/design/d4-distinctness.d.ts +36 -0
- package/dist/site-system/rules/design/d4-distinctness.d.ts.map +1 -0
- package/dist/site-system/rules/design/d4-distinctness.js +207 -0
- package/dist/site-system/rules/design/d4-distinctness.js.map +1 -0
- package/dist/site-system/rules/design/index.d.ts +17 -0
- package/dist/site-system/rules/design/index.d.ts.map +1 -0
- package/dist/site-system/rules/design/index.js +7 -0
- package/dist/site-system/rules/design/index.js.map +1 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.d.ts +31 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.js +211 -0
- package/dist/site-system/rules/enquiry/c2-quote-form.js.map +1 -0
- package/dist/site-system/rules/enquiry/common.d.ts +37 -0
- package/dist/site-system/rules/enquiry/common.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/common.js +59 -0
- package/dist/site-system/rules/enquiry/common.js.map +1 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts +8 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.js +59 -0
- package/dist/site-system/rules/enquiry/e1-recorded-titles.js.map +1 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts +22 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.js +55 -0
- package/dist/site-system/rules/enquiry/e9-cost-guides.js.map +1 -0
- package/dist/site-system/rules/enquiry/html.d.ts +53 -0
- package/dist/site-system/rules/enquiry/html.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/html.js +294 -0
- package/dist/site-system/rules/enquiry/html.js.map +1 -0
- package/dist/site-system/rules/enquiry/index.d.ts +17 -0
- package/dist/site-system/rules/enquiry/index.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/index.js +26 -0
- package/dist/site-system/rules/enquiry/index.js.map +1 -0
- package/dist/site-system/rules/enquiry/k8-sections.d.ts +17 -0
- package/dist/site-system/rules/enquiry/k8-sections.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/k8-sections.js +76 -0
- package/dist/site-system/rules/enquiry/k8-sections.js.map +1 -0
- package/dist/site-system/rules/enquiry/literal.d.ts +29 -0
- package/dist/site-system/rules/enquiry/literal.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/literal.js +189 -0
- package/dist/site-system/rules/enquiry/literal.js.map +1 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts +73 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.js +275 -0
- package/dist/site-system/rules/enquiry/t11-licence-classes.js.map +1 -0
- package/dist/site-system/rules/enquiry/t5-reviews.d.ts +23 -0
- package/dist/site-system/rules/enquiry/t5-reviews.d.ts.map +1 -0
- package/dist/site-system/rules/enquiry/t5-reviews.js +91 -0
- package/dist/site-system/rules/enquiry/t5-reviews.js.map +1 -0
- package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
- package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
- package/dist/site-system/rules/form/c11-contact-route.js +132 -0
- package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.js +67 -0
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
- package/dist/site-system/rules/form/index.d.ts +14 -0
- package/dist/site-system/rules/form/index.d.ts.map +1 -0
- package/dist/site-system/rules/form/index.js +13 -0
- package/dist/site-system/rules/form/index.js.map +1 -0
- package/dist/site-system/rules/library/common.d.ts +36 -0
- package/dist/site-system/rules/library/common.d.ts.map +1 -0
- package/dist/site-system/rules/library/common.js +70 -0
- package/dist/site-system/rules/library/common.js.map +1 -0
- package/dist/site-system/rules/library/index.d.ts +16 -0
- package/dist/site-system/rules/library/index.d.ts.map +1 -0
- package/dist/site-system/rules/library/index.js +19 -0
- package/dist/site-system/rules/library/index.js.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.js +97 -0
- package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
- package/dist/site-system/rules/navigation/extract.d.ts +50 -0
- package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/extract.js +220 -0
- package/dist/site-system/rules/navigation/extract.js.map +1 -0
- package/dist/site-system/rules/navigation/html.d.ts +23 -0
- package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/html.js +219 -0
- package/dist/site-system/rules/navigation/html.js.map +1 -0
- package/dist/site-system/rules/navigation/index.d.ts +37 -0
- package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/index.js +163 -0
- package/dist/site-system/rules/navigation/index.js.map +1 -0
- package/dist/site-system/rules/navigation/record.d.ts +8 -0
- package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/record.js +39 -0
- package/dist/site-system/rules/navigation/record.js.map +1 -0
- package/dist/site-system/rules/sections/exif.d.ts +22 -0
- package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
- package/dist/site-system/rules/sections/exif.js +218 -0
- package/dist/site-system/rules/sections/exif.js.map +1 -0
- package/dist/site-system/rules/sections/images.d.ts +31 -0
- package/dist/site-system/rules/sections/images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/images.js +96 -0
- package/dist/site-system/rules/sections/images.js.map +1 -0
- package/dist/site-system/rules/sections/index.d.ts +20 -0
- package/dist/site-system/rules/sections/index.d.ts.map +1 -0
- package/dist/site-system/rules/sections/index.js +34 -0
- package/dist/site-system/rules/sections/index.js.map +1 -0
- package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
- package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k11-density.js +88 -0
- package/dist/site-system/rules/sections/k11-density.js.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.js +70 -0
- package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.js +186 -0
- package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.js +41 -0
- package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.js +92 -0
- package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
- package/dist/site-system/rules/shell/banned-wording.d.ts +24 -0
- package/dist/site-system/rules/shell/banned-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/banned-wording.js +212 -0
- package/dist/site-system/rules/shell/banned-wording.js.map +1 -0
- package/dist/site-system/rules/shell/budgets.d.ts +43 -0
- package/dist/site-system/rules/shell/budgets.d.ts.map +1 -0
- package/dist/site-system/rules/shell/budgets.js +297 -0
- package/dist/site-system/rules/shell/budgets.js.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts +17 -0
- package/dist/site-system/rules/shell/cta-labels.d.ts.map +1 -0
- package/dist/site-system/rules/shell/cta-labels.js +86 -0
- package/dist/site-system/rules/shell/cta-labels.js.map +1 -0
- package/dist/site-system/rules/shell/facts.d.ts +33 -0
- package/dist/site-system/rules/shell/facts.d.ts.map +1 -0
- package/dist/site-system/rules/shell/facts.js +217 -0
- package/dist/site-system/rules/shell/facts.js.map +1 -0
- package/dist/site-system/rules/shell/headings.d.ts +5 -0
- package/dist/site-system/rules/shell/headings.d.ts.map +1 -0
- package/dist/site-system/rules/shell/headings.js +23 -0
- package/dist/site-system/rules/shell/headings.js.map +1 -0
- package/dist/site-system/rules/shell/hero-location.d.ts +4 -0
- package/dist/site-system/rules/shell/hero-location.d.ts.map +1 -0
- package/dist/site-system/rules/shell/hero-location.js +45 -0
- package/dist/site-system/rules/shell/hero-location.js.map +1 -0
- package/dist/site-system/rules/shell/html.d.ts +53 -0
- package/dist/site-system/rules/shell/html.d.ts.map +1 -0
- package/dist/site-system/rules/shell/html.js +341 -0
- package/dist/site-system/rules/shell/html.js.map +1 -0
- package/dist/site-system/rules/shell/index.d.ts +18 -0
- package/dist/site-system/rules/shell/index.d.ts.map +1 -0
- package/dist/site-system/rules/shell/index.js +65 -0
- package/dist/site-system/rules/shell/index.js.map +1 -0
- package/dist/site-system/rules/shell/licence.d.ts +4 -0
- package/dist/site-system/rules/shell/licence.d.ts.map +1 -0
- package/dist/site-system/rules/shell/licence.js +60 -0
- package/dist/site-system/rules/shell/licence.js.map +1 -0
- package/dist/site-system/rules/shell/location-wording.d.ts +13 -0
- package/dist/site-system/rules/shell/location-wording.d.ts.map +1 -0
- package/dist/site-system/rules/shell/location-wording.js +15 -0
- package/dist/site-system/rules/shell/location-wording.js.map +1 -0
- package/dist/site-system/rules/shell/navigation.d.ts +5 -0
- package/dist/site-system/rules/shell/navigation.d.ts.map +1 -0
- package/dist/site-system/rules/shell/navigation.js +272 -0
- package/dist/site-system/rules/shell/navigation.js.map +1 -0
- package/dist/site-system/rules/shell/overlays.d.ts +4 -0
- package/dist/site-system/rules/shell/overlays.d.ts.map +1 -0
- package/dist/site-system/rules/shell/overlays.js +93 -0
- package/dist/site-system/rules/shell/overlays.js.map +1 -0
- package/dist/site-system/rules/shell/page.d.ts +51 -0
- package/dist/site-system/rules/shell/page.d.ts.map +1 -0
- package/dist/site-system/rules/shell/page.js +256 -0
- package/dist/site-system/rules/shell/page.js.map +1 -0
- package/dist/site-system/rules/shell/phone.d.ts +13 -0
- package/dist/site-system/rules/shell/phone.d.ts.map +1 -0
- package/dist/site-system/rules/shell/phone.js +99 -0
- package/dist/site-system/rules/shell/phone.js.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts +7 -0
- package/dist/site-system/rules/shell/proof-numbers.d.ts.map +1 -0
- package/dist/site-system/rules/shell/proof-numbers.js +81 -0
- package/dist/site-system/rules/shell/proof-numbers.js.map +1 -0
- package/dist/site-system/rules/shell/types.d.ts +80 -0
- package/dist/site-system/rules/shell/types.d.ts.map +1 -0
- package/dist/site-system/rules/shell/types.js +17 -0
- package/dist/site-system/rules/shell/types.js.map +1 -0
- package/dist/site-system/runners.d.ts +37 -0
- package/dist/site-system/runners.d.ts.map +1 -0
- package/dist/site-system/runners.js +48 -0
- package/dist/site-system/runners.js.map +1 -0
- package/dist/site-system/samples.d.ts +43 -0
- package/dist/site-system/samples.d.ts.map +1 -0
- package/dist/site-system/samples.js +99 -0
- package/dist/site-system/samples.js.map +1 -0
- package/dist/site-system/steps.d.ts +66 -0
- package/dist/site-system/steps.d.ts.map +1 -0
- package/dist/site-system/steps.js +45 -0
- package/dist/site-system/steps.js.map +1 -0
- package/dist/site-system/streamed-html.d.ts +9 -0
- package/dist/site-system/streamed-html.d.ts.map +1 -0
- package/dist/site-system/streamed-html.js +43 -0
- package/dist/site-system/streamed-html.js.map +1 -0
- package/dist/site-system-e2e.d.ts +6 -0
- package/dist/site-system-e2e.d.ts.map +1 -0
- package/dist/site-system-e2e.js +54 -0
- package/dist/site-system-e2e.js.map +1 -0
- package/package.json +22 -5
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small tolerant HTML reader for the rendered rules: it builds an element tree from server HTML so a rule can ask
|
|
3
|
+
* for "the nav in the header" and read links and text in order. It is not a validating parser; it understands
|
|
4
|
+
* comments, void elements, raw-text elements, quoted and bare attributes, and the implied end of a list item.
|
|
5
|
+
*/
|
|
6
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
7
|
+
const RAW_TEXT = new Set(["script", "style", "textarea", "noscript", "template"]);
|
|
8
|
+
const LIST_BOUNDARY = new Set(["ul", "ol", "menu"]);
|
|
9
|
+
const ENTITIES = {
|
|
10
|
+
amp: "&",
|
|
11
|
+
lt: "<",
|
|
12
|
+
gt: ">",
|
|
13
|
+
quot: '"',
|
|
14
|
+
apos: "'",
|
|
15
|
+
nbsp: " ",
|
|
16
|
+
ndash: "–",
|
|
17
|
+
mdash: "—",
|
|
18
|
+
lsquo: "‘",
|
|
19
|
+
rsquo: "’",
|
|
20
|
+
ldquo: "“",
|
|
21
|
+
rdquo: "”",
|
|
22
|
+
hellip: "…",
|
|
23
|
+
middot: "·",
|
|
24
|
+
rarr: "→",
|
|
25
|
+
};
|
|
26
|
+
export function decodeEntities(value) {
|
|
27
|
+
return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, body) => {
|
|
28
|
+
if (body.startsWith("#")) {
|
|
29
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
30
|
+
return Number.isFinite(code) && code > 0 && code < 0x110000 ? String.fromCodePoint(code) : whole;
|
|
31
|
+
}
|
|
32
|
+
return ENTITIES[body.toLowerCase()] ?? whole;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function isSpace(char) {
|
|
36
|
+
return char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f";
|
|
37
|
+
}
|
|
38
|
+
function readOpenTag(html, start) {
|
|
39
|
+
let index = start + 1;
|
|
40
|
+
let name = "";
|
|
41
|
+
while (index < html.length && !isSpace(html[index]) && html[index] !== "/" && html[index] !== ">") {
|
|
42
|
+
name += html[index];
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
if (!/^[a-zA-Z][a-zA-Z0-9:-]*$/.test(name))
|
|
46
|
+
return null;
|
|
47
|
+
const attrs = {};
|
|
48
|
+
let selfClosing = false;
|
|
49
|
+
while (index < html.length) {
|
|
50
|
+
while (isSpace(html[index]))
|
|
51
|
+
index += 1;
|
|
52
|
+
const char = html[index];
|
|
53
|
+
if (char === undefined)
|
|
54
|
+
return null;
|
|
55
|
+
if (char === ">")
|
|
56
|
+
return { tag: name.toLowerCase(), attrs, selfClosing, end: index + 1 };
|
|
57
|
+
if (char === "/") {
|
|
58
|
+
selfClosing = html[index + 1] === ">";
|
|
59
|
+
index += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let key = "";
|
|
63
|
+
while (index < html.length && !isSpace(html[index]) && html[index] !== "=" && html[index] !== ">" && html[index] !== "/") {
|
|
64
|
+
key += html[index];
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
while (isSpace(html[index]))
|
|
68
|
+
index += 1;
|
|
69
|
+
let value = "";
|
|
70
|
+
if (html[index] === "=") {
|
|
71
|
+
index += 1;
|
|
72
|
+
while (isSpace(html[index]))
|
|
73
|
+
index += 1;
|
|
74
|
+
const quote = html[index];
|
|
75
|
+
if (quote === '"' || quote === "'") {
|
|
76
|
+
const close = html.indexOf(quote, index + 1);
|
|
77
|
+
if (close === -1)
|
|
78
|
+
return null;
|
|
79
|
+
value = html.slice(index + 1, close);
|
|
80
|
+
index = close + 1;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
while (index < html.length && !isSpace(html[index]) && html[index] !== ">") {
|
|
84
|
+
value += html[index];
|
|
85
|
+
index += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (key !== "" && !(key.toLowerCase() in attrs))
|
|
90
|
+
attrs[key.toLowerCase()] = decodeEntities(value);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Builds the element tree for a document or fragment. The returned root is a `#document` element. */
|
|
95
|
+
export function parseHtml(html) {
|
|
96
|
+
const root = { tag: "#document", attrs: {}, children: [], parent: null };
|
|
97
|
+
let current = root;
|
|
98
|
+
let index = 0;
|
|
99
|
+
const addText = (text) => {
|
|
100
|
+
if (text !== "")
|
|
101
|
+
current.children.push(text);
|
|
102
|
+
};
|
|
103
|
+
while (index < html.length) {
|
|
104
|
+
const lt = html.indexOf("<", index);
|
|
105
|
+
if (lt === -1) {
|
|
106
|
+
addText(decodeEntities(html.slice(index)));
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (lt > index)
|
|
110
|
+
addText(decodeEntities(html.slice(index, lt)));
|
|
111
|
+
const next = html[lt + 1];
|
|
112
|
+
if (html.startsWith("<!--", lt)) {
|
|
113
|
+
const end = html.indexOf("-->", lt + 4);
|
|
114
|
+
index = end === -1 ? html.length : end + 3;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (next === "!" || next === "?") {
|
|
118
|
+
const end = html.indexOf(">", lt);
|
|
119
|
+
index = end === -1 ? html.length : end + 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (next === "/") {
|
|
123
|
+
const end = html.indexOf(">", lt);
|
|
124
|
+
const name = html.slice(lt + 2, end === -1 ? html.length : end).trim().toLowerCase();
|
|
125
|
+
index = end === -1 ? html.length : end + 1;
|
|
126
|
+
for (let open = current; open && open !== root; open = open.parent) {
|
|
127
|
+
if (open.tag === name) {
|
|
128
|
+
current = open.parent ?? root;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const tag = readOpenTag(html, lt);
|
|
135
|
+
if (!tag) {
|
|
136
|
+
addText("<");
|
|
137
|
+
index = lt + 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
index = tag.end;
|
|
141
|
+
if (tag.tag === "li") {
|
|
142
|
+
// An open list item ends when the next one starts.
|
|
143
|
+
for (let open = current; open && open !== root && !LIST_BOUNDARY.has(open.tag); open = open.parent) {
|
|
144
|
+
if (open.tag === "li") {
|
|
145
|
+
current = open.parent ?? root;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const element = { tag: tag.tag, attrs: tag.attrs, children: [], parent: current };
|
|
151
|
+
current.children.push(element);
|
|
152
|
+
if (RAW_TEXT.has(tag.tag) && !tag.selfClosing) {
|
|
153
|
+
const close = new RegExp(`</${tag.tag}\\s*>`, "i").exec(html.slice(index));
|
|
154
|
+
index = close ? index + close.index + close[0].length : html.length;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (VOID.has(tag.tag) || tag.selfClosing)
|
|
158
|
+
continue;
|
|
159
|
+
current = element;
|
|
160
|
+
}
|
|
161
|
+
return root;
|
|
162
|
+
}
|
|
163
|
+
export function elementChildren(element) {
|
|
164
|
+
return element.children.filter((child) => typeof child !== "string");
|
|
165
|
+
}
|
|
166
|
+
/** Every descendant element in document order. */
|
|
167
|
+
export function descendants(element) {
|
|
168
|
+
const found = [];
|
|
169
|
+
const visit = (node) => {
|
|
170
|
+
for (const child of node.children) {
|
|
171
|
+
if (typeof child === "string")
|
|
172
|
+
continue;
|
|
173
|
+
found.push(child);
|
|
174
|
+
visit(child);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
visit(element);
|
|
178
|
+
return found;
|
|
179
|
+
}
|
|
180
|
+
export function isInside(element, ancestor) {
|
|
181
|
+
for (let node = element.parent; node; node = node.parent) {
|
|
182
|
+
if (node === ancestor)
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
export function hasAncestor(element, test) {
|
|
188
|
+
for (let node = element.parent; node; node = node.parent) {
|
|
189
|
+
if (test(node))
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const HIDDEN_TEXT_CLASS = /(^|\s)(sr-only|visually-hidden|screen-reader-text)(\s|$)/;
|
|
195
|
+
/** The visible text of an element: no SVG, no screen-reader-only text, no hidden or aria-hidden parts. */
|
|
196
|
+
export function textOf(element) {
|
|
197
|
+
const parts = [];
|
|
198
|
+
const visit = (node) => {
|
|
199
|
+
for (const child of node.children) {
|
|
200
|
+
if (typeof child === "string") {
|
|
201
|
+
parts.push(child);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (child.tag === "svg" || child.tag === "br") {
|
|
205
|
+
if (child.tag === "br")
|
|
206
|
+
parts.push(" ");
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if ("hidden" in child.attrs || child.attrs["aria-hidden"] === "true" || HIDDEN_TEXT_CLASS.test(child.attrs.class ?? ""))
|
|
210
|
+
continue;
|
|
211
|
+
parts.push(" ");
|
|
212
|
+
visit(child);
|
|
213
|
+
parts.push(" ");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
visit(element);
|
|
217
|
+
return parts.join("").replace(/\s+/g, " ").trim();
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AACtI,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAClF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAEpD,MAAM,QAAQ,GAAqC;IACjD,GAAG,EAAE,GAAG;IACR,EAAE,EAAE,GAAG;IACP,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,GAAG;IACX,IAAI,EAAE,GAAG;CACV,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,+BAA+B,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC5E,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1H,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACnG,CAAC;QACD,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,KAAK,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO,CAAC,IAAwB;IACvC,OAAO,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC;AAC1F,CAAC;AAUD,SAAS,WAAW,CAAC,IAAY,EAAE,KAAa;IAC9C,IAAI,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC;IACtB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;QAClG,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,CAAC;IACb,CAAC;IACD,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,IAAI,KAAK,GAAG;YAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;QACzF,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,WAAW,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;YACtC,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QACD,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;YACzH,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;YACxB,KAAK,IAAI,CAAC,CAAC;YACX,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAAE,KAAK,IAAI,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;gBACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBAC7C,IAAI,KAAK,KAAK,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAC9B,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;gBACrC,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3E,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;oBACrB,KAAK,IAAI,CAAC,CAAC;gBACb,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,IAAI,GAAgB,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACtF,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE;QAC/B,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC,CAAC;IACF,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACpC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACd,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM;QACR,CAAC;QACD,IAAI,EAAE,GAAG,KAAK;YAAE,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YACxC,KAAK,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;YAC3C,SAAS;QACX,CAAC;QACD,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAClC,KAAK,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;YAC3C,SAAS;QACX,CAAC;QACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrF,KAAK,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;YAC3C,KAAK,IAAI,IAAI,GAAuB,OAAO,EAAE,IAAI,IAAI,IAAI,KAAK,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBACvF,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;oBACtB,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;oBAC9B,MAAM;gBACR,CAAC;YACH,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,CAAC;YACb,KAAK,GAAG,EAAE,GAAG,CAAC,CAAC;YACf,SAAS;QACX,CAAC;QACD,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC;QAChB,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;YACrB,mDAAmD;YACnD,KAAK,IAAI,IAAI,GAAuB,OAAO,EAAE,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBACvH,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;oBACtB,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;oBAC9B,MAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAgB,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAC/F,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;YAC3E,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;YACpE,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,WAAW;YAAE,SAAS;QACnD,OAAO,GAAG,OAAO,CAAC;IACpB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAoB;IAClD,OAAO,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAwB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;AAC7F,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,OAAoB;IAC9C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,CAAC,IAAiB,EAAE,EAAE;QAClC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,SAAS;YACxC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,KAAK,CAAC,KAAK,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAoB,EAAE,QAAqB;IAClE,KAAK,IAAI,IAAI,GAAuB,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC7E,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAoB,EAAE,IAAwC;IACxF,KAAK,IAAI,IAAI,GAAuB,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC7E,IAAI,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAC9B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,iBAAiB,GAAG,0DAA0D,CAAC;AAErF,0GAA0G;AAC1G,MAAM,UAAU,MAAM,CAAC,OAAoB;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAiB,EAAE,EAAE;QAClC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,SAAS;YACX,CAAC;YACD,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;gBAC9C,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACxC,SAAS;YACX,CAAC;YACD,IAAI,QAAQ,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;gBAAE,SAAS;YAClI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAChB,KAAK,CAAC,KAAK,CAAC,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACpD,CAAC","sourcesContent":["/**\n * A small tolerant HTML reader for the rendered rules: it builds an element tree from server HTML so a rule can ask\n * for \"the nav in the header\" and read links and text in order. It is not a validating parser; it understands\n * comments, void elements, raw-text elements, quoted and bare attributes, and the implied end of a list item.\n */\n\nexport interface HtmlElement {\n readonly tag: string;\n readonly attrs: Readonly<Record<string, string>>;\n readonly children: HtmlChild[];\n readonly parent: HtmlElement | null;\n}\n\nexport type HtmlChild = HtmlElement | string;\n\nconst VOID = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"param\", \"source\", \"track\", \"wbr\"]);\nconst RAW_TEXT = new Set([\"script\", \"style\", \"textarea\", \"noscript\", \"template\"]);\nconst LIST_BOUNDARY = new Set([\"ul\", \"ol\", \"menu\"]);\n\nconst ENTITIES: Readonly<Record<string, string>> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n ndash: \"–\",\n mdash: \"—\",\n lsquo: \"‘\",\n rsquo: \"’\",\n ldquo: \"“\",\n rdquo: \"”\",\n hellip: \"…\",\n middot: \"·\",\n rarr: \"→\",\n};\n\nexport function decodeEntities(value: string): string {\n return value.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (whole, body: string) => {\n if (body.startsWith(\"#\")) {\n const code = body[1] === \"x\" || body[1] === \"X\" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);\n return Number.isFinite(code) && code > 0 && code < 0x110000 ? String.fromCodePoint(code) : whole;\n }\n return ENTITIES[body.toLowerCase()] ?? whole;\n });\n}\n\nfunction isSpace(char: string | undefined): boolean {\n return char === \" \" || char === \"\\n\" || char === \"\\t\" || char === \"\\r\" || char === \"\\f\";\n}\n\ninterface OpenTag {\n readonly tag: string;\n readonly attrs: Record<string, string>;\n readonly selfClosing: boolean;\n /** The index just after the closing `>`. */\n readonly end: number;\n}\n\nfunction readOpenTag(html: string, start: number): OpenTag | null {\n let index = start + 1;\n let name = \"\";\n while (index < html.length && !isSpace(html[index]) && html[index] !== \"/\" && html[index] !== \">\") {\n name += html[index];\n index += 1;\n }\n if (!/^[a-zA-Z][a-zA-Z0-9:-]*$/.test(name)) return null;\n const attrs: Record<string, string> = {};\n let selfClosing = false;\n while (index < html.length) {\n while (isSpace(html[index])) index += 1;\n const char = html[index];\n if (char === undefined) return null;\n if (char === \">\") return { tag: name.toLowerCase(), attrs, selfClosing, end: index + 1 };\n if (char === \"/\") {\n selfClosing = html[index + 1] === \">\";\n index += 1;\n continue;\n }\n let key = \"\";\n while (index < html.length && !isSpace(html[index]) && html[index] !== \"=\" && html[index] !== \">\" && html[index] !== \"/\") {\n key += html[index];\n index += 1;\n }\n while (isSpace(html[index])) index += 1;\n let value = \"\";\n if (html[index] === \"=\") {\n index += 1;\n while (isSpace(html[index])) index += 1;\n const quote = html[index];\n if (quote === '\"' || quote === \"'\") {\n const close = html.indexOf(quote, index + 1);\n if (close === -1) return null;\n value = html.slice(index + 1, close);\n index = close + 1;\n } else {\n while (index < html.length && !isSpace(html[index]) && html[index] !== \">\") {\n value += html[index];\n index += 1;\n }\n }\n }\n if (key !== \"\" && !(key.toLowerCase() in attrs)) attrs[key.toLowerCase()] = decodeEntities(value);\n }\n return null;\n}\n\n/** Builds the element tree for a document or fragment. The returned root is a `#document` element. */\nexport function parseHtml(html: string): HtmlElement {\n const root: HtmlElement = { tag: \"#document\", attrs: {}, children: [], parent: null };\n let current = root;\n let index = 0;\n const addText = (text: string) => {\n if (text !== \"\") current.children.push(text);\n };\n while (index < html.length) {\n const lt = html.indexOf(\"<\", index);\n if (lt === -1) {\n addText(decodeEntities(html.slice(index)));\n break;\n }\n if (lt > index) addText(decodeEntities(html.slice(index, lt)));\n const next = html[lt + 1];\n if (html.startsWith(\"<!--\", lt)) {\n const end = html.indexOf(\"-->\", lt + 4);\n index = end === -1 ? html.length : end + 3;\n continue;\n }\n if (next === \"!\" || next === \"?\") {\n const end = html.indexOf(\">\", lt);\n index = end === -1 ? html.length : end + 1;\n continue;\n }\n if (next === \"/\") {\n const end = html.indexOf(\">\", lt);\n const name = html.slice(lt + 2, end === -1 ? html.length : end).trim().toLowerCase();\n index = end === -1 ? html.length : end + 1;\n for (let open: HtmlElement | null = current; open && open !== root; open = open.parent) {\n if (open.tag === name) {\n current = open.parent ?? root;\n break;\n }\n }\n continue;\n }\n const tag = readOpenTag(html, lt);\n if (!tag) {\n addText(\"<\");\n index = lt + 1;\n continue;\n }\n index = tag.end;\n if (tag.tag === \"li\") {\n // An open list item ends when the next one starts.\n for (let open: HtmlElement | null = current; open && open !== root && !LIST_BOUNDARY.has(open.tag); open = open.parent) {\n if (open.tag === \"li\") {\n current = open.parent ?? root;\n break;\n }\n }\n }\n const element: HtmlElement = { tag: tag.tag, attrs: tag.attrs, children: [], parent: current };\n current.children.push(element);\n if (RAW_TEXT.has(tag.tag) && !tag.selfClosing) {\n const close = new RegExp(`</${tag.tag}\\\\s*>`, \"i\").exec(html.slice(index));\n index = close ? index + close.index + close[0].length : html.length;\n continue;\n }\n if (VOID.has(tag.tag) || tag.selfClosing) continue;\n current = element;\n }\n return root;\n}\n\nexport function elementChildren(element: HtmlElement): HtmlElement[] {\n return element.children.filter((child): child is HtmlElement => typeof child !== \"string\");\n}\n\n/** Every descendant element in document order. */\nexport function descendants(element: HtmlElement): HtmlElement[] {\n const found: HtmlElement[] = [];\n const visit = (node: HtmlElement) => {\n for (const child of node.children) {\n if (typeof child === \"string\") continue;\n found.push(child);\n visit(child);\n }\n };\n visit(element);\n return found;\n}\n\nexport function isInside(element: HtmlElement, ancestor: HtmlElement): boolean {\n for (let node: HtmlElement | null = element.parent; node; node = node.parent) {\n if (node === ancestor) return true;\n }\n return false;\n}\n\nexport function hasAncestor(element: HtmlElement, test: (ancestor: HtmlElement) => boolean): boolean {\n for (let node: HtmlElement | null = element.parent; node; node = node.parent) {\n if (test(node)) return true;\n }\n return false;\n}\n\nconst HIDDEN_TEXT_CLASS = /(^|\\s)(sr-only|visually-hidden|screen-reader-text)(\\s|$)/;\n\n/** The visible text of an element: no SVG, no screen-reader-only text, no hidden or aria-hidden parts. */\nexport function textOf(element: HtmlElement): string {\n const parts: string[] = [];\n const visit = (node: HtmlElement) => {\n for (const child of node.children) {\n if (typeof child === \"string\") {\n parts.push(child);\n continue;\n }\n if (child.tag === \"svg\" || child.tag === \"br\") {\n if (child.tag === \"br\") parts.push(\" \");\n continue;\n }\n if (\"hidden\" in child.attrs || child.attrs[\"aria-hidden\"] === \"true\" || HIDDEN_TEXT_CLASS.test(child.attrs.class ?? \"\")) continue;\n parts.push(\" \");\n visit(child);\n parts.push(\" \");\n }\n };\n visit(element);\n return parts.join(\"\").replace(/\\s+/g, \" \").trim();\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N1, the rendered side: the header menu on the home page must satisfy the same rules as `defineBrandlabNavigation`
|
|
3
|
+
* (required links when their pages exist, About in the first two items, services before Areas, Contact last, labels
|
|
4
|
+
* from the closed list, every dropdown holding its hub link, no prices, split mode, a recorded order kept exactly).
|
|
5
|
+
* It reads the `<nav aria-label="Main">` in the `<header>`, so it works on the ui-kit header and on bespoke ones.
|
|
6
|
+
*
|
|
7
|
+
* The rules themselves live in `@brandlab-digital/site-core/navigation` and are read at run time, like the other
|
|
8
|
+
* site-core modules, so this package compiles without a site-core build and checks against the site's own copy.
|
|
9
|
+
*
|
|
10
|
+
* Which pages exist is read from the pages supplied and from the links on every one of them outside its own main menu
|
|
11
|
+
* (a page the footer links to exists). Money-service dropdowns are read from the header itself. What Romie recorded
|
|
12
|
+
* (an exact order, labels, an Areas dropdown) comes from `navigation` in `src/data/strategy.ts`.
|
|
13
|
+
*/
|
|
14
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
15
|
+
export interface NavigationRulesInput {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
pages: readonly {
|
|
18
|
+
path: string;
|
|
19
|
+
html: string;
|
|
20
|
+
}[];
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Applies N1 to the header menu on the home page. `recorded` is the `navigation` of the site's strategy record;
|
|
25
|
+
* `runNavigationRules` reads it from `src/data/strategy.ts` for you.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkRenderedNavigation(input: {
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
pages: readonly {
|
|
30
|
+
path: string;
|
|
31
|
+
html: string;
|
|
32
|
+
}[];
|
|
33
|
+
recorded?: Record<string, unknown>;
|
|
34
|
+
}): Promise<SiteSystemFinding[]>;
|
|
35
|
+
/** The N1 rendered rule, in the shape the ready gate's rendered runners share. */
|
|
36
|
+
export declare function runNavigationRules(input: NavigationRulesInput): Promise<readonly SiteSystemFinding[]>;
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AASzD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,WAAW,EAAE,MAAM,CAAC;CACrB;AAgHD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA4D/B;AAED,kFAAkF;AAClF,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAI3G"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { extractMainNavigation, headerActions, linkedPaths, findMainNav } from "./extract.js";
|
|
2
|
+
import { parseHtml } from "./html.js";
|
|
3
|
+
import { readRecordedNavigation } from "./record.js";
|
|
4
|
+
const NAVIGATION_MODULE = "@brandlab-digital/site-core/navigation";
|
|
5
|
+
let siteCoreNavigation;
|
|
6
|
+
function loadSiteCoreNavigation() {
|
|
7
|
+
siteCoreNavigation ??= import(NAVIGATION_MODULE).then((module) => {
|
|
8
|
+
if (typeof module.validateBrandlabNavigation !== "function" ||
|
|
9
|
+
typeof module.navigationPagesFromRoutes !== "function" ||
|
|
10
|
+
!module.BRANDLAB_NAV_PAGE_ALIASES ||
|
|
11
|
+
!module.BRANDLAB_NAV_LABELS) {
|
|
12
|
+
throw new Error("@brandlab-digital/site-core/navigation has no validateBrandlabNavigation: install site-core 3.9.0 or later with the conformance package.");
|
|
13
|
+
}
|
|
14
|
+
return module;
|
|
15
|
+
});
|
|
16
|
+
return siteCoreNavigation;
|
|
17
|
+
}
|
|
18
|
+
function finding(status, target, message) {
|
|
19
|
+
return { checkId: "N1", status, ...(target === undefined ? {} : { target }), message };
|
|
20
|
+
}
|
|
21
|
+
function canonical(path) {
|
|
22
|
+
const [beforeHash = ""] = path.split("#");
|
|
23
|
+
const [beforeQuery = ""] = beforeHash.split("?");
|
|
24
|
+
const trimmed = beforeQuery.length > 1 ? beforeQuery.replace(/\/+$/, "") : beforeQuery;
|
|
25
|
+
return (trimmed || "/").toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
function asList(value) {
|
|
28
|
+
return value === undefined ? [] : typeof value === "string" ? [value] : [...value];
|
|
29
|
+
}
|
|
30
|
+
/** The page each dropdown label is the hub of, for a label that is a button rather than a link. */
|
|
31
|
+
const HUB_PAGES = {
|
|
32
|
+
services: ["services"],
|
|
33
|
+
"more services": ["services"],
|
|
34
|
+
areas: ["areas"],
|
|
35
|
+
"our work": ["projects", "gallery"],
|
|
36
|
+
projects: ["projects"],
|
|
37
|
+
gallery: ["gallery"],
|
|
38
|
+
resources: ["guides", "faqs"],
|
|
39
|
+
guides: ["guides"],
|
|
40
|
+
blog: ["guides"],
|
|
41
|
+
about: ["about"],
|
|
42
|
+
faqs: ["faqs"],
|
|
43
|
+
contact: ["contact"],
|
|
44
|
+
};
|
|
45
|
+
/** A dropdown whose label is a button has no hub of its own; its hub is the panel link to the page the label names. */
|
|
46
|
+
function withHubs(items, pages) {
|
|
47
|
+
return items.map((item) => {
|
|
48
|
+
if (item.kind !== "menu" || item.href !== "")
|
|
49
|
+
return item;
|
|
50
|
+
const wanted = (HUB_PAGES[item.label.toLowerCase()] ?? []).flatMap((key) => asList(pages[key])).map(canonical);
|
|
51
|
+
const hub = item.links.find((link) => wanted.includes(canonical(link.href)));
|
|
52
|
+
return hub ? { ...item, href: hub.href, hubLabel: hub.label, links: item.links.filter((link) => link !== hub) } : item;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The money services the header itself shows. A dropdown is a money-service dropdown when its label is not a closed
|
|
57
|
+
* label, its hub is a page of its own (not the services hub or any other page the menu names) and its panel holds two
|
|
58
|
+
* or more links besides the hub. Plain links under the services hub count as money services without sub-services.
|
|
59
|
+
* None are inferred when the header has the one combined Services dropdown, which is not split mode.
|
|
60
|
+
*/
|
|
61
|
+
function inferMoneyServices(items, pages, closedLabels) {
|
|
62
|
+
const named = new Set(Object.values(pages).flatMap(asList).map(canonical));
|
|
63
|
+
const servicesHubs = asList(pages["services"]).map(canonical);
|
|
64
|
+
const combined = items.some((item) => servicesHubs.includes(canonical(item.href)) && item.label !== "More services");
|
|
65
|
+
if (combined)
|
|
66
|
+
return [];
|
|
67
|
+
const found = [];
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
if (closedLabels.includes(item.label) || !item.href.startsWith("/"))
|
|
70
|
+
continue;
|
|
71
|
+
const hub = canonical(item.href);
|
|
72
|
+
const underServices = servicesHubs.some((root) => hub.startsWith(`${root}/`));
|
|
73
|
+
if (item.kind === "menu" && !named.has(hub) && item.links.length >= 2) {
|
|
74
|
+
found.push({ label: item.label, href: item.href, subServices: item.links.length });
|
|
75
|
+
}
|
|
76
|
+
else if (underServices) {
|
|
77
|
+
found.push({ label: item.label, href: item.href, subServices: item.kind === "menu" ? item.links.length : 0 });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
const ALWAYS_REQUIRED = [
|
|
83
|
+
{ key: "about", labels: ["About"] },
|
|
84
|
+
{ key: "services", labels: ["Services", "More services"] },
|
|
85
|
+
{ key: "contact", labels: ["Contact"] },
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* Applies N1 to the header menu on the home page. `recorded` is the `navigation` of the site's strategy record;
|
|
89
|
+
* `runNavigationRules` reads it from `src/data/strategy.ts` for you.
|
|
90
|
+
*/
|
|
91
|
+
export async function checkRenderedNavigation(input) {
|
|
92
|
+
const home = input.pages.find((page) => canonical(page.path) === "/");
|
|
93
|
+
if (!home)
|
|
94
|
+
return [finding("fail", undefined, "N1 could not run: the home page was not supplied, and the header menu is read from it.")];
|
|
95
|
+
let core;
|
|
96
|
+
try {
|
|
97
|
+
core = await loadSiteCoreNavigation();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return [finding("fail", "/", error instanceof Error ? error.message : String(error))];
|
|
101
|
+
}
|
|
102
|
+
const extracted = extractMainNavigation(home.html, input.baseUrl);
|
|
103
|
+
if (!extracted) {
|
|
104
|
+
return [finding("fail", "/", 'The header on / has no <nav aria-label="Main">, so N1 cannot read the menu.')];
|
|
105
|
+
}
|
|
106
|
+
if (extracted.items.length === 0) {
|
|
107
|
+
return [finding("fail", "/", "The main navigation on the home page has no links in the server HTML.")];
|
|
108
|
+
}
|
|
109
|
+
// Which pages exist: the pages supplied, and every link on any of them outside its own main menu.
|
|
110
|
+
const evidence = new Set(input.pages.map((page) => canonical(page.path)));
|
|
111
|
+
for (const page of input.pages) {
|
|
112
|
+
const root = page === home ? extracted.root : parseHtml(page.html);
|
|
113
|
+
for (const path of linkedPaths(root, input.baseUrl, page === home ? extracted.nav : findMainNav(root)))
|
|
114
|
+
evidence.add(canonical(path));
|
|
115
|
+
}
|
|
116
|
+
for (const aliases of Object.values(core.BRANDLAB_NAV_PAGE_ALIASES)) {
|
|
117
|
+
for (const alias of aliases) {
|
|
118
|
+
if ([...evidence].some((path) => path.startsWith(`${alias}/`)))
|
|
119
|
+
evidence.add(alias);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const pages = core.navigationPagesFromRoutes([...evidence].map((path) => ({ path })));
|
|
123
|
+
// About, Services and Contact are always required; a hub at an unusual path is read from the menu item that names it.
|
|
124
|
+
for (const { key, labels } of ALWAYS_REQUIRED) {
|
|
125
|
+
if ((core.BRANDLAB_NAV_PAGE_ALIASES[key] ?? []).some((alias) => evidence.has(alias)))
|
|
126
|
+
continue;
|
|
127
|
+
const named = extracted.items.find((item) => labels.includes(item.label));
|
|
128
|
+
if (named?.href.startsWith("/"))
|
|
129
|
+
pages[key] = [canonical(named.href)];
|
|
130
|
+
}
|
|
131
|
+
const items = withHubs(extracted.items, pages);
|
|
132
|
+
const moneyServices = inferMoneyServices(items, pages, core.BRANDLAB_NAV_LABELS);
|
|
133
|
+
const recorded = input.recorded;
|
|
134
|
+
// Panel shapes are the library's; a header built by hand has its own. Whether a site has both a gallery and
|
|
135
|
+
// projects can't be known from the pages supplied. Whether Areas is a dropdown is only enforced where recorded.
|
|
136
|
+
const skip = ["panel", "proof-label"];
|
|
137
|
+
if (recorded?.["areas"] === undefined)
|
|
138
|
+
skip.push("areas-mode");
|
|
139
|
+
const context = {
|
|
140
|
+
pages,
|
|
141
|
+
...(moneyServices.length > 0 ? { moneyServices } : {}),
|
|
142
|
+
...(recorded ? { recorded } : {}),
|
|
143
|
+
};
|
|
144
|
+
const problems = core.validateBrandlabNavigation({ items }, context, { skip }).map((problem) => `Header menu on /: ${problem.message}`);
|
|
145
|
+
// The phone and the quote button sit in the header beside the menu; they are required links but not menu items.
|
|
146
|
+
const actions = headerActions(extracted.header);
|
|
147
|
+
if (!actions.phone)
|
|
148
|
+
problems.push("The header on / has no phone link (tel:); the phone sits beside the quote button in the header.");
|
|
149
|
+
if (!actions.quote)
|
|
150
|
+
problems.push("The header on / has no quote button; it sits beside the phone in the header.");
|
|
151
|
+
if (problems.length === 0) {
|
|
152
|
+
return [finding("pass", "/", `The header menu on / passes N1 (${items.length} items: ${items.map((item) => item.label).join(", ")}).`)];
|
|
153
|
+
}
|
|
154
|
+
return problems.map((problem) => finding("fail", "/", problem));
|
|
155
|
+
}
|
|
156
|
+
/** The N1 rendered rule, in the shape the ready gate's rendered runners share. */
|
|
157
|
+
export async function runNavigationRules(input) {
|
|
158
|
+
const { recorded, problem } = readRecordedNavigation(input.projectRoot);
|
|
159
|
+
if (problem)
|
|
160
|
+
return [finding("fail", "src/data/strategy.ts", problem)];
|
|
161
|
+
return checkRenderedNavigation({ baseUrl: input.baseUrl, pages: input.pages, ...(recorded ? { recorded } : {}) });
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * N1, the rendered side: the header menu on the home page must satisfy the same rules as `defineBrandlabNavigation`\n * (required links when their pages exist, About in the first two items, services before Areas, Contact last, labels\n * from the closed list, every dropdown holding its hub link, no prices, split mode, a recorded order kept exactly).\n * It reads the `<nav aria-label=\"Main\">` in the `<header>`, so it works on the ui-kit header and on bespoke ones.\n *\n * The rules themselves live in `@brandlab-digital/site-core/navigation` and are read at run time, like the other\n * site-core modules, so this package compiles without a site-core build and checks against the site's own copy.\n *\n * Which pages exist is read from the pages supplied and from the links on every one of them outside its own main menu\n * (a page the footer links to exists). Money-service dropdowns are read from the header itself. What Romie recorded\n * (an exact order, labels, an Areas dropdown) comes from `navigation` in `src/data/strategy.ts`.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { extractMainNavigation, headerActions, linkedPaths, findMainNav, type NavigationItemLike } from \"./extract.js\";\nimport { parseHtml } from \"./html.js\";\nimport { readRecordedNavigation } from \"./record.js\";\n\nconst NAVIGATION_MODULE: string = \"@brandlab-digital/site-core/navigation\";\n\ntype PageValue = string | readonly string[];\n\nexport interface NavigationRulesInput {\n baseUrl: string;\n pages: readonly { path: string; html: string }[];\n projectRoot: string;\n}\n\ninterface SiteCoreFinding {\n rule: string;\n message: string;\n item?: string;\n}\n\ninterface SiteCoreNavigation {\n validateBrandlabNavigation(\n nav: { items: readonly NavigationItemLike[] },\n context: Record<string, unknown>,\n options?: { skip?: readonly string[] },\n ): readonly SiteCoreFinding[];\n navigationPagesFromRoutes(routes: readonly { path: string; kind?: string }[]): Record<string, PageValue | undefined>;\n BRANDLAB_NAV_PAGE_ALIASES: Readonly<Record<string, readonly string[]>>;\n BRANDLAB_NAV_LABELS: readonly string[];\n}\n\nlet siteCoreNavigation: Promise<SiteCoreNavigation> | undefined;\n\nfunction loadSiteCoreNavigation(): Promise<SiteCoreNavigation> {\n siteCoreNavigation ??= (import(NAVIGATION_MODULE) as Promise<Partial<SiteCoreNavigation>>).then((module) => {\n if (\n typeof module.validateBrandlabNavigation !== \"function\" ||\n typeof module.navigationPagesFromRoutes !== \"function\" ||\n !module.BRANDLAB_NAV_PAGE_ALIASES ||\n !module.BRANDLAB_NAV_LABELS\n ) {\n throw new Error(\"@brandlab-digital/site-core/navigation has no validateBrandlabNavigation: install site-core 3.9.0 or later with the conformance package.\");\n }\n return module as SiteCoreNavigation;\n });\n return siteCoreNavigation;\n}\n\nfunction finding(status: SiteSystemFinding[\"status\"], target: string | undefined, message: string): SiteSystemFinding {\n return { checkId: \"N1\", status, ...(target === undefined ? {} : { target }), message };\n}\n\nfunction canonical(path: string): string {\n const [beforeHash = \"\"] = path.split(\"#\");\n const [beforeQuery = \"\"] = beforeHash.split(\"?\");\n const trimmed = beforeQuery.length > 1 ? beforeQuery.replace(/\\/+$/, \"\") : beforeQuery;\n return (trimmed || \"/\").toLowerCase();\n}\n\nfunction asList(value: PageValue | undefined): string[] {\n return value === undefined ? [] : typeof value === \"string\" ? [value] : [...value];\n}\n\n/** The page each dropdown label is the hub of, for a label that is a button rather than a link. */\nconst HUB_PAGES: Readonly<Record<string, readonly string[]>> = {\n services: [\"services\"],\n \"more services\": [\"services\"],\n areas: [\"areas\"],\n \"our work\": [\"projects\", \"gallery\"],\n projects: [\"projects\"],\n gallery: [\"gallery\"],\n resources: [\"guides\", \"faqs\"],\n guides: [\"guides\"],\n blog: [\"guides\"],\n about: [\"about\"],\n faqs: [\"faqs\"],\n contact: [\"contact\"],\n};\n\n/** A dropdown whose label is a button has no hub of its own; its hub is the panel link to the page the label names. */\nfunction withHubs(items: readonly NavigationItemLike[], pages: Record<string, PageValue | undefined>): NavigationItemLike[] {\n return items.map((item) => {\n if (item.kind !== \"menu\" || item.href !== \"\") return item;\n const wanted = (HUB_PAGES[item.label.toLowerCase()] ?? []).flatMap((key) => asList(pages[key])).map(canonical);\n const hub = item.links.find((link) => wanted.includes(canonical(link.href)));\n return hub ? { ...item, href: hub.href, hubLabel: hub.label, links: item.links.filter((link) => link !== hub) } : item;\n });\n}\n\n/**\n * The money services the header itself shows. A dropdown is a money-service dropdown when its label is not a closed\n * label, its hub is a page of its own (not the services hub or any other page the menu names) and its panel holds two\n * or more links besides the hub. Plain links under the services hub count as money services without sub-services.\n * None are inferred when the header has the one combined Services dropdown, which is not split mode.\n */\nfunction inferMoneyServices(\n items: readonly NavigationItemLike[],\n pages: Record<string, PageValue | undefined>,\n closedLabels: readonly string[],\n): { label: string; href: string; subServices: number }[] {\n const named = new Set(Object.values(pages).flatMap(asList).map(canonical));\n const servicesHubs = asList(pages[\"services\"]).map(canonical);\n const combined = items.some((item) => servicesHubs.includes(canonical(item.href)) && item.label !== \"More services\");\n if (combined) return [];\n const found: { label: string; href: string; subServices: number }[] = [];\n for (const item of items) {\n if (closedLabels.includes(item.label) || !item.href.startsWith(\"/\")) continue;\n const hub = canonical(item.href);\n const underServices = servicesHubs.some((root) => hub.startsWith(`${root}/`));\n if (item.kind === \"menu\" && !named.has(hub) && item.links.length >= 2) {\n found.push({ label: item.label, href: item.href, subServices: item.links.length });\n } else if (underServices) {\n found.push({ label: item.label, href: item.href, subServices: item.kind === \"menu\" ? item.links.length : 0 });\n }\n }\n return found;\n}\n\nconst ALWAYS_REQUIRED = [\n { key: \"about\", labels: [\"About\"] },\n { key: \"services\", labels: [\"Services\", \"More services\"] },\n { key: \"contact\", labels: [\"Contact\"] },\n] as const;\n\n/**\n * Applies N1 to the header menu on the home page. `recorded` is the `navigation` of the site's strategy record;\n * `runNavigationRules` reads it from `src/data/strategy.ts` for you.\n */\nexport async function checkRenderedNavigation(input: {\n baseUrl: string;\n pages: readonly { path: string; html: string }[];\n recorded?: Record<string, unknown>;\n}): Promise<SiteSystemFinding[]> {\n const home = input.pages.find((page) => canonical(page.path) === \"/\");\n if (!home) return [finding(\"fail\", undefined, \"N1 could not run: the home page was not supplied, and the header menu is read from it.\")];\n\n let core: SiteCoreNavigation;\n try {\n core = await loadSiteCoreNavigation();\n } catch (error) {\n return [finding(\"fail\", \"/\", error instanceof Error ? error.message : String(error))];\n }\n\n const extracted = extractMainNavigation(home.html, input.baseUrl);\n if (!extracted) {\n return [finding(\"fail\", \"/\", 'The header on / has no <nav aria-label=\"Main\">, so N1 cannot read the menu.')];\n }\n if (extracted.items.length === 0) {\n return [finding(\"fail\", \"/\", \"The main navigation on the home page has no links in the server HTML.\")];\n }\n\n // Which pages exist: the pages supplied, and every link on any of them outside its own main menu.\n const evidence = new Set<string>(input.pages.map((page) => canonical(page.path)));\n for (const page of input.pages) {\n const root = page === home ? extracted.root : parseHtml(page.html);\n for (const path of linkedPaths(root, input.baseUrl, page === home ? extracted.nav : findMainNav(root))) evidence.add(canonical(path));\n }\n for (const aliases of Object.values(core.BRANDLAB_NAV_PAGE_ALIASES)) {\n for (const alias of aliases) {\n if ([...evidence].some((path) => path.startsWith(`${alias}/`))) evidence.add(alias);\n }\n }\n const pages: Record<string, PageValue | undefined> = core.navigationPagesFromRoutes([...evidence].map((path) => ({ path })));\n // About, Services and Contact are always required; a hub at an unusual path is read from the menu item that names it.\n for (const { key, labels } of ALWAYS_REQUIRED) {\n if ((core.BRANDLAB_NAV_PAGE_ALIASES[key] ?? []).some((alias) => evidence.has(alias))) continue;\n const named = extracted.items.find((item) => (labels as readonly string[]).includes(item.label));\n if (named?.href.startsWith(\"/\")) pages[key] = [canonical(named.href)];\n }\n\n const items = withHubs(extracted.items, pages);\n const moneyServices = inferMoneyServices(items, pages, core.BRANDLAB_NAV_LABELS);\n const recorded = input.recorded;\n // Panel shapes are the library's; a header built by hand has its own. Whether a site has both a gallery and\n // projects can't be known from the pages supplied. Whether Areas is a dropdown is only enforced where recorded.\n const skip = [\"panel\", \"proof-label\"];\n if (recorded?.[\"areas\"] === undefined) skip.push(\"areas-mode\");\n const context: Record<string, unknown> = {\n pages,\n ...(moneyServices.length > 0 ? { moneyServices } : {}),\n ...(recorded ? { recorded } : {}),\n };\n\n const problems = core.validateBrandlabNavigation({ items }, context, { skip }).map((problem) => `Header menu on /: ${problem.message}`);\n // The phone and the quote button sit in the header beside the menu; they are required links but not menu items.\n const actions = headerActions(extracted.header);\n if (!actions.phone) problems.push(\"The header on / has no phone link (tel:); the phone sits beside the quote button in the header.\");\n if (!actions.quote) problems.push(\"The header on / has no quote button; it sits beside the phone in the header.\");\n if (problems.length === 0) {\n return [finding(\"pass\", \"/\", `The header menu on / passes N1 (${items.length} items: ${items.map((item) => item.label).join(\", \")}).`)];\n }\n return problems.map((problem) => finding(\"fail\", \"/\", problem));\n}\n\n/** The N1 rendered rule, in the shape the ready gate's rendered runners share. */\nexport async function runNavigationRules(input: NavigationRulesInput): Promise<readonly SiteSystemFinding[]> {\n const { recorded, problem } = readRecordedNavigation(input.projectRoot);\n if (problem) return [finding(\"fail\", \"src/data/strategy.ts\", problem)];\n return checkRenderedNavigation({ baseUrl: input.baseUrl, pages: input.pages, ...(recorded ? { recorded } : {}) });\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RecordedNavigationRead {
|
|
2
|
+
/** The recorded menu as plain data; absent when the strategy records none. */
|
|
3
|
+
recorded?: Record<string, unknown>;
|
|
4
|
+
/** Why a recorded menu could not be read. */
|
|
5
|
+
problem?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function readRecordedNavigation(projectRoot: string): RecordedNavigationRead;
|
|
8
|
+
//# sourceMappingURL=record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/record.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,sBAAsB,CAgBlF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Romie recorded for the menu: the `navigation` of `defineSiteStrategy` in `src/data/strategy.ts` (an exact
|
|
3
|
+
* order, labels outside the closed list, whether Areas is a dropdown). It is read as plain data with the same literal
|
|
4
|
+
* reader the other strategy checks use, so the site never has to run.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import { D4_STRATEGY_FILE } from "../design/d4-distinctness.js";
|
|
10
|
+
import { isFullyResolved, isLiteralObject, LiteralReader } from "../enquiry/literal.js";
|
|
11
|
+
function propertyKey(name) {
|
|
12
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function readRecordedNavigation(projectRoot) {
|
|
15
|
+
const file = join(projectRoot, D4_STRATEGY_FILE);
|
|
16
|
+
if (!existsSync(file))
|
|
17
|
+
return {};
|
|
18
|
+
const reader = new LiteralReader(readFileSync(file, "utf8"), D4_STRATEGY_FILE);
|
|
19
|
+
const argument = reader.callArguments("defineSiteStrategy")[0] ?? reader.constant("strategy");
|
|
20
|
+
// A strategy that is not a literal is reported by check D4; there is nothing for N1 to add.
|
|
21
|
+
if (!argument || !ts.isObjectLiteralExpression(argument))
|
|
22
|
+
return {};
|
|
23
|
+
for (const property of argument.properties) {
|
|
24
|
+
if (ts.isPropertyAssignment(property) && propertyKey(property.name) === "navigation") {
|
|
25
|
+
return readValue(reader.evaluate(property.initializer));
|
|
26
|
+
}
|
|
27
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === "navigation") {
|
|
28
|
+
return readValue(reader.evaluate(property.name));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
function readValue(value) {
|
|
34
|
+
if (!isLiteralObject(value) || !isFullyResolved(value)) {
|
|
35
|
+
return { problem: `${D4_STRATEGY_FILE} records a navigation that is not plain data, so N1 cannot read it; write it as a literal.` };
|
|
36
|
+
}
|
|
37
|
+
return { recorded: value };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/record.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AASxF,SAAS,WAAW,CAAC,IAAqB;IACxC,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,WAAmB;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9F,4FAA4F;IAC5F,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC,yBAAyB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACpE,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3C,IAAI,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,YAAY,EAAE,CAAC;YACrF,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,EAAE,CAAC,6BAA6B,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACtF,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,SAAS,CAAC,KAA4C;IAC7D,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,OAAO,EAAE,GAAG,gBAAgB,4FAA4F,EAAE,CAAC;IACtI,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,KAAgC,EAAE,CAAC;AACxD,CAAC","sourcesContent":["/**\n * What Romie recorded for the menu: the `navigation` of `defineSiteStrategy` in `src/data/strategy.ts` (an exact\n * order, labels outside the closed list, whether Areas is a dropdown). It is read as plain data with the same literal\n * reader the other strategy checks use, so the site never has to run.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport ts from \"typescript\";\nimport { D4_STRATEGY_FILE } from \"../design/d4-distinctness.js\";\nimport { isFullyResolved, isLiteralObject, LiteralReader } from \"../enquiry/literal.js\";\n\nexport interface RecordedNavigationRead {\n /** The recorded menu as plain data; absent when the strategy records none. */\n recorded?: Record<string, unknown>;\n /** Why a recorded menu could not be read. */\n problem?: string;\n}\n\nfunction propertyKey(name: ts.PropertyName): string | undefined {\n return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : undefined;\n}\n\nexport function readRecordedNavigation(projectRoot: string): RecordedNavigationRead {\n const file = join(projectRoot, D4_STRATEGY_FILE);\n if (!existsSync(file)) return {};\n const reader = new LiteralReader(readFileSync(file, \"utf8\"), D4_STRATEGY_FILE);\n const argument = reader.callArguments(\"defineSiteStrategy\")[0] ?? reader.constant(\"strategy\");\n // A strategy that is not a literal is reported by check D4; there is nothing for N1 to add.\n if (!argument || !ts.isObjectLiteralExpression(argument)) return {};\n for (const property of argument.properties) {\n if (ts.isPropertyAssignment(property) && propertyKey(property.name) === \"navigation\") {\n return readValue(reader.evaluate(property.initializer));\n }\n if (ts.isShorthandPropertyAssignment(property) && property.name.text === \"navigation\") {\n return readValue(reader.evaluate(property.name));\n }\n }\n return {};\n}\n\nfunction readValue(value: ReturnType<LiteralReader[\"evaluate\"]>): RecordedNavigationRead {\n if (!isLiteralObject(value) || !isFullyResolved(value)) {\n return { problem: `${D4_STRATEGY_FILE} records a navigation that is not plain data, so N1 cannot read it; write it as a literal.` };\n }\n return { recorded: value as Record<string, unknown> };\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads location metadata out of an image file without a dependency (T9). A photo straight off a phone carries the GPS
|
|
3
|
+
* position of the job site, and often the street and suburb; none of it may ship in `public/`.
|
|
4
|
+
*
|
|
5
|
+
* What counts as location metadata:
|
|
6
|
+
* - an EXIF GPS block with any tag beyond the version (latitude, longitude, altitude, a timestamp, a processing method);
|
|
7
|
+
* - XMP `exif:GPS*` properties and `photoshop:City`, `photoshop:State` and `Iptc4xmpCore:Location` (sublocation);
|
|
8
|
+
* - IPTC-IIM city (2:90), sublocation (2:92) and province or state (2:95) records.
|
|
9
|
+
*
|
|
10
|
+
* Formats: JPEG (APP1 EXIF and XMP, APP13 IPTC), PNG (`eXIf`, and `iTXt`/`tEXt` XMP), WebP (`EXIF` and `XMP ` chunks)
|
|
11
|
+
* and HEIC/HEIF/AVIF (the `Exif` item is found by its TIFF header, and XMP by its packet; this is a scan, not a full
|
|
12
|
+
* ISO box parser). A file in none of those formats, or one the reader cannot walk, reports nothing.
|
|
13
|
+
*/
|
|
14
|
+
export interface LocationMetadata {
|
|
15
|
+
/** Short names of what was found, for example "EXIF GPS (latitude, longitude)". */
|
|
16
|
+
readonly found: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/** Reads a TIFF block (EXIF) and returns the names of the GPS tags it carries. */
|
|
19
|
+
export declare function gpsTagsInTiff(bytes: Uint8Array, start: number): string[];
|
|
20
|
+
/** The location metadata an image file carries; an empty `found` for a clean file or a format this reader skips. */
|
|
21
|
+
export declare function locationMetadata(bytes: Uint8Array): LocationMetadata;
|
|
22
|
+
//# sourceMappingURL=exif.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exif.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/exif.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAiBD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAuCxE;AA4HD,oHAAoH;AACpH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAUpE"}
|