@brandlab-digital/conformance 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/current-release.js +5 -5
  2. package/dist/current-release.js.map +1 -1
  3. package/dist/engineering.js +4 -2
  4. package/dist/engineering.js.map +1 -1
  5. package/dist/site-system/checks.d.ts +1 -1
  6. package/dist/site-system/checks.js +1 -1
  7. package/dist/site-system/checks.js.map +1 -1
  8. package/dist/site-system/rule-modules.d.ts.map +1 -1
  9. package/dist/site-system/rule-modules.js +22 -1
  10. package/dist/site-system/rule-modules.js.map +1 -1
  11. package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
  12. package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
  13. package/dist/site-system/rules/form/c11-contact-route.js +132 -0
  14. package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
  15. package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
  16. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
  17. package/dist/site-system/rules/form/c7-thank-you.js +67 -0
  18. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
  19. package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
  20. package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
  21. package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
  22. package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
  23. package/dist/site-system/rules/form/index.d.ts +14 -0
  24. package/dist/site-system/rules/form/index.d.ts.map +1 -0
  25. package/dist/site-system/rules/form/index.js +13 -0
  26. package/dist/site-system/rules/form/index.js.map +1 -0
  27. package/dist/site-system/rules/library/common.d.ts +36 -0
  28. package/dist/site-system/rules/library/common.d.ts.map +1 -0
  29. package/dist/site-system/rules/library/common.js +70 -0
  30. package/dist/site-system/rules/library/common.js.map +1 -0
  31. package/dist/site-system/rules/library/index.d.ts +16 -0
  32. package/dist/site-system/rules/library/index.d.ts.map +1 -0
  33. package/dist/site-system/rules/library/index.js +19 -0
  34. package/dist/site-system/rules/library/index.js.map +1 -0
  35. package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
  36. package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
  37. package/dist/site-system/rules/library/k1-shared-components.js +97 -0
  38. package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
  39. package/dist/site-system/rules/navigation/extract.d.ts +50 -0
  40. package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
  41. package/dist/site-system/rules/navigation/extract.js +220 -0
  42. package/dist/site-system/rules/navigation/extract.js.map +1 -0
  43. package/dist/site-system/rules/navigation/html.d.ts +23 -0
  44. package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
  45. package/dist/site-system/rules/navigation/html.js +219 -0
  46. package/dist/site-system/rules/navigation/html.js.map +1 -0
  47. package/dist/site-system/rules/navigation/index.d.ts +37 -0
  48. package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
  49. package/dist/site-system/rules/navigation/index.js +163 -0
  50. package/dist/site-system/rules/navigation/index.js.map +1 -0
  51. package/dist/site-system/rules/navigation/record.d.ts +8 -0
  52. package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
  53. package/dist/site-system/rules/navigation/record.js +39 -0
  54. package/dist/site-system/rules/navigation/record.js.map +1 -0
  55. package/dist/site-system/rules/sections/exif.d.ts +22 -0
  56. package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
  57. package/dist/site-system/rules/sections/exif.js +218 -0
  58. package/dist/site-system/rules/sections/exif.js.map +1 -0
  59. package/dist/site-system/rules/sections/images.d.ts +31 -0
  60. package/dist/site-system/rules/sections/images.d.ts.map +1 -0
  61. package/dist/site-system/rules/sections/images.js +96 -0
  62. package/dist/site-system/rules/sections/images.js.map +1 -0
  63. package/dist/site-system/rules/sections/index.d.ts +20 -0
  64. package/dist/site-system/rules/sections/index.d.ts.map +1 -0
  65. package/dist/site-system/rules/sections/index.js +34 -0
  66. package/dist/site-system/rules/sections/index.js.map +1 -0
  67. package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
  68. package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
  69. package/dist/site-system/rules/sections/k11-density.js +88 -0
  70. package/dist/site-system/rules/sections/k11-density.js.map +1 -0
  71. package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
  72. package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
  73. package/dist/site-system/rules/sections/k12-contact.js +70 -0
  74. package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
  75. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
  76. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
  77. package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
  78. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
  79. package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
  80. package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
  81. package/dist/site-system/rules/sections/n9-footer.js +186 -0
  82. package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
  83. package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
  84. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
  85. package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
  86. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
  87. package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
  88. package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
  89. package/dist/site-system/rules/sections/t7-main-images.js +41 -0
  90. package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
  91. package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
  92. package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
  93. package/dist/site-system/rules/sections/t8-prices.js +92 -0
  94. package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
  95. package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
  96. package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
  97. package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
  98. package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
  99. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/extract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the main navigation out of rendered HTML: the `<nav aria-label=\"Main\">` inside the `<header>`, its top-level\n * items in order, and for each dropdown the links of its panel. It reads structure, not class names, so it works on\n * the ui-kit header and on a header a site built itself: a top-level item is a list entry (or, with no list, a child\n * of the nav); its label link is the first link outside the panel; a dropdown is an entry with a disclosure button\n * or a panel (the element the button controls, a nested list, or an element marked as a panel).\n */\nimport { descendants, elementChildren, hasAncestor, isInside, parseHtml, textOf, type HtmlElement } from \"./html.js\";\n\nexport interface NavigationLinkLike {\n label: string;\n href: string;\n}\n\nexport interface NavigationLinkItemLike {\n kind: \"link\";\n id: string;\n label: string;\n href: string;\n}\n\nexport interface NavigationMenuItemLike {\n kind: \"menu\";\n id: string;\n label: string;\n href: string;\n hubLabel: string;\n panel: \"list\";\n links: NavigationLinkLike[];\n}\n\nexport type NavigationItemLike = NavigationLinkItemLike | NavigationMenuItemLike;\n\nexport interface ExtractedNavigation {\n readonly root: HtmlElement;\n readonly nav: HtmlElement;\n /** The header that holds the nav, or the whole document when the nav stands on its own. */\n readonly header: HtmlElement;\n readonly items: NavigationItemLike[];\n}\n\nexport function slugOf(text: string): string {\n return text.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/(^-|-$)/g, \"\");\n}\n\nfunction isNavElement(element: HtmlElement): boolean {\n return element.tag === \"nav\" || element.attrs.role === \"navigation\";\n}\n\n/** The main navigation: the first `<nav aria-label=\"Main\">` inside the `<header>`, outside any dialog or drawer. */\nexport function findMainNav(root: HtmlElement): HtmlElement | undefined {\n const all = descendants(root);\n const headers = all.filter((element) => element.tag === \"header\" || element.attrs.role === \"banner\");\n const inDialog = (element: HtmlElement) => hasAncestor(element, (ancestor) => ancestor.tag === \"dialog\" || ancestor.attrs[\"aria-modal\"] === \"true\");\n return all.find(\n (element) =>\n isNavElement(element) &&\n (element.attrs[\"aria-label\"] ?? \"\").trim() === \"Main\" &&\n headers.some((header) => isInside(element, header)) &&\n !inDialog(element),\n );\n}\n\nfunction sameSite(left: string, right: string): boolean {\n return left.replace(/^www\\./i, \"\").toLowerCase() === right.replace(/^www\\./i, \"\").toLowerCase();\n}\n\n/** A link as the menu sees it: a same-site path without query or hash, an absolute URL for other sites, or \"\" for no page. */\nexport function sitePath(href: string | undefined, baseUrl: string): string {\n const raw = (href ?? \"\").trim();\n if (raw === \"\" || raw.startsWith(\"#\") || /^javascript:/i.test(raw)) return \"\";\n if (/^(tel|mailto|sms|whatsapp):/i.test(raw)) return raw;\n try {\n const base = new URL(baseUrl || \"https://site.invalid\");\n const url = new URL(raw, base);\n return sameSite(url.hostname, base.hostname) ? url.pathname : url.href;\n } catch {\n return raw;\n }\n}\n\n/** A `<summary>` always opens something; a button does when it says so. */\nfunction isDisclosure(element: HtmlElement): boolean {\n if (element.tag === \"summary\") return true;\n const control = element.tag === \"button\" || element.attrs.role === \"button\";\n return control && (\"aria-expanded\" in element.attrs || \"aria-haspopup\" in element.attrs || \"aria-controls\" in element.attrs);\n}\n\n/** A button that opens the phone drawer or toggles the whole menu is not a dropdown of this menu. */\nconst MENU_BUTTON_TEXT = /^(open |close |toggle |show |hide )?(the )?(main |site |mobile )?(menu|navigation|nav)( menu)?$/i;\n\nconst PANEL_TAGS = new Set([\"div\", \"ul\", \"ol\", \"nav\", \"section\", \"aside\"]);\nconst LABELISH_TAGS = new Set([\"a\", \"button\", \"span\", \"summary\"]);\n\nfunction previousElement(element: HtmlElement): HtmlElement | undefined {\n const siblings = element.parent ? elementChildren(element.parent) : [];\n return siblings[siblings.indexOf(element) - 1];\n}\n\n/**\n * The panels inside a nav: what a disclosure button controls, anything marked as a panel, and a block that follows\n * a labelled link, button or span inside an item and holds links (`<a>Services</a><div>...links...</div>`).\n */\nfunction findPanels(nav: HtmlElement): Set<HtmlElement> {\n const all = descendants(nav);\n const panels = new Set<HtmlElement>();\n for (const toggle of all.filter(isDisclosure)) {\n const id = toggle.attrs[\"aria-controls\"];\n const target = id ? all.find((element) => element.attrs.id === id) : undefined;\n if (target) panels.add(target);\n }\n for (const element of all) {\n if (element.attrs[\"data-slot\"] === \"panel\" || element.attrs.role === \"menu\") panels.add(element);\n }\n for (const element of all) {\n if (!PANEL_TAGS.has(element.tag) || !element.parent || element.parent === nav) continue;\n const before = previousElement(element);\n if (!before || !LABELISH_TAGS.has(before.tag)) continue;\n const beforeText = cleanLabel(textOf(before));\n if (beforeText === \"\" || MENU_BUTTON_TEXT.test(beforeText)) continue;\n if (descendants(element).some((entry) => entry.tag === \"a\" && \"href\" in entry.attrs)) panels.add(element);\n }\n return panels;\n}\n\n/** Entries that are actions, not menu items: the phone, the email, the quote button. */\nfunction isAction(entry: HtmlElement, label: string, href: string): boolean {\n if (/^(tel|mailto|sms|whatsapp):/i.test(href)) return true;\n const marked = [entry, ...descendants(entry)].some(\n (element) => element.attrs[\"data-bl\"] === \"quote-button\" || \"data-bl-cta\" in element.attrs || \"data-lead-cta\" in element.attrs || \"data-phone-location\" in element.attrs,\n );\n if (marked) return true;\n return /\\bquote\\b/i.test(label) || /^(call|phone|ring)\\b/i.test(label) || /^\\+?[\\d\\s()-]{8,}$/.test(label) || /^send photos?$/i.test(label);\n}\n\n/** Drops the chevron glyphs a bespoke header may put in the label text. */\nfunction cleanLabel(text: string): string {\n return text.replace(/[\\u25bc\\u25be\\u25bf\\u2304\\u02c5\\u2193\\u25b8\\u203a]+$/u, \"\").replace(/\\s+/g, \" \").trim();\n}\n\n/** A panel's closing link to the whole group: \"All services\", \"View all decking\". */\nconst HUB_LINK_TEXT = /^(view |see |browse )?all\\b/i;\n\n/**\n * The hub of a dropdown whose label has no page, from its panel: the first link that is the parent path of the\n * others, else the first link worded as the whole group (\"All gardening services\").\n */\nfunction guessHub(links: readonly NavigationLinkLike[]): NavigationLinkLike | undefined {\n return (\n links.find((link) => link.href.startsWith(\"/\") && links.some((other) => other !== link && other.href.startsWith(`${link.href.replace(/\\/+$/, \"\")}/`))) ??\n links.find((link) => link.href.startsWith(\"/\") && HUB_LINK_TEXT.test(link.label))\n );\n}\n\ninterface NavScan {\n readonly nav: HtmlElement;\n readonly panels: Set<HtmlElement>;\n readonly inPanel: (element: HtmlElement) => boolean;\n}\n\n/**\n * The top-level entries, in order. Each entry has exactly one label: a link with text, or a button with text that\n * opens a dropdown, outside any panel. An entry is the widest element that holds that one label and no other.\n */\nfunction topLevelEntries(scan: NavScan): { entry: HtmlElement; label: HtmlElement }[] {\n const { nav, inPanel } = scan;\n const labels = descendants(nav).filter((element) => {\n if (inPanel(element)) return false;\n const text = cleanLabel(textOf(element));\n if (text === \"\") return false;\n if (element.tag === \"a\") return \"href\" in element.attrs;\n // A chevron drawn as text (v, +) is not a label; a button needs a word, and the menu button is not a dropdown.\n return isDisclosure(element) && /[a-z]{2,}/i.test(text) && !MENU_BUTTON_TEXT.test(text);\n });\n const within = (container: HtmlElement) => labels.filter((label) => label === container || isInside(label, container)).length;\n return labels.map((label) => {\n let entry = label;\n while (entry.parent && entry.parent !== nav && within(entry.parent) === 1) entry = entry.parent;\n return { entry, label };\n });\n}\n\nfunction readEntry(scan: NavScan, entry: HtmlElement, label: HtmlElement, baseUrl: string): NavigationItemLike | undefined {\n if (\"hidden\" in entry.attrs || entry.attrs[\"aria-hidden\"] === \"true\") return undefined;\n const { nav, panels, inPanel } = scan;\n const inside = [entry, ...descendants(entry)];\n const toggles = inside.filter((element) => isDisclosure(element) && !inPanel(element));\n const controlled = toggles\n .map((toggle) => toggle.attrs[\"aria-controls\"])\n .map((id) => (id ? descendants(nav).find((element) => element.attrs.id === id) : undefined))\n .find((target): target is HtmlElement => target !== undefined && panels.has(target));\n const panel = controlled ?? inside.find((element) => panels.has(element) && !hasAncestor(element, (ancestor) => panels.has(ancestor)));\n const panelAnchors = panel ? [panel, ...descendants(panel)].filter((element) => element.tag === \"a\" && \"href\" in element.attrs) : [];\n const labelIsAnchor = label.tag === \"a\";\n const labelHref = labelIsAnchor ? sitePath(label.attrs.href, baseUrl) : \"\";\n const text = cleanLabel(textOf(label));\n if (text === \"\" || isAction(entry, text, labelHref)) return undefined;\n const opens = !labelIsAnchor || \"aria-haspopup\" in label.attrs || \"aria-expanded\" in label.attrs || toggles.some((toggle) => toggle !== label);\n if (panelAnchors.length === 0 && !opens) return { kind: \"link\", id: slugOf(text), label: text, href: labelHref };\n\n const panelLinks = panelAnchors\n .map((anchor) => ({ label: cleanLabel(textOf(anchor)), href: sitePath(anchor.attrs.href, baseUrl) }))\n .filter((link) => link.href !== \"\");\n const hub = labelHref !== \"\" ? labelHref : guessHub(panelLinks)?.href ?? \"\";\n const hubKey = hub.replace(/\\/+$/, \"\").toLowerCase();\n const hubLink = hub === \"\" ? undefined : panelLinks.find((link) => link.href.replace(/\\/+$/, \"\").toLowerCase() === hubKey);\n return {\n kind: \"menu\",\n id: slugOf(text),\n label: text,\n href: hub,\n hubLabel: hubLink?.label ?? \"\",\n panel: \"list\",\n links: panelLinks.filter((link) => link !== hubLink && link.href.replace(/\\/+$/, \"\").toLowerCase() !== hubKey),\n };\n}\n\n/** The main navigation of a document: its element and its top-level items in order. Undefined when the page has none. */\nexport function extractMainNavigation(html: string, baseUrl: string): ExtractedNavigation | undefined {\n const root = parseHtml(html);\n const nav = findMainNav(root);\n if (!nav) return undefined;\n const panels = findPanels(nav);\n const scan: NavScan = { nav, panels, inPanel: (element) => panels.has(element) || hasAncestor(element, (ancestor) => panels.has(ancestor)) };\n const items = topLevelEntries(scan)\n .map(({ entry, label }) => readEntry(scan, entry, label, baseUrl))\n .filter((item): item is NavigationItemLike => item !== undefined);\n let header: HtmlElement = root;\n for (let node = nav.parent; node; node = node.parent) {\n if (node.tag === \"header\" || node.attrs.role === \"banner\") header = node;\n }\n return { root, nav, header, items };\n}\n\n/** Whether the header holds a phone link and a quote button, outside any drawer or dialog. */\nexport function headerActions(header: HtmlElement): { phone: boolean; quote: boolean } {\n const live = descendants(header).filter((element) => !hasAncestor(element, (ancestor) => ancestor.tag === \"dialog\" || ancestor.attrs[\"aria-modal\"] === \"true\"));\n const phone = live.some((element) => element.tag === \"a\" && /^tel:/i.test((element.attrs.href ?? \"\").trim()));\n const quote = live.some((element) => {\n if (element.attrs[\"data-bl\"] === \"quote-button\" || \"data-bl-cta\" in element.attrs || \"data-lead-cta\" in element.attrs) return true;\n if (element.tag !== \"a\" && element.tag !== \"button\") return false;\n const text = textOf(element);\n return /\\bquote\\b/i.test(text) || /^send photos?$/i.test(text);\n });\n return { phone, quote };\n}\n\n/** Every link on a page outside the given element, as same-site paths: the evidence of which pages exist. */\nexport function linkedPaths(root: HtmlElement, baseUrl: string, except?: HtmlElement): string[] {\n const paths = new Set<string>();\n for (const element of descendants(root)) {\n if (element.tag !== \"a\" || !(\"href\" in element.attrs)) continue;\n if (except && (element === except || isInside(element, except))) continue;\n const path = sitePath(element.attrs.href, baseUrl);\n if (path.startsWith(\"/\")) paths.add(path);\n }\n return [...paths];\n}\n"]}
@@ -0,0 +1,23 @@
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
+ export interface HtmlElement {
7
+ readonly tag: string;
8
+ readonly attrs: Readonly<Record<string, string>>;
9
+ readonly children: HtmlChild[];
10
+ readonly parent: HtmlElement | null;
11
+ }
12
+ export type HtmlChild = HtmlElement | string;
13
+ export declare function decodeEntities(value: string): string;
14
+ /** Builds the element tree for a document or fragment. The returned root is a `#document` element. */
15
+ export declare function parseHtml(html: string): HtmlElement;
16
+ export declare function elementChildren(element: HtmlElement): HtmlElement[];
17
+ /** Every descendant element in document order. */
18
+ export declare function descendants(element: HtmlElement): HtmlElement[];
19
+ export declare function isInside(element: HtmlElement, ancestor: HtmlElement): boolean;
20
+ export declare function hasAncestor(element: HtmlElement, test: (ancestor: HtmlElement) => boolean): boolean;
21
+ /** The visible text of an element: no SVG, no screen-reader-only text, no hidden or aria-hidden parts. */
22
+ export declare function textOf(element: HtmlElement): string;
23
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAwB7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AA8DD,sGAAsG;AACtG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAgEnD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAEnE;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAW/D;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAK7E;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,OAAO,GAAG,OAAO,CAKnG;AAID,0GAA0G;AAC1G,wBAAgB,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAoBnD"}
@@ -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":"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","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"}