remark-replace-delimited
v0.2.0
Published
remark plugin to replace arbitrary delimited text with HTML
Maintainers
Readme
remark-replace-delimited
This is a unified (remark) plugin that replaces arbitrary delimited text with HTML. It enables the creation of new Markdown-style delimiter runs and converts them into valid HTML.
This plugin was created to support Obsidian-specific Markdown (e.g., ==highlights== and [[internal wikilinks]]) in non-Obsidian use cases (e.g., Astro), as well as other custom delimiters as defined by the developer.
Examples
These examples are only for demonstration purposes. Please adhere to common Markdown practices when adding your own delimiter run replacements.
Highlighting
==Finish writing <b>this</b> article.==...run through:
remarkReplaceDelimited({
beginDelimiter: '==',
beginReplacement: '<mark>',
endReplacement: '</mark>',
})(tree);...becomes:
<mark>Finish writing <b>this</b> article.</mark>Internal wikilinks
Refer to the main article on [[Encyclopedia Britannica]]....run through:
import escapeHtml from 'escape-html';
remarkReplaceDelimited({
beginDelimiter: '[[',
endDelimiter: ']]',
beginReplacement: (value) => {
value = escapeHtml(value).toLowerCase().replace(/\s/g, '-');
return `<a href="/${value}">`;
},
endReplacement: '</a>',
})(tree);...becomes:
Refer to the main article on <a href="/encyclopedia-britannica">Encyclopedia Britannica</a>.YouTube video as JSON
[yt[{"title": "Developers", "id": "Vhh_GeBPOhs"}]]...run through:
import escapeHtml from 'escape-html';
remarkReplaceDelimited({
beginDelimiter: /\[([a-z]+)\[/,
endDelimiter: ']]',
replacement: (value, beginMatch, _endMatch) => {
if (beginMatch[1] !== 'yt') { // Match first regexp group
throw new EvalError(
`Delimiter run '${beginMatch}' replacement not implemented`
);
}
const data = JSON.parse(value);
return (
`<iframe src="https://www.youtube.com/embed/${escapeHtml(data.id)}" title="'${escapeHtml(data.title)}' on YouTube"></iframe>`
);
}
});...becomes:
<iframe src="https://www.youtube.com/embed/Vhh_GeBPOhs" title="'Developers' on YouTube"></iframe>Usage
[!WARNING] This package can result in cross-site scripting (XSS) attacks, as replacement values are inserted directly into the document tree as HTML nodes. Ensure that unknown strings are sanitised before returning them in replacement values (e.g., using escape-html).
This package has a single default export, remarkReplaceDelimited(), which accepts an options object containing the below properties. A new "type": "html" node is created for each replacement, and their values are set to the replacement value.
| Name | Type | Description |
| - | - | - |
| beginDelimiter | string or RegExp | The delimiter that indicates the start of an affected span of text. |
| endDelimiter? | string or RegExp | The delimiter that indicates the end of an affected span of text. If undefined, will be beginDelimiter. |
| includeHtmlContent? | boolean | Whether to include text nodes nested within HTML elements when searching for delimiters. If undefined, defaults to false to mirror Markdown parsing. |
| beginReplacement? | ReplacementValue | The text to replace beginDelimiter with. |
| endReplacement? | ReplacementValue | The text to replace endDelimiter with. |
| replacement? | ReplacementValue | The text to replace the entire delimiter run with. Cannot be used if beginReplacement or endReplacement are in use. |
ReplacementValue is a type alias for either a raw string value, or a function that takes the following arguments:
| Name | Type | Description |
| - | - | - |
| value | string | The inner text of the delimiter run. |
| beginMatch | string or RegExpExecArray | The match object for the beginDelimiter. If beginDelimiter is a RegExp, this will be a RegExpExecArray. |
| endMatch | string or RegExpExecArray | The match object for the endDelimiter. If endDelimiter is a RegExp, this will be a RegExpExecArray. |
remark, Astro and others do not support tree transformer functions being passed in directly, as they expect parameterless functions that return transformers. Therefore, this plugin must be passed in using a wrapper function, as shown below.
To use this plugin in Astro, update astro.config.mjs like so:
export default defineConfig({
markdown: {
processor: unified({
remarkPlugins: [
() =>
remarkReplaceDelimited({
// Add your options here
}),
],
}),
},
});Specification
- Single regular expression replacement is not supported, due to the need to retain node awareness
- Text to replace must be delimited on both sides, therefore, blank delimiter matches are not supported
- Delimiter runs are only identified in headings, paragraphs and table cells (GitHub Flavoured Markdown only), and not within code or HTML, as in Markdown
- Within-HTML identification is optional for the sake of readability
- All but one replacement can be undefined to support icon image insertion etc.
- Replacements can be functions to support using inner text, enabling internal wikilink syntax etc.
- Whole string replacement is supported to allow for removing metadata etc.
- Delimiter-only replacement remains in order to support nested replacement
- Inner text is passed to replacement functions as a concatenated values string
- Passing the tree introduces unneeded complexity
- Replacements are strings that are converted to HTML nodes regardless of their content
- Requiring replacement nodes introduces unneeded complexity, developers should instead follow XSS best practices
- Existing node positions are updated to exclude removed characters
- Created nodes have no position set, as they are not in the original document
See also
remark-directive- Convert generic directives (e.g.:cite[smith04],::youtube[Video of a cat in a box]{v=01ab2cd3efg}, etc.) to valid HTMLremark-math- Convert LaTeX math blocks to valid HTML using KaTeX or MathJax
Contributing
To start contributing, fork this repository. Execute npm run test to ensure no changes are broken. Pull requests are welcome.
