Skip to content

Latest commit

 

History

History
97 lines (73 loc) · 6.86 KB

plugins.md

File metadata and controls

97 lines (73 loc) · 6.86 KB

Plugins

fgmd has nine built-in plugins, each written against the same plugin API you can use yourself.

Pass plugins as objects, or by name so plain JSON configs (the CLI, PHP) can use them:

import { markdown, callouts, typography, emoji } from '@decbr/fgmd';
import { gemoji } from '@decbr/fgmd/emoji';

markdown(source, { plugins: [callouts(), typography({ smart: { quotes: '„“‚‘' } }), emoji({ map: gemoji })] });
markdown(source, { plugins: ['callouts', 'typography', ['emoji', { map: { tada: '🎉' } }]] });

Names are registered when @decbr/fgmd is imported. The Svelte components don't import it, to keep bundles small, so in a component pass the plugin itself: plugins={[callouts()]}. A name only works there if code that also runs in the browser imports @decbr/fgmd; an import in server-only code (such as +page.server.ts) lets the server render the page and then fails during hydration.

plugin syntax output
callouts({ types, className, containers }) GitHub alerts > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION] (plus your own types). A title can follow the marker: > [!TIP] Pro tip. [!NOTE]- / [!NOTE]+ make it collapsible, starting closed or open. :::name Title … ::: containers; :::details folds. <aside class="callout callout-note"><p class="callout-title">…, <details> when collapsible, <div class="name"> for other containers
typography({ mark, superscript, subscript, smart }) ==mark==, ^sup^, ~sub~, and smart quotes/dashes/ellipses. With subscript on, a single ~ means subscript while ~~ stays strikethrough. smart: { quotes: '“”‘’', dashes, ellipses } <mark>, <sup>, <sub>, “ ” ‘ ’ – — …
attributes({ allow }) {#id .class key=val}: at the end of a heading or paragraph, straight after a link, image, code span or emphasis (![x](a.jpg){width=300}), or in a fence's info string (```js {.numbered}). on* and style are never allowed, and the other keys go through an allowlist, so it's safe for user content. attributes on that element
definitionLists() Term then : definition lines. Indented lines continue a definition, and a blank line after the term makes it loose. <dl><dt><dd>
abbreviations({ titles }) *[HTML]: HyperText Markup Language anywhere in the document, plus titles from options <abbr title="…">HTML</abbr> on whole words
math({ render }) $inline$ (Pandoc rules: $5 and $10 stays money), $$display$$, and $$ fenced blocks <span class="math math-inline"> / <div class="math math-display"> holding escaped TeX, ready for KaTeX auto-render. Pass render: (tex, { display }) => katex.renderToString(tex, { displayMode: display }) for HTML output
emoji({ map, lookup }) :shortcode: the emoji. @decbr/fgmd/emoji exports GitHub's full set (gemoji); lookup can return nodes, e.g. custom images
components({ allow, render }) MDX-style <Chart data={[1, 2]} />, <Tabs> … </Tabs> around Markdown, and inline <Kbd>Ctrl</Kbd>. See Components whatever you map the name to; without a mapping, just the content
wikilinks({ resolve, exists, embeds, resolveEmbed }) [[Page]], [[Page|label]], [[Page#Heading]], ![[image.png]] <a class="wikilink">, plus wikilink-new when exists(page) is false

Components

components() lets Markdown use your own components, as MDX does, with props that are data rather than code. It's safe for content anyone can write. See Components for the syntax, how it works, and how it compares with MDX.

Writing a plugin

A plugin is a plain object. Every hook is optional:

import { mapText, type Plugin } from '@decbr/fgmd';

const mentions: Plugin = {
  name: 'mentions',

  // new inline syntax, tried at its trigger characters before fgmd's own
  inline: [{
    triggers: '@',
    parse(state) {
      if (/\w/.test(state.charBefore())) return null;          // not the @ in an email
      const m = state.match(/@([a-z0-9_]+)/i);                 // matched at state.pos, which then advances
      if (!m) return null;                                     // decline: fgmd carries on as normal
      return { type: 'link', url: `/u/${m[1]}`, title: null,
               children: [{ type: 'text', value: m[0] }],
               data: { hProperties: { class: 'mention' } } };
    }
  }],

  // wrapping syntax from a repeated character: ++inserted++. goes through the same
  // algorithm as *emphasis*, so it nests and interleaves correctly
  delimiters: [{ char: '+', lengths: { 2: 'insert' } }],

  // block syntax: a raw fence, a markdown container, or a single line
  block: [
    { kind: 'fence', char: '%', length: 3, node: (content, info) => ({ type: 'code', lang: 'mermaid', meta: info || null, value: content }) },
    { kind: 'container', names: ['aside'], node: (c) => { c.data = { hName: 'aside' }; } },
    { kind: 'line', match: /^\[\[toc\]\]$/, chars: '[', node: () => ({ type: 'html', value: '<nav class="toc"></nav>' }) },
    // markdown between an opening and a closing line of your choosing
    { kind: 'wrapper', chars: '@', open: (line) => (line === '@box' ? {} : null), close: (line) => line === '@end',
      node: (children) => ({ type: 'container', name: 'box', title: null, collapsible: null, children }) }
  ],

  // lines consumed from the start of a paragraph, the way [ref]: /url is
  definitions: [{ match: /%define (\w+) (.*)/, define: (m, ctx) => { ctx.data[m[1]] = m[2]; } }],

  // edit the parsed tree
  transform(tree, ctx) {
    mapText(tree, (text) => text.value.includes('{{') ? [{ type: 'text', value: text.value.replace(/\{\{(\w+)\}\}/g, (_, k) => String(ctx.data[k] ?? '')) }] : undefined);
  },

  // HTML for node types fgmd doesn't know (your own renderers option overrides these)
  renderers: { insert: (node, ctx) => `<ins>${ctx.children()}</ins>` }
};

With { positions: true }, nodes from block rules and inline rules get their position from the parser, so a rule never has to work it out. A node your transform creates has no position unless you give it one, for example by copying the node it replaces.

A line rule's chars lists the characters its line can start with. Without it, every line is tried against the rule. A wrapper's closing line goes to the innermost open wrapper that accepts it, unless it sits inside a fenced code block.

A node type neither renderer knows draws itself from data.hName/data.hProperties, in HTML and Svelte alike: { type: 'keycap', data: { hName: 'kbd' }, children: […] }. You only need a renderer for anything fancier.

For TypeScript, declare custom nodes the way mdast does:

declare module '@decbr/fgmd' {
  interface PhrasingContentMap { insert: { type: 'insert'; children: PhrasingContent[] } }
}

To let JSON configs name your plugin, call registerPlugin('mentions', () => mentions).