From 50251013e1b6ea6bfb48c4fa6cdb017b3f51fcb4 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 1 Jul 2026 11:45:24 +0300 Subject: [PATCH 1/4] fix: resolve guideline dependency links --- .../ipa/Guideline/Guideline.module.css | 1 + .../ipa/Guideline/Guideline.test.tsx | 1 + .../ipa/Guideline/GuidelineFooter.tsx | 33 +++++++++++++------ src/components/ipa/Guideline/index.tsx | 3 ++ vitest.config.ts | 3 ++ vitest.setup.ts | 14 ++++++++ 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/components/ipa/Guideline/Guideline.module.css b/src/components/ipa/Guideline/Guideline.module.css index 228845c..60b37d3 100644 --- a/src/components/ipa/Guideline/Guideline.module.css +++ b/src/components/ipa/Guideline/Guideline.module.css @@ -2,6 +2,7 @@ display: flex; align-items: flex-start; gap: 0.75rem; + scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); } .numbered { diff --git a/src/components/ipa/Guideline/Guideline.test.tsx b/src/components/ipa/Guideline/Guideline.test.tsx index e216ade..e1aba41 100644 --- a/src/components/ipa/Guideline/Guideline.test.tsx +++ b/src/components/ipa/Guideline/Guideline.test.tsx @@ -21,6 +21,7 @@ describe(" standalone", () => { const guideline = document.querySelector("[data-guideline-id]"); expect(guideline?.tagName).toBe("DIV"); + expect(guideline).toHaveAttribute("id", "IPA-001-must-test-a"); expect( document.querySelector("[data-guideline-id] [aria-hidden='true']"), ).toBeNull(); diff --git a/src/components/ipa/Guideline/GuidelineFooter.tsx b/src/components/ipa/Guideline/GuidelineFooter.tsx index da34c15..42aa123 100644 --- a/src/components/ipa/Guideline/GuidelineFooter.tsx +++ b/src/components/ipa/Guideline/GuidelineFooter.tsx @@ -1,12 +1,13 @@ import type { ReactElement } from "react"; +import Link from "@docusaurus/Link"; import { useGuideline } from "../../../hooks/useGuideline"; import { usePrinciple } from "../../../hooks/usePrinciple"; import styles from "./GuidelineFooter.module.css"; -// IPA-107-must-use-http-patch → "Use HTTP Patch" +// IPA-107-must-use-http-patch → "Must Use HTTP Patch" function slugToTitle(id: string): string { - const slug = id.replace(/^IPA-\d{3}-(must|should|may)-/, ""); - return slug + const slug = id.split("-").slice(2).join("-"); + return (slug || id) .split("-") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); @@ -14,22 +15,34 @@ function slugToTitle(id: string): string { // IPA-107-must-use-http-patch → 107 function ipaNumber(id: string): number | null { - const match = id.match(/^IPA-(\d{3})/); - return match ? parseInt(match[1], 10) : null; + const [prefix, number] = id.split("-"); + if (prefix !== "IPA") return null; + + const parsed = Number(number); + return Number.isInteger(parsed) ? parsed : null; +} + +function hasGuidelineAnchor(id: string): boolean { + return id.split("-").length > 2; } -// "IPA-107-must-use-http-patch" → "IPA-107: Use HTTP Patch" +// "IPA-107-must-use-http-patch" → "IPA 107: Must Use HTTP Patch" function depLabel(depId: string): string { + if (!hasGuidelineAnchor(depId)) return depId; + const num = ipaNumber(depId); const title = slugToTitle(depId); - return num !== null ? `IPA-${num}: ${title}` : title; + return num !== null ? `IPA ${num}: ${title}` : title; } // Cross-IPA: /107#IPA-107-must-use-http-patch // Same-IPA: #IPA-107-must-use-http-patch function depHref(depId: string, currentIpa: number): string { const depIpa = ipaNumber(depId); + const hasAnchor = hasGuidelineAnchor(depId); const anchor = `#${depId}`; + if (!hasAnchor) return depIpa !== null ? `/${depIpa}` : anchor; + return depIpa !== null && depIpa !== currentIpa ? `/${depIpa}${anchor}` : anchor; @@ -46,13 +59,13 @@ export function GuidelineFooter(): ReactElement | null { Depends on
{guideline.dependsOn.map((depId) => ( - {depLabel(depId)} - + ))}
diff --git a/src/components/ipa/Guideline/index.tsx b/src/components/ipa/Guideline/index.tsx index 003a70d..91e2ceb 100644 --- a/src/components/ipa/Guideline/index.tsx +++ b/src/components/ipa/Guideline/index.tsx @@ -1,4 +1,5 @@ import { type ReactNode, type ReactElement } from "react"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; import clsx from "clsx"; import type { Guideline as GuidelineData } from "../../../types/guideline"; import { GuidelineContext } from "../../../hooks/useGuideline"; @@ -18,12 +19,14 @@ function GuidelineBase({ ...guideline }: GuidelineProps): ReactElement { const isInsideGuidelines = useIsInsideGuidelines(); + useBrokenLinks().collectAnchor(guideline.id); const Root = isInsideGuidelines ? "li" : "div"; return ( {isInsideGuidelines && } diff --git a/vitest.config.ts b/vitest.config.ts index 286efcc..c59bb38 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,6 +12,9 @@ export default defineConfig({ // Docusaurus aliases @site to the repo root; tests must match. alias: { "@site": fileURLToPath(new URL(".", import.meta.url)), + "@docusaurus/Link": "@docusaurus/core/lib/client/exports/Link", + "@docusaurus/useBrokenLinks": + "@docusaurus/core/lib/client/exports/useBrokenLinks", "@docusaurus/useDocusaurusContext": "@docusaurus/core/lib/client/exports/useDocusaurusContext", }, diff --git a/vitest.setup.ts b/vitest.setup.ts index 7f12baf..92d6b13 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -7,3 +7,17 @@ // `@docusaurus/*` and need stubs at unit-test time. import "@testing-library/jest-dom/vitest"; +import { createElement } from "react"; +import { vi } from "vitest"; + +vi.mock("@docusaurus/Link", () => ({ + default: ({ to, href, children, ...props }: any) => + createElement("a", { href: to ?? href, ...props }, children), +})); + +vi.mock("@docusaurus/useBrokenLinks", () => ({ + default: () => ({ + collectAnchor: vi.fn(), + collectLink: vi.fn(), + }), +})); From 2396ed95a8271a3a6d8f3048ce386912bfa32436 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 1 Jul 2026 17:55:23 +0300 Subject: [PATCH 2/4] fix: address code review findings in dependency link helpers - ipaNumber: guard against Number('') === 0 with `&& parsed > 0` - depHref: apply same-page check in the bare-IPA branch to avoid unnecessary full-page navigation when depIpa === currentIpa - depLabel: format bare IPA-NNN refs as "IPA 107" (space) to match the style of full guideline labels - slugToTitle: remove dead `|| id` fallback (unreachable since depLabel guards with hasGuidelineAnchor before calling) - vitest.setup.ts: return singleton from useBrokenLinks mock so collectAnchor is the same vi.fn() instance across calls - Guideline.test.tsx: add test verifying collectAnchor registration --- src/components/ipa/Guideline/Guideline.test.tsx | 13 ++++++++++++- src/components/ipa/Guideline/GuidelineFooter.tsx | 12 +++++++----- vitest.setup.ts | 6 ++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/ipa/Guideline/Guideline.test.tsx b/src/components/ipa/Guideline/Guideline.test.tsx index e1aba41..2828260 100644 --- a/src/components/ipa/Guideline/Guideline.test.tsx +++ b/src/components/ipa/Guideline/Guideline.test.tsx @@ -1,5 +1,6 @@ import { render } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; import { Guideline } from "./index"; import type { Guideline as GuidelineData } from "../../../types/guideline"; @@ -16,6 +17,10 @@ const minimalGuideline = { } satisfies GuidelineData; describe(" standalone", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("renders as a standalone block without a numbering circle", () => { render(content); const guideline = document.querySelector("[data-guideline-id]"); @@ -26,4 +31,10 @@ describe(" standalone", () => { document.querySelector("[data-guideline-id] [aria-hidden='true']"), ).toBeNull(); }); + + it("registers the guideline anchor with the Docusaurus broken-link checker", () => { + const { collectAnchor } = vi.mocked(useBrokenLinks)(); + render(content); + expect(collectAnchor).toHaveBeenCalledWith("IPA-001-must-test-a"); + }); }); diff --git a/src/components/ipa/Guideline/GuidelineFooter.tsx b/src/components/ipa/Guideline/GuidelineFooter.tsx index 42aa123..6248f1c 100644 --- a/src/components/ipa/Guideline/GuidelineFooter.tsx +++ b/src/components/ipa/Guideline/GuidelineFooter.tsx @@ -7,7 +7,7 @@ import styles from "./GuidelineFooter.module.css"; // IPA-107-must-use-http-patch → "Must Use HTTP Patch" function slugToTitle(id: string): string { const slug = id.split("-").slice(2).join("-"); - return (slug || id) + return slug .split("-") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); @@ -19,7 +19,7 @@ function ipaNumber(id: string): number | null { if (prefix !== "IPA") return null; const parsed = Number(number); - return Number.isInteger(parsed) ? parsed : null; + return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } function hasGuidelineAnchor(id: string): boolean { @@ -27,10 +27,11 @@ function hasGuidelineAnchor(id: string): boolean { } // "IPA-107-must-use-http-patch" → "IPA 107: Must Use HTTP Patch" +// "IPA-107" → "IPA 107" function depLabel(depId: string): string { - if (!hasGuidelineAnchor(depId)) return depId; - const num = ipaNumber(depId); + if (!hasGuidelineAnchor(depId)) return num !== null ? `IPA ${num}` : depId; + const title = slugToTitle(depId); return num !== null ? `IPA ${num}: ${title}` : title; } @@ -41,7 +42,8 @@ function depHref(depId: string, currentIpa: number): string { const depIpa = ipaNumber(depId); const hasAnchor = hasGuidelineAnchor(depId); const anchor = `#${depId}`; - if (!hasAnchor) return depIpa !== null ? `/${depIpa}` : anchor; + if (!hasAnchor) + return depIpa !== null && depIpa !== currentIpa ? `/${depIpa}` : anchor; return depIpa !== null && depIpa !== currentIpa ? `/${depIpa}${anchor}` diff --git a/vitest.setup.ts b/vitest.setup.ts index 92d6b13..3ab88a4 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -15,9 +15,7 @@ vi.mock("@docusaurus/Link", () => ({ createElement("a", { href: to ?? href, ...props }, children), })); +const _brokenLinksMock = { collectAnchor: vi.fn(), collectLink: vi.fn() }; vi.mock("@docusaurus/useBrokenLinks", () => ({ - default: () => ({ - collectAnchor: vi.fn(), - collectLink: vi.fn(), - }), + default: vi.fn(() => _brokenLinksMock), })); From c43d90cd646bdc9f20e286a80dbfe8de3a2b9a86 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 1 Jul 2026 22:18:02 +0300 Subject: [PATCH 3/4] refactor: rename depHref to dependencyHref --- src/components/ipa/Guideline/GuidelineFooter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ipa/Guideline/GuidelineFooter.tsx b/src/components/ipa/Guideline/GuidelineFooter.tsx index 6248f1c..23cc80a 100644 --- a/src/components/ipa/Guideline/GuidelineFooter.tsx +++ b/src/components/ipa/Guideline/GuidelineFooter.tsx @@ -38,7 +38,7 @@ function depLabel(depId: string): string { // Cross-IPA: /107#IPA-107-must-use-http-patch // Same-IPA: #IPA-107-must-use-http-patch -function depHref(depId: string, currentIpa: number): string { +function dependencyHref(depId: string, currentIpa: number): string { const depIpa = ipaNumber(depId); const hasAnchor = hasGuidelineAnchor(depId); const anchor = `#${depId}`; @@ -63,7 +63,7 @@ export function GuidelineFooter(): ReactElement | null { {guideline.dependsOn.map((depId) => ( {depLabel(depId)} From e07883f222282bed3f6021829b2b895d7afece84 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Thu, 2 Jul 2026 17:40:08 +0300 Subject: [PATCH 4/4] style: Highlight targeted guidelines --- .../ipa/Guideline/Guideline.module.css | 24 +++++++++++++++++++ .../ipa/Guidelines/Guidelines.module.css | 7 +++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/components/ipa/Guideline/Guideline.module.css b/src/components/ipa/Guideline/Guideline.module.css index 60b37d3..d54729f 100644 --- a/src/components/ipa/Guideline/Guideline.module.css +++ b/src/components/ipa/Guideline/Guideline.module.css @@ -5,6 +5,10 @@ scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); } +.root:target { + animation: guidelineTargetFlash 1.4s ease-out; +} + .numbered { counter-increment: number-circle; } @@ -18,3 +22,23 @@ font-size: 1rem; line-height: 1.625; } + +@keyframes guidelineTargetFlash { + 0% { + background-color: var(--ifm-color-primary-lightest); + box-shadow: 0 0 0 0.25rem var(--ifm-color-primary-lightest); + } + + 100% { + background-color: transparent; + box-shadow: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .root:target { + animation: none; + outline: 2px solid var(--ifm-color-primary); + outline-offset: 0.25rem; + } +} diff --git a/src/components/ipa/Guidelines/Guidelines.module.css b/src/components/ipa/Guidelines/Guidelines.module.css index f35e107..8ef6e13 100644 --- a/src/components/ipa/Guidelines/Guidelines.module.css +++ b/src/components/ipa/Guidelines/Guidelines.module.css @@ -2,8 +2,7 @@ counter-reset: number-circle; list-style: none; margin: 2rem 0; - /* Horizontal padding only. */ - padding: 0 1.5rem; + padding: 0; border-radius: 0.5rem; border: 1px solid var(--ifm-color-emphasis-200); background-color: var(--ifm-card-background-color); @@ -11,9 +10,9 @@ overflow: hidden; } -/* Vertical padding per guideline. */ +/* Padding per guideline, so target highlights include the inset space. */ .root > * { - padding: 1.5rem 0; + padding: 1.5rem; } /* Separator, centered between guidelines (mirrors the infima list-item margin). */