Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Extending

There are two ways to add a new :source: or :display::

Built into this repo

Add a built-in display

A display takes the items and returns a single AST node. It also gets the placeholder node (for its options) and the page’s vfile. Defaults and options that only one display uses belong inside that display, like renderTable’s handling of :sortable:. Add a function to the displays map in src/display.ts:

function renderCount(items: any[]) {
  return { type: "paragraph", children: [{ type: "text", value: `${items.length} items` }] };
}

export const displays = { table: renderTable, /* ...the other displays */ count: renderCount };

Now :display: count works:

```{listing}
:path: posts/*.md
:display: count
```

Add a built-in collector

A collector fills node.items. Add a function to the collectors map in src/collect.ts.

For a structured-data format, pass a parse function to collectData. It reads the directive body or the :path: file, checks for a list, and skips entries with no title. The last argument describes the expected shape, for the error message when the data doesn’t match. For example, the json source is one line:

json: (node, vfile) => collectData(node, vfile, JSON.parse, "a top-level list"),

Other sources set node.items themselves; see collectFiles and collectToc.

Change sorting or filtering

The Transform stage lives in src/transform.ts, behind selectItems(items, node). To add a new behaviour, read a new option off the placeholder inside selectItems. It’s pure (a list of items in, a list of items out), so you can unit-test it without building the docs. The Transform stage can only be changed here, not from a separate plugin.

From a separate plugin

The {listing} directive emits a listingPlaceholder node carrying the user’s options (source, display, path, sort, limit, ...). Each option is a field of the same name, camelCased when hyphenated (:tag-fields: → node.tagFields), and left unset when the user didn’t give it. Collectors fill in the rest:

Unless a collector sets ordered, its items get the same defaults as any listing: sorted date-desc and capped at 10. With ordered, neither default applies.

Your plugin should ship a document-stage transform that selects those nodes and either:

An item is a plain object; see Items on the Collectors page for the fields the built-ins understand.

Staging and ordering

Run your transform at the document stage. It can be async, for example to fetch from an API. Plugins run in the order they’re listed in myst.yml:

Order matters because of links. MyST resolves links at the start of the project stage, so item titles only link correctly if the listing is rendered before then. If a collector is listed after myst-listing by mistake, its items still render, but their titles won’t link.

myst-listing leaves a placeholder alone if it doesn’t recognize its :source: or :display:, so your plugin can claim it. A placeholder that no collector has filled by the project stage is reported as an unknown source.

Add a collector

Set node.items for the source you own; leave the rest alone:

const collectStars = {
  name: "listing-collect-stars",
  stage: "document",
  plugin: (_opts, utils) => async (tree) => {
    for (const node of utils.selectAll("listingPlaceholder", tree)) {
      if (node.source !== "stars") continue;        // only the source we own
      node.items = await fetchStars(node.path);      // your async logic here
    }
  },
};

export default { name: "Listing stars", transforms: [collectStars] };

Load both plugins in myst.yml (yours first), and :source: stars now works.

Wrap it in your own directive

A collector plugin can also ship its own directive, so users write {stars} instead of a {listing} with :source: stars. The directive emits the same placeholder node that {listing} would, and your collector and myst-listing take it from there:

const starsDirective = {
  name: "stars",
  arg: { type: String, doc: "GitHub repository in org/repo format" },
  run: (data) => [
    {
      type: "listingPlaceholder",
      children: [],
      source: "stars", // claimed by your collector
      path: data.arg,
      display: "gallery", // any {listing} option field; omitted ones get its defaults
    },
  ],
};

export default { name: "Listing stars", directives: [starsDirective], transforms: [collectStars] };

myst-release-notes uses this pattern for its {release-notes} directive.

Add a display

Replace a node whose :display: you own with your own AST. By this point a collector has already filled node.items:

const renderBadges = {
  name: "listing-display-badges",
  stage: "document",
  plugin: (_opts, utils) => (tree) => {
    for (const node of utils.selectAll("listingPlaceholder", tree)) {
      if (node.display !== "badges" || node.items === undefined) continue;
      // Replace the placeholder in place with your node.
      const out = { type: "div", children: node.items.map(toBadge) };
      for (const key of Object.keys(node)) if (key !== "type") delete node[key];
      Object.assign(node, out);
    }
  },
};

export default { name: "Listing badges", transforms: [renderBadges] };

Load it after myst-listing in myst.yml, and :display: badges now works.