Skip to content

Instantly share code, notes, and snippets.

@boylett
Created February 17, 2024 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boylett/1885e0cff5ab0486bcf89575e3be51e3 to your computer and use it in GitHub Desktop.
Save boylett/1885e0cff5ab0486bcf89575e3be51e3 to your computer and use it in GitHub Desktop.
Configurable TypeScript template for creating MarkdownIt inline plugins
import { PluginWithOptions, Token } from 'markdown-it';
import { type RuleInline } from 'markdown-it/lib/parser_inline.js';
import type StateInline from 'markdown-it/lib/rules_inline/state_inline.js';
/**
* Inline plugin factory
*
* @example
*
* import MarkdownItInline from './MarkdownItInline';
*
* mdit.use(
* MarkdownItInline({
* char: '!',
* name: 'enlarge',
* tokens: {
* open: {
* tag: 'h1',
* },
* },
* })
* );
*
* Markdown result:
* !!enlarged text!!
* Renders as:
* <h1>enlarged text</h1>
*
* Based on:
* @link https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/mark/src/plugin.ts
*/
const plugin: PluginWithOptions<{
char: string;
name: string;
tokens: {
open: Partial<Token>,
close?: Partial<Token>,
},
}> = (md, plugin = {
char: '=',
name: 'mark',
tokens: {
open: {
content: '',
tag: 'mark',
},
},
}) => {
/*
* Insert each marker as a separate text token, and add it to delimiter list
*/
const tokenize: RuleInline = (state, silent) => {
const start = state.pos;
const marker = state.src.charAt(start);
if (silent || marker !== plugin.char) {
return false;
}
const scanned = state.scanDelims(state.pos, true);
let { length } = scanned;
if (length < 2) {
return false;
}
let token;
if (length % 2) {
token = state.push('text', '', 0);
token.content = marker;
length -= 1;
}
for (let index = 0; index < length; index += 2) {
token = state.push('text', '', 0);
token.content = `${ marker }${ marker }`;
if (scanned.can_open || scanned.can_close) {
state.delimiters.push({
marker: plugin.char.charCodeAt(0),
length: 0, // disable 'rule of 3' length checks meant for emphasis
jump: index / 2, // 1 delimiter = 2 characters
token: state.tokens.length - 1,
end: -1,
open: scanned.can_open,
close: scanned.can_close,
});
}
}
state.pos += scanned.length;
return true;
};
/*
* Walk through delimiter list and replace text tokens with tags
*/
const postProcess = (
state: StateInline,
delimiters: StateInline.Delimiter[],
): void => {
let token;
const loneMarkers = [];
const max = delimiters.length;
for (let index = 0; index < max; index++) {
const startDelim = delimiters[ index ];
if (startDelim.marker === plugin.char.charCodeAt(0) && startDelim.end !== -1) {
const endDelim = delimiters[ startDelim.end ];
state.tokens[ startDelim.token ] = {
...state.tokens[ startDelim.token ],
markup: `${ plugin.char }${ plugin.char }`,
...plugin.tokens.open,
nesting: 1,
type: `${ plugin.name }_open`,
} as Token;
state.tokens[ endDelim.token ] = {
...state.tokens[ endDelim.token ],
markup: `${ plugin.char }${ plugin.char }`,
...plugin.tokens.close || plugin.tokens.open,
nesting: -1,
type: `${ plugin.name }_close`,
} as Token;
if (
state.tokens[ endDelim.token - 1 ].type === 'text' &&
state.tokens[ endDelim.token - 1 ].content === plugin.char
) {
loneMarkers.push(endDelim.token - 1);
}
}
}
/*
* If a marker sequence has an odd number of characters, it’s splitted
* like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
* start of the sequence.
*
* So, we have to move all those markers after subsequent s_close tags.
*/
while (loneMarkers.length) {
let current = loneMarkers.pop()!;
let next = current + 1;
while (next < state.tokens.length && state.tokens[ next ].type === `${ plugin.name }_close`) {
next += 1;
}
next -= 1;
if (current !== next) {
token = state.tokens[ next ];
state.tokens[ next ] = state.tokens[ current ];
state.tokens[ current ] = token;
}
}
};
md.inline.ruler.before('emphasis', plugin.name, tokenize);
md.inline.ruler2.before('emphasis', plugin.name, (state) => {
const tokensMeta = state.tokens_meta || [];
postProcess(state, state.delimiters);
for (let current = 0; current < tokensMeta.length; current++) {
const tokenMeta = tokensMeta[ current ];
if (tokenMeta?.delimiters) {
postProcess(state, tokenMeta.delimiters);
}
}
return true;
});
};
export default plugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment