/* Impementation of CommonMark specification for markdown with support * for custom syntax extensions via the parser options. Instead of * returning an AST that has a second conversion pass to JSX, the * returned value of 'parse' is 'engine.Node' which can be stringified * via clover's SSR engine. This way, generation optimizations, async * components, and other features are gained for free here. */ function parse(src: string, options: Partial = {}) { } /* Render markdown content. Same function as 'parse', but JSX components * only take one argument and must start with a capital letter. */ export function Markdown( { src, ...options }: { src: string } & Partial, ) { return parse(src, options); } function parseInline(src: string, options: Partial = {}) { const { rules = inlineRules, links = new Map() } = options; const opts: InlineOpts = { rules, links }; const parts: engine.Node[] = []; const ruleList = Object.values(rules); parse: while (true) { for (const rule of ruleList) { if (!rule.match) continue; const match = src.match(rule.match); if (!match) continue; const index = UNWRAP(match.index); const after = src.slice(index + match[0].length); const parse = rule.parse({ after, match: match[0], opts }); if (!parse) continue; // parse before parts.push(src.slice(0, index), parse.result); src = parse.rest ?? after; continue parse; } break; } parts.push(src); return parts; } // -- interfaces -- interface ParseOpts { gfm: boolean; blockRules: Record; inlineRules: Record; } interface InlineOpts { rules: Record; links: Map; } interface InlineRule { match: RegExp; parse(opts: { after: string; match: string; opts: InlineOpts; }): InlineParse | null; } interface InlineParse { result: engine.Node; rest?: string; } interface LinkRef { href: string; title: string | null; } interface BlockRule { match: RegExp; parse(opts: {}): unknown; } export const inlineRules: Record = { code: { match: /`+/, // 6.1 - code spans parse({ after, match }) { const end = after.indexOf(match); if (end === -1) return null; let inner = after.slice(0, end); const rest = after.slice(end + match.length); // If the resulting string both begins and ends with a space // character, but does not consist entirely of space characters, // a single space character is removed from the front and back. if (inner.match(/^ [^ ]+ $/)) inner = inner.slice(1, -1); return { result: {inner}, rest }; }, }, link: { match: /(?{parseInline(textSrc, opts)}, rest, }; }, }, image: { match: /!\[/, // 6.4 - images parse({ after, opts }) { // Match '[' to let the inner-most link win. const splitText = splitFirst(after, /[[\]]/); if (!splitText) return null; if (splitText.delim !== "]") return null; const { first: textSrc, rest: afterText } = splitText; }, }, emphasis: { // detect left-flanking delimiter runs, but this expression does not // consider preceding escapes. instead, those are programatically // checked inside the parse function. match: /(?:\*+|(? }; }, }, }; function parseLinkTarget(src: string) { let href: string, title: string | null = null; href = src; return { href, title }; } /* Find a delimiter while considering backslash escapes. */ function splitFirst(text: string, match: RegExp) { let first = "", delim: string, escaped: boolean; do { const find = text.match(match); if (!find) return null; delim = find[0]; const index = UNWRAP(find.index); let i = index - 1; escaped = false; while (i >= 0 && text[i] === "\\") escaped = !escaped, i -= 1; first += text.slice(0, index - +escaped); text = text.slice(index + find[0].length); } while (escaped); return { first, delim, rest: text }; } console.log(engine.ssrSync(parseInline("meow `bwaa` `` ` `` `` `z``"))); import * as engine from "#ssr"; import type { ParseOptions } from "node:querystring";