// ------------------------------------ // Markdown - GitHub-style alerts // ------------------------------------ /** * The five kinds GitHub defines, and what each one becomes here. * * They are mapped onto the admonition classes the content stylesheet already draws — the ones * `{.is-info}` and friends attach — so an alert and a hand-classed blockquote are the same object on * the page, and there is one place where an admonition is styled. `important` is the one kind with no * existing counterpart, and has a hue of its own in `css/_page-contents.scss`. * * The labels are English, as the marker itself is: what the renderer emits is stored as the page's * HTML, so nothing here can follow the reader's locale afterwards. */ const KINDS = new Map([ ['note', { className: 'is-info', label: 'Note' }], ['tip', { className: 'is-success', label: 'Tip' }], ['important', { className: 'is-important', label: 'Important' }], ['warning', { className: 'is-warning', label: 'Warning' }], ['caution', { className: 'is-danger', label: 'Caution' }] ]) /** * The marker, which has to be the whole of the blockquote's first line. * * Anything after it on that line means the author wrote a blockquote that happens to open with * brackets, which is what GitHub decides too — and the line is then left exactly as it was typed. */ const MARKER = /^\[!([a-z]+)\][ \t]*(?:\n|$)/i /** * The label, as three tokens: a paragraph carrying a class, its inline content, and the close. * * The inline token is left with nothing but `content`; the core `inline` rule runs after this one and * is what turns that into children, the same as for every other paragraph on the page. */ function labelTokens(state, label) { const open = new state.Token('paragraph_open', 'p', 1) open.attrSet('class', 'alert-title') open.block = true const inline = new state.Token('inline', '', 0) inline.content = label inline.children = [] const close = new state.Token('paragraph_close', 'p', -1) close.block = true return [open, inline, close] } export default (md) => { /* After `block` and so before `inline`, which is what makes this a matter of cutting a line off a string: at this point a paragraph is still one `inline` token holding its raw source. Run after `inline` instead and the same job means walking children and reasoning about where markdown-it put the break — a soft one, or a hard one where the author left two spaces after the marker, as the examples in GitHub's own documentation do. */ md.core.ruler.after('block', 'github_alert', (state) => { const tokens = state.tokens for (let i = 0; i < tokens.length; i++) { if ( tokens[i].type !== 'blockquote_open' || tokens[i + 1]?.type !== 'paragraph_open' || tokens[i + 2]?.type !== 'inline' ) { continue } const marker = MARKER.exec(tokens[i + 2].content) const kind = marker ? KINDS.get(marker[1].toLowerCase()) : null if (!kind) { continue } // -> Joined rather than set: an author may have classed the quote themselves, and `is-info` on // top of that is what the stylesheet is written to expect tokens[i].attrJoin('class', kind.className) const rest = tokens[i + 2].content.slice(marker[0].length) if (rest) { tokens[i + 2].content = rest tokens.splice(i + 1, 0, ...labelTokens(state, kind.label)) } else { // -> The marker was the whole paragraph, so the label takes its place rather than joining it tokens.splice(i + 1, 3, ...labelTokens(state, kind.label)) } } }) }