Skip to content

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.

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.

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));
});
},

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:

src/adapters/codeblocks-api.mjs
/** 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:

astro.config.mjs
import codeblocks from 'starlight-codeblocks';
import { nextflow } from 'starlight-codeblocks/adapters/nextflow';
import { python } from 'starlight-codeblocks/adapters/python';
import { codeblocksApi } from './src/adapters/codeblocks-api.mjs';
apiLinks: { adapters: [python(), nextflow(), codeblocksApi()] },
});

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 code that findSymbols gets. The plugin ignores a name that spans two lines.
  • Keep findSymbols synchronous. Do all slow work, such as a fetch, in setup.
  • 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 setup for a missing index. Log a warning with context.warn and continue. If setup throws, the build logs a warning, and the adapter links nothing.
  • Use http or https URLs, or URLs inside the site. The plugin drops a link with any other scheme, such as javascript:.

If two adapters return the same name, or names that overlap, the first adapter in the list wins.

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-description with 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.