Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/ipa/Guideline/Guideline.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
display: flex;
align-items: flex-start;
gap: 0.75rem;
scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem);
}

.numbered {
Expand Down
14 changes: 13 additions & 1 deletion src/components/ipa/Guideline/Guideline.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,13 +17,24 @@ const minimalGuideline = {
} satisfies GuidelineData;

describe("<Guideline> standalone", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("renders as a standalone block without a numbering circle", () => {
render(<Guideline {...minimalGuideline}>content</Guideline>);
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();
});

it("registers the guideline anchor with the Docusaurus broken-link checker", () => {
const { collectAnchor } = vi.mocked(useBrokenLinks)();
render(<Guideline {...minimalGuideline}>content</Guideline>);
expect(collectAnchor).toHaveBeenCalledWith("IPA-001-must-test-a");
});
});
35 changes: 25 additions & 10 deletions src/components/ipa/Guideline/GuidelineFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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)-/, "");
const slug = id.split("-").slice(2).join("-");
return slug
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
Expand All @@ -14,22 +15,36 @@ 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 > 0 ? 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"
// "IPA-107" → "IPA 107"
function depLabel(depId: string): string {
const num = ipaNumber(depId);
if (!hasGuidelineAnchor(depId)) return num !== null ? `IPA ${num}` : 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 {
function dependencyHref(depId: string, currentIpa: number): string {
const depIpa = ipaNumber(depId);
const hasAnchor = hasGuidelineAnchor(depId);
const anchor = `#${depId}`;
if (!hasAnchor)
return depIpa !== null && depIpa !== currentIpa ? `/${depIpa}` : anchor;

return depIpa !== null && depIpa !== currentIpa
? `/${depIpa}${anchor}`
: anchor;
Expand All @@ -46,13 +61,13 @@ export function GuidelineFooter(): ReactElement | null {
<span className={styles.label}>Depends on</span>
<div className={styles.deps}>
{guideline.dependsOn.map((depId) => (
<a
<Link
key={depId}
href={depHref(depId, principle.id)}
to={dependencyHref(depId, principle.id)}
className={styles.depTag}
>
{depLabel(depId)}
</a>
</Link>
))}
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/components/ipa/Guideline/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,12 +19,14 @@ function GuidelineBase({
...guideline
}: GuidelineProps): ReactElement {
const isInsideGuidelines = useIsInsideGuidelines();
useBrokenLinks().collectAnchor(guideline.id);
const Root = isInsideGuidelines ? "li" : "div";

return (
<GuidelineContext.Provider value={{ guideline }}>
<Root
className={clsx(styles.root, isInsideGuidelines && styles.numbered)}
id={guideline.id}
data-guideline-id={guideline.id}
>
{isInsideGuidelines && <NumberCircle />}
Expand Down
3 changes: 3 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
12 changes: 12 additions & 0 deletions vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@
// `@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),
}));

const _brokenLinksMock = { collectAnchor: vi.fn(), collectLink: vi.fn() };
vi.mock("@docusaurus/useBrokenLinks", () => ({
default: vi.fn(() => _brokenLinksMock),
}));