diff --git a/documentation/src/components/GdkApiReference/index.tsx b/documentation/src/components/GdkApiReference/index.tsx index dcadf618b..7e0c02fa8 100644 --- a/documentation/src/components/GdkApiReference/index.tsx +++ b/documentation/src/components/GdkApiReference/index.tsx @@ -1,4 +1,9 @@ -import React, { useMemo, useState } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; +import clsx from "clsx"; +import Link from "@docusaurus/Link"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; +import { useHistory, useLocation } from "@docusaurus/router"; +import { useAnchorTargetClassName } from "@docusaurus/theme-common"; import CodeBlock from "@theme/CodeBlock"; import apiData from "@site/src/data/gdk-api.json"; import { LANGUAGES, Language, LanguageId } from "./languages"; @@ -39,6 +44,25 @@ type GdkApiDoc = { const VERSIONS = (apiData as { versions: GdkApiDoc[] }).versions; +const DEFAULT_VERSION = VERSIONS[0].docVersion; +const VERSION_PARAM = "version"; + +const isKnownVersion = (docVersion: string | null): docVersion is string => + VERSIONS.some((entry) => entry.docVersion === docVersion); + +const readVersionParam = (search: string) => + new URLSearchParams(search).get(VERSION_PARAM); + +const versionSearch = (docVersion: string, search: string) => { + const params = new URLSearchParams(search); + params.set(VERSION_PARAM, docVersion); + return `?${params.toString()}`; +}; + +const VersionSearchContext = React.createContext( + `?${VERSION_PARAM}=${DEFAULT_VERSION}`, +); + const KIND_LABELS: Record = { object: "Class", callback: "Interface", @@ -58,6 +82,35 @@ const KIND_HEADINGS: Record = { const slug = (...parts: string[]) => parts.join("-").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase(); +// Anchors use the canonical Rust names so a link keeps working when the reader +// switches the language or version toggle. +const funcAnchor = (func: GdkFunc, owner?: string) => slug(owner ?? "fn", func.name); +const itemAnchor = (item: GdkItem) => slug(item.name); +const memberAnchor = (ownerAnchor: string, kind: string, name: string) => + slug(ownerAnchor, kind, name); + +function anchorsForFunc(func: GdkFunc, owner?: string): string[] { + const anchor = funcAnchor(func, owner); + return [anchor, ...func.params.map((param) => memberAnchor(anchor, "param", param.name))]; +} + +function anchorsForDoc(doc: GdkApiDoc): string[] { + const anchors = ["functions"]; + doc.functions.forEach((func) => anchors.push(...anchorsForFunc(func))); + + doc.items.forEach((item) => { + const anchor = itemAnchor(item); + anchors.push(anchor, slug(item.kind, "types")); + item.fields.forEach((field) => anchors.push(memberAnchor(anchor, "field", field.name))); + item.variants.forEach((variant) => + anchors.push(memberAnchor(anchor, "variant", variant.name)), + ); + item.methods.forEach((method) => anchors.push(...anchorsForFunc(method, item.name))); + }); + + return anchors; +} + function signature(func: GdkFunc, language: Language, owner?: string): string { const params = func.params .map((param) => { @@ -98,14 +151,56 @@ function signature(func: GdkFunc, language: Language, owner?: string): string { return `${throwsAnnotation}${suspend}fun ${prefix}${name}(${params})${returns ? `: ${returns}` : ""}`; } +function HashLink({ anchor, label }: { anchor: string; label: string }) { + const search = useContext(VersionSearchContext); + const title = `Direct link to ${label}`; + return ( + + ​ + + ); +} + +function Anchored({ + as: As, + anchor, + label, + className, + children, +}: { + as: "h2" | "h3" | "h4" | "td"; + anchor: string; + label: string; + className?: string; + children: React.ReactNode; +}) { + const anchorTargetClassName = useAnchorTargetClassName(anchor); + return ( + + {children} + + + ); +} + function ParamTable({ rows, language, caption, + ownerAnchor, + rowKind, }: { rows: GdkParam[]; language: Language; caption: string; + ownerAnchor: string; + rowKind: string; }) { if (rows.length === 0) return null; const hasDefaults = rows.some((row) => row.default); @@ -120,20 +215,28 @@ function ParamTable({ - {rows.map((row) => ( - - - {language.field(row.name)} - - - {language.type(row.type)} - - {hasDefaults && ( - {row.default ? {language.default(row.default)} : "—"} - )} - {row.docs || "—"} - - ))} + {rows.map((row) => { + const name = language.field(row.name); + return ( + + + {name} + + + {language.type(row.type)} + + {hasDefaults && ( + {row.default ? {language.default(row.default)} : "—"} + )} + {row.docs || "—"} + + ); + })} ); @@ -148,14 +251,22 @@ function FuncEntry({ language: Language; owner?: string; }) { + const anchor = funcAnchor(func, owner); + const name = language.func(func.name); return ( -
-

- {language.func(func.name)} -

+
+ + {name} + {func.docs &&

{func.docs}

} {signature(func, language, owner)} - + {func.throws && (

Raises {language.errorType(func.throws)} @@ -167,15 +278,23 @@ function FuncEntry({ function ItemEntry({ item, language }: { item: GdkItem; language: Language }) { const dataCarrying = item.variants.some((variant) => variant.fields.length > 0); + const anchor = itemAnchor(item); + const name = item.kind === "error" ? language.errorType(item.name) : item.name; return ( -

-

- {item.kind === "error" ? language.errorType(item.name) : item.name} +
+ + {name} {KIND_LABELS[item.kind]} -

+ {item.docs &&

{item.docs}

} - + {item.variants.length > 0 && ( @@ -186,28 +305,35 @@ function ItemEntry({ item, language }: { item: GdkItem; language: Language }) { - {item.variants.map((variant) => ( - - - - - ))} + {item.variants.map((variant) => { + const variantName = + item.kind === "error" && language.id === "kotlin" + ? `${language.errorType(item.name)}.${variant.name}` + : language.variant(variant.name, dataCarrying); + return ( + + + {variantName} + + + + ); + })}
- - {item.kind === "error" && language.id === "kotlin" - ? `${language.errorType(item.name)}.${variant.name}` - : language.variant(variant.name, dataCarrying)} - - - {variant.fields.length === 0 - ? "—" - : variant.fields.map((field) => ( -
- - {language.field(field.name)}: {language.type(field.type)} - -
- ))} -
+ {variant.fields.length === 0 + ? "—" + : variant.fields.map((field) => ( +
+ + {language.field(field.name)}: {language.type(field.type)} + +
+ ))} +
)} @@ -221,7 +347,23 @@ function ItemEntry({ item, language }: { item: GdkItem; language: Language }) { export default function GdkApiReference() { const [languageId, setLanguageId] = useState("rust"); - const [docVersion, setDocVersion] = useState(VERSIONS[0].docVersion); + const [docVersion, setDocVersion] = useState(DEFAULT_VERSION); + const brokenLinks = useBrokenLinks(); + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + const requested = readVersionParam(location.search); + if (isKnownVersion(requested)) { + setDocVersion(requested); + return; + } + setDocVersion(DEFAULT_VERSION); + history.replace({ + search: versionSearch(DEFAULT_VERSION, location.search), + hash: location.hash, + }); + }, [history, location.hash, location.search]); const language = LANGUAGES.find((entry) => entry.id === languageId)!; const doc = useMemo( @@ -229,6 +371,14 @@ export default function GdkApiReference() { [docVersion], ); + anchorsForDoc(VERSIONS[0]).forEach((anchor) => brokenLinks.collectAnchor(anchor)); + + useEffect(() => { + const anchor = location.hash.slice(1); + if (!anchor) return; + document.getElementById(decodeURIComponent(anchor))?.scrollIntoView(); + }, [docVersion, location.hash]); + const grouped = useMemo(() => { const order: GdkItem["kind"][] = ["object", "callback", "record", "enum", "error"]; return order @@ -237,56 +387,71 @@ export default function GdkApiReference() { }, [doc]); return ( -
-
-
- {LANGUAGES.map((entry) => ( - + ))} +
+ +
- + + ))}
- -

- Generated from {doc.source} at goose-sdk {doc.version}. -

- -

Functions

- {doc.functions.map((func) => ( - - ))} - - {grouped.map((group) => ( - -

{KIND_HEADINGS[group.kind]}

- {group.items.map((item) => ( - - ))} -
- ))} -
+ ); } diff --git a/documentation/src/components/GdkApiReference/styles.module.css b/documentation/src/components/GdkApiReference/styles.module.css index 675c0640d..4a99c0e92 100644 --- a/documentation/src/components/GdkApiReference/styles.module.css +++ b/documentation/src/components/GdkApiReference/styles.module.css @@ -101,3 +101,8 @@ .table td { vertical-align: top; } + +/* Keeps a row's hash link on the same line as the name it anchors. */ +.nameCell { + white-space: nowrap; +}