Created
February 17, 2024 20:33
-
-
Save boylett/1885e0cff5ab0486bcf89575e3be51e3 to your computer and use it in GitHub Desktop.
Configurable TypeScript template for creating MarkdownIt inline plugins
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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