docs: add GDK API Reference nav item + deep-link anchors (#11564)
This commit is contained in:
@@ -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<GdkItem["kind"], string> = {
|
||||
object: "Class",
|
||||
callback: "Interface",
|
||||
@@ -58,6 +82,35 @@ const KIND_HEADINGS: Record<GdkItem["kind"], string> = {
|
||||
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 (
|
||||
<Link
|
||||
className="hash-link"
|
||||
to={`${search}#${anchor}`}
|
||||
aria-label={title}
|
||||
title={title}
|
||||
translate="no"
|
||||
>
|
||||
​
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<As id={anchor} className={clsx("anchor", anchorTargetClassName, className)}>
|
||||
{children}
|
||||
<HashLink anchor={anchor} label={label} />
|
||||
</As>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.name}>
|
||||
<td>
|
||||
<code>{language.field(row.name)}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{language.type(row.type)}</code>
|
||||
</td>
|
||||
{hasDefaults && (
|
||||
<td>{row.default ? <code>{language.default(row.default)}</code> : "—"}</td>
|
||||
)}
|
||||
<td>{row.docs || "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.map((row) => {
|
||||
const name = language.field(row.name);
|
||||
return (
|
||||
<tr key={row.name}>
|
||||
<Anchored
|
||||
as="td"
|
||||
anchor={memberAnchor(ownerAnchor, rowKind, row.name)}
|
||||
label={name}
|
||||
className={styles.nameCell}
|
||||
>
|
||||
<code>{name}</code>
|
||||
</Anchored>
|
||||
<td>
|
||||
<code>{language.type(row.type)}</code>
|
||||
</td>
|
||||
{hasDefaults && (
|
||||
<td>{row.default ? <code>{language.default(row.default)}</code> : "—"}</td>
|
||||
)}
|
||||
<td>{row.docs || "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
@@ -148,14 +251,22 @@ function FuncEntry({
|
||||
language: Language;
|
||||
owner?: string;
|
||||
}) {
|
||||
const anchor = funcAnchor(func, owner);
|
||||
const name = language.func(func.name);
|
||||
return (
|
||||
<div className={styles.entry} id={slug(owner ?? "fn", func.name)}>
|
||||
<h4 className={styles.entryTitle}>
|
||||
<code>{language.func(func.name)}</code>
|
||||
</h4>
|
||||
<div className={styles.entry}>
|
||||
<Anchored as="h4" anchor={anchor} label={name} className={styles.entryTitle}>
|
||||
<code>{name}</code>
|
||||
</Anchored>
|
||||
{func.docs && <p>{func.docs}</p>}
|
||||
<CodeBlock language={language.prism}>{signature(func, language, owner)}</CodeBlock>
|
||||
<ParamTable rows={func.params} language={language} caption="Parameter" />
|
||||
<ParamTable
|
||||
rows={func.params}
|
||||
language={language}
|
||||
caption="Parameter"
|
||||
ownerAnchor={anchor}
|
||||
rowKind="param"
|
||||
/>
|
||||
{func.throws && (
|
||||
<p className={styles.meta}>
|
||||
Raises <code>{language.errorType(func.throws)}</code>
|
||||
@@ -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 (
|
||||
<section className={styles.item} id={slug(item.name)}>
|
||||
<h3 className={styles.itemTitle}>
|
||||
<code>{item.kind === "error" ? language.errorType(item.name) : item.name}</code>
|
||||
<section className={styles.item}>
|
||||
<Anchored as="h3" anchor={anchor} label={name} className={styles.itemTitle}>
|
||||
<code>{name}</code>
|
||||
<span className={styles.badge}>{KIND_LABELS[item.kind]}</span>
|
||||
</h3>
|
||||
</Anchored>
|
||||
{item.docs && <p>{item.docs}</p>}
|
||||
|
||||
<ParamTable rows={item.fields} language={language} caption="Field" />
|
||||
<ParamTable
|
||||
rows={item.fields}
|
||||
language={language}
|
||||
caption="Field"
|
||||
ownerAnchor={anchor}
|
||||
rowKind="field"
|
||||
/>
|
||||
|
||||
{item.variants.length > 0 && (
|
||||
<table className={styles.table}>
|
||||
@@ -186,28 +305,35 @@ function ItemEntry({ item, language }: { item: GdkItem; language: Language }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.variants.map((variant) => (
|
||||
<tr key={variant.name}>
|
||||
<td>
|
||||
<code>
|
||||
{item.kind === "error" && language.id === "kotlin"
|
||||
? `${language.errorType(item.name)}.${variant.name}`
|
||||
: language.variant(variant.name, dataCarrying)}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{variant.fields.length === 0
|
||||
? "—"
|
||||
: variant.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<code>
|
||||
{language.field(field.name)}: {language.type(field.type)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{item.variants.map((variant) => {
|
||||
const variantName =
|
||||
item.kind === "error" && language.id === "kotlin"
|
||||
? `${language.errorType(item.name)}.${variant.name}`
|
||||
: language.variant(variant.name, dataCarrying);
|
||||
return (
|
||||
<tr key={variant.name}>
|
||||
<Anchored
|
||||
as="td"
|
||||
anchor={memberAnchor(anchor, "variant", variant.name)}
|
||||
label={variantName}
|
||||
className={styles.nameCell}
|
||||
>
|
||||
<code>{variantName}</code>
|
||||
</Anchored>
|
||||
<td>
|
||||
{variant.fields.length === 0
|
||||
? "—"
|
||||
: variant.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<code>
|
||||
{language.field(field.name)}: {language.type(field.type)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
@@ -221,7 +347,23 @@ function ItemEntry({ item, language }: { item: GdkItem; language: Language }) {
|
||||
|
||||
export default function GdkApiReference() {
|
||||
const [languageId, setLanguageId] = useState<LanguageId>("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 (
|
||||
<div>
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.tabs} role="tablist" aria-label="GDK language">
|
||||
{LANGUAGES.map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={entry.id === languageId}
|
||||
className={entry.id === languageId ? styles.tabActive : styles.tab}
|
||||
onClick={() => setLanguageId(entry.id)}
|
||||
<VersionSearchContext.Provider value={versionSearch(docVersion, location.search)}>
|
||||
<div>
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.tabs} role="tablist" aria-label="GDK language">
|
||||
{LANGUAGES.map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={entry.id === languageId}
|
||||
className={entry.id === languageId ? styles.tabActive : styles.tab}
|
||||
onClick={() => setLanguageId(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className={styles.version}>
|
||||
Version
|
||||
<select
|
||||
value={docVersion}
|
||||
onChange={(event) =>
|
||||
history.replace({
|
||||
search: versionSearch(event.target.value, location.search),
|
||||
hash: location.hash,
|
||||
})
|
||||
}
|
||||
aria-label="GDK version"
|
||||
>
|
||||
{entry.label}
|
||||
</button>
|
||||
))}
|
||||
{VERSIONS.map((entry) => (
|
||||
<option key={entry.docVersion} value={entry.docVersion}>
|
||||
{entry.docVersion}.x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className={styles.version}>
|
||||
Version
|
||||
<select
|
||||
value={docVersion}
|
||||
onChange={(event) => setDocVersion(event.target.value)}
|
||||
aria-label="GDK version"
|
||||
>
|
||||
{VERSIONS.map((entry) => (
|
||||
<option key={entry.docVersion} value={entry.docVersion}>
|
||||
{entry.docVersion}.x
|
||||
</option>
|
||||
<p className={styles.meta}>
|
||||
Generated from <code>{doc.source}</code> at <code>goose-sdk {doc.version}</code>.
|
||||
</p>
|
||||
|
||||
<Anchored as="h2" anchor="functions" label="Functions">
|
||||
Functions
|
||||
</Anchored>
|
||||
{doc.functions.map((func) => (
|
||||
<FuncEntry key={func.name} func={func} language={language} />
|
||||
))}
|
||||
|
||||
{grouped.map((group) => (
|
||||
<React.Fragment key={group.kind}>
|
||||
<Anchored
|
||||
as="h2"
|
||||
anchor={slug(group.kind, "types")}
|
||||
label={KIND_HEADINGS[group.kind]}
|
||||
>
|
||||
{KIND_HEADINGS[group.kind]}
|
||||
</Anchored>
|
||||
{group.items.map((item) => (
|
||||
<ItemEntry key={item.name} item={item} language={language} />
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className={styles.meta}>
|
||||
Generated from <code>{doc.source}</code> at <code>goose-sdk {doc.version}</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="functions">Functions</h2>
|
||||
{doc.functions.map((func) => (
|
||||
<FuncEntry key={func.name} func={func} language={language} />
|
||||
))}
|
||||
|
||||
{grouped.map((group) => (
|
||||
<React.Fragment key={group.kind}>
|
||||
<h2 id={slug(group.kind, "types")}>{KIND_HEADINGS[group.kind]}</h2>
|
||||
{group.items.map((item) => (
|
||||
<ItemEntry key={item.name} item={item} language={language} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</VersionSearchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user