@sidvishnoi/imd
v0.0.2
Published
Tiny inline-markdown renderer for i18n strings, with pluggable backends.
Downloads
291
Maintainers
Readme
@sidvishnoi/imd
A tiny inline-markdown renderer for small strings, with support for React, Preact, DOM, and HTML strings.
Why
i18n strings often need a bit of inline formatting (a bold word, a link), but pulling in a full markdown parser for that is overkill, and hand-splitting strings around <a> tags in every component is repetitive and easy to get wrong across languages. imd gives translators a tiny, predictable syntax to write in, and gives you one md() call per rendering target that turns their string into real elements, with full control over how each piece (a link, a button, a custom tag) actually renders.
Usage
Install the package (npm i -S @sidvishnoi/imd), then import the backend for whatever you're rendering to. Each module exports the same shape: md(text, renderers?).
React
import { md } from '@sidvishnoi/imd/react';
const TEXT = 'Please review our [terms of service](/terms) before continuing.';
{md(TEXT, { link: (children, url) => (<a href={url} className="link">{children}</a>) })}
// Renders: Please review our <a href="/terms" class="link">terms of service</a> before continuing.Preact
import { md } from '@sidvishnoi/imd/preact';
const TEXT = 'Your changes are *saved* automatically.';
{md(TEXT, { bold: (children) => <strong class="font-bold">{children}</strong> })}
// Renders: Your changes are <strong class="font-bold">saved</strong> automatically.DOM
Returns Node[]; insert with container.replaceChildren(...).
import { md } from '@sidvishnoi/imd/dom';
const TEXT = "This can't be undone. <button>Delete account</button>";
el.replaceChildren(
...md(TEXT, {
button: (children) => {
const btn = document.createElement('button');
btn.className = 'btn-danger';
btn.append(...children);
return btn;
},
}),
);
// el's markup is now:
// This can't be undone. <button class="btn-danger">Delete account</button>HTML string
Returns a string; children arrive already joined, so you can interpolate them directly.
import { md } from '@sidvishnoi/imd/html';
const TEXT = 'Enter your <mark>API key</mark> to continue.';
const markup = md(TEXT, {
html: ({ tag, children }) => `<${tag} class="highlight">${children}</${tag}>`,
});
// markup:
// 'Enter your <mark class="highlight">API key</mark> to continue.'Every renderer above is optional. Anything you don't override falls back to a plain element (<a href="…">, <strong>, <em>) with no attributes added, so md(text) with no second argument always works. Renderers just let you attach classes, handlers, or a11y attributes at the call site.
Bold, italics and hyperlinks have a simple to use syntax:
*bold* -> bold(children) => <strong>{children}</strong>
_italic_ -> italic(children) => <em>{children}</em>
[text](url) -> link(children, url) => <a href="{url}">{children}</a>For every raw-HTML string, use the html renderer to attach custom handlers:
<tag attr="…">…</tag> -> html({ tag, children, attrs }) => <tag {...attrs}>{children}</tag>For more examples (custom tags, button, the html catch-all, edge cases in parsing), see ./tests; each source module has a matching test file.
How it works
md() does two things under the hood.
First it parses: the string is scanned left to right for the four patterns above (link, bold, italic, custom tag). At each position the earliest match wins, and if two rules could match at the same spot, link beats bold beats italic beats a custom tag. This ordering is what gives the syntax its known quirks (see Caveats).
Then it renders: the resulting tree is walked node by node. For each element, it tries your renderer for that tag (link/bold/italic/button) first, then your generic html catch-all if you passed one, then falls back to a plain default element. The first one that doesn't return undefined wins.
All of this parsing and rendering logic lives once in a framework-agnostic core. react, preact, dom, and html are each just a few lines telling it how to construct an element, a text node, and how to combine a list of children for that target.
Caveats
- There are no lists, headings, code blocks, or block-level anything, so this isn't CommonMark. It's just enough syntax for a translator to write
click *here*orsee [our terms](url)inside a single string. - Custom tags can't nest and can't self-close, and attribute values must be quoted; there's no escaping syntax either. This is a fixed grammar, not an extensible one.
- Because matching is regex and position based rather than a real parser, delimiters are naive:
2 * 3 *reads as bold, anda_b_creads as italic. That's fine for short, authored UI copy, but reword or restructure the source string if you hit it. - Treat these strings as authored content, not user input. This is meant for text your team or translators write, not text submitted by end users.
