Write an API link adapter
API auto-linking links names in code blocks through adapters. The plugin has adapters for Python and Nextflow. A site can add its own adapter for another language, or for its own library, in the apiLinks.adapters option.
The interface
Section titled “The interface”An adapter is an object with a name, a list of languages, two functions and an optional third:
interface ApiLinkAdapter { name: string; languages: string[]; setup(context: AdapterContext): Promise<void>; findSymbols(code: string, language: string, attributes: Record<string, string>): SymbolRef[]; describe?(symbol: SymbolRef): Promise<string | undefined>;}
interface SymbolRef { start: number; end: number; href: string; name: string; kind?: string; signature?: string; summary?: string; source: string; icon?: string | false;}| Property | Description |
|---|---|
name |
The name of the adapter. Build warnings start with it. |
languages |
The languages of the code blocks that the adapter reads, such as py. A name also covers the other names of its language: py covers python. |
setup |
Loads the indexes. The plugin calls it once, before the first code block in one of the languages. |
findSymbols |
Returns the names in the code that are links, with the link and the text of the card. attributes has the attributes on the fence line of the code block that have a text value, such as title. |
describe |
Optional. Returns a summary for a name that findSymbols returned with no summary. Use it for text that needs a fetch, such as a documentation page. |
A SymbolRef is one name in code, its link and the text of its card:
| Property | Description |
|---|---|
start |
The index of the first character of the name in code. |
end |
The index after the last character of the name. |
href |
The URL of the reference page. A URL that starts with / gets Astro’s base in front of it. |
name |
The qualified name, such as pathlib.Path. |
kind |
The kind of object, such as function, class or process. |
signature |
The first line of the card. Without a signature, the card shows the kind and the name. |
summary |
One sentence under the signature. |
source |
Where the link comes from, at the bottom of the card. |
icon |
The Simple Icons slug of the icon before the source, such as flask, or false for no icon. By default, the plugin uses the project at the start of source: NumPy 2.1 documentation gets the NumPy icon. |
The adapter context
Section titled “The adapter context”setup() gets a context with these properties:
| Property | Description |
|---|---|
root |
The project root, as an absolute path. |
cacheDir |
Astro’s cache folder, where other integrations keep their data. |
fetch(url, check?, options?) |
Gets a URL once, and keeps the body on disk. Returns the body as a Uint8Array, or null. With { quiet: true }, a failed request shows no warning. |
warn(message) |
Logs a build warning with the name of the adapter. |
Use fetch from the context to load an index from another site. The body stays in node_modules/.cache/starlight-codeblocks/, so later builds read it from disk and work without the network. If the request fails and there is no copy on disk, fetch logs a warning and returns null. Continue without that index:
async setup(context) { const data = await context.fetch('https://example.com/docs/index.json'); if (data) index = JSON.parse(new TextDecoder().decode(data));},A server can send an HTML error page with a success status. To stop fetch from keeping that page, give it a check function that throws for a body you cannot use. fetch then keeps nothing, logs a warning that ends with the error message, and returns null. If a copy on disk fails the check, fetch gets the URL again:
async setup(context) { await context.fetch('https://example.com/docs/index.json', (data) => { index = JSON.parse(new TextDecoder().decode(data)); });},An example adapter
Section titled “An example adapter”This site links the names that a JavaScript block imports from starlight-codeblocks to their reference pages. The adapter reads the import lines, then finds each imported name outside strings and comments:
/** Reference pages of the names that this package exports. */const pages = { codeblocks: { href: '/reference/options/', signature: 'codeblocks(options?: CodeblocksOptions): StarlightPlugin', summary: 'Adds the features to the Starlight site.', }, pluginCodeblocks: { href: '/reference/expressive-code-plugins/', signature: 'pluginCodeblocks(options?: CodeblocksOptions): ExpressiveCodePlugin[]', summary: 'Returns every feature as Expressive Code plugins.', }, python: { href: '/features/api-auto-linking/#python-adapter', signature: 'python(options?: PythonAdapterOptions): ApiLinkAdapter', summary: 'Links Python names through the imports in each block.', }, nextflow: { href: '/features/api-auto-linking/#nextflow-adapter', signature: 'nextflow(options?: NextflowAdapterOptions): ApiLinkAdapter', summary: 'Links Nextflow channel factories, operators and modules.', },};
// Comments and strings come first, so that names inside them never match as names.const TOKENS = /\/\/.*|\/\*[\s\S]*?\*\/|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\.|[^`\\])*`|[A-Za-z_$][\w$]*/g;const IMPORT = /^import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]*)\})?\s+from\s+'starlight-codeblocks(?:\/[\w/-]+)?';?$/gm;
/** Links the names that a JavaScript block imports from starlight-codeblocks to their reference pages. */export function codeblocksApi() { return { name: 'codeblocks-api', languages: ['js', 'mjs', 'ts'], async setup() {}, findSymbols(code) { const imported = new Set(); for (const [, name, list] of code.matchAll(IMPORT)) { for (const item of [name, ...(list?.split(',') ?? [])]) if (item) imported.add(item.trim()); } const symbols = []; for (const { 0: name, index } of code.matchAll(TOKENS)) { const page = imported.has(name) && pages[name]; const isKeyOrProperty = code[index - 1] === '.' || /^\s*:(?!:)/.test(code.slice(index + name.length)); if (page && !isKeyOrProperty) { symbols.push({ start: index, end: index + name.length, name, ...page, source: 'starlight-codeblocks reference', }); } } return symbols; }, };}To use an adapter, add it to apiLinks.adapters. Keep the built-in adapters in the list, because the list replaces the default:
import { codeblocksApi } from './src/adapters/codeblocks-api.mjs';
});The code block above shows the result: codeblocks, nextflow and python are links.
- Leave out a name when you are not certain about its link. A wrong link is worse than no link.
- Never return names inside strings or comments from
findSymbols. - Give positions in the
codethatfindSymbolsgets. The plugin ignores a name that spans two lines. - Keep
findSymbolssynchronous. Do all slow work, such as a fetch, insetup. - Return the same result for the same code. The functions run at build time, so they cannot read anything from the browser.
- Do not throw in
setupfor a missing index. Log a warning withcontext.warnand continue. Ifsetupthrows, the build logs a warning, and the adapter links nothing. - Use
httporhttpsURLs, or URLs inside the site. The plugin drops a link with any other scheme, such asjavascript:.
If two adapters return the same name, or names that overlap, the first adapter in the list wins.
Cards on links outside code blocks
Section titled “Cards on links outside code blocks”Another plugin can give its own links the same card, for example the type names in the signatures of starlight-pydocs. The plugin writes the text of the card on the link. starlight-codeblocks owns the script and the styles.
Readers see
A mapping is a dict of names to values.
You write
<p data-scb-api-links="">A mapping is a <a href="https://docs.python.org/3/library/stdtypes.html#dict" target="_blank" rel="noopener" data-scb-api-head="class dict" data-scb-api-summary="Create a new dictionary." data-scb-api-source="Python 3 documentation" data-scb-api-action="Opens docs.python.org in a new tab." aria-description="class dict. Create a new dictionary. Python 3 documentation. Opens docs.python.org in a new tab.">dict</a> of names to values.</p>| Attribute | Where | Description |
|---|---|---|
data-scb-api-links |
An element that holds the links | Loads the card script on the page. |
data-scb-api-head |
Each a element |
The first line of the card, such as a signature. The attribute marks the link for the card. |
data-scb-api-summary |
Each a element, optional |
One sentence under the first line. |
data-scb-api-source |
Each a element, optional |
Where the link comes from. |
data-scb-api-icon |
Each a element, optional |
The key of the icon before the source, in data-scb-api-icons. |
data-scb-api-icons |
The element that holds the links, optional | JSON that maps each icon key to the path of a 24 by 24 SVG icon. |
data-scb-api-action |
Each a element, optional |
What the link does when a reader clicks it, such as “Opens docs.python.org in a new tab.” |
- The card works on every page, also on a page with no code blocks. It gets the colours of the code block themes.
- The card is only for sighted readers. Give each link an
aria-descriptionwith the same text for screen readers. - The attributes are a public interface. A change to them is a breaking change.
- The card needs
codeblocks()in the Starlight plugins. It does not work with the Expressive Code preset alone.