start the markdown parser
This commit is contained in:
parent
ea5f2bc325
commit
4c2a4f7578
2 changed files with 168 additions and 0 deletions
167
framework/lib/markdown.tsx
Normal file
167
framework/lib/markdown.tsx
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/* 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<ParseOpts> = {}) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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<ParseOpts>) {
|
||||||
|
return parse(src, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInline(src: string, options: Partial<InlineOpts> = {}) {
|
||||||
|
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;
|
||||||
|
parts.push(src.slice(0, index), parse.result);
|
||||||
|
src = parse.rest ?? after;
|
||||||
|
continue parse;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parts.push(src);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- interfaces --
|
||||||
|
interface ParseOpts {
|
||||||
|
blockRules: Record<string, BlockRule>;
|
||||||
|
inlineRules: Record<string, InlineRule>;
|
||||||
|
}
|
||||||
|
interface InlineOpts {
|
||||||
|
rules: Record<string, InlineRule>;
|
||||||
|
links: Map<string, LinkRef>;
|
||||||
|
}
|
||||||
|
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<string, InlineRule> = {
|
||||||
|
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: <code>{inner}</code>, rest };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emphasis: {},
|
||||||
|
link: {
|
||||||
|
match: /(?<!!)\[/,
|
||||||
|
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;
|
||||||
|
let href: string, title: string | null = null, rest: string;
|
||||||
|
if (afterText[0] === "(") {
|
||||||
|
// Inline link
|
||||||
|
const splitTarget = splitFirst(afterText.slice(1), /\)/);
|
||||||
|
if (!splitTarget) return null;
|
||||||
|
({ rest } = splitTarget);
|
||||||
|
const target = parseLinkTarget(splitTarget.first);
|
||||||
|
if (!target) return null;
|
||||||
|
({ href, title } = target);
|
||||||
|
} else if (afterText[0] === "[") {
|
||||||
|
const splitTarget = splitFirst(afterText.slice(1), /]/);
|
||||||
|
if (!splitTarget) return null;
|
||||||
|
const name = splitTarget.first.trim().length === 0
|
||||||
|
// Collapsed reference link
|
||||||
|
? textSrc.trim()
|
||||||
|
// Full Reference Link
|
||||||
|
: splitTarget.first.trim();
|
||||||
|
const target = opts.links.get(name);
|
||||||
|
if (!target) return null;
|
||||||
|
({ href, title } = target);
|
||||||
|
({ rest } = splitTarget);
|
||||||
|
} else {
|
||||||
|
// Shortcut reference link
|
||||||
|
const target = opts.links.get(textSrc);
|
||||||
|
if (!target) return null;
|
||||||
|
({ href, title } = target);
|
||||||
|
rest = afterText;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: <a {...{ href, title }}>{parseInline(textSrc, opts)}</a>,
|
||||||
|
rest,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
image: {},
|
||||||
|
autolink: {},
|
||||||
|
html: {},
|
||||||
|
br: {
|
||||||
|
match: / +\n|\\\n/,
|
||||||
|
parse() {
|
||||||
|
return { result: <br /> };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
|
@ -120,6 +120,7 @@ export const imagePresets = [
|
||||||
"-effort",
|
"-effort",
|
||||||
"9",
|
"9",
|
||||||
"-update",
|
"-update",
|
||||||
|
"1",
|
||||||
"-frames:v",
|
"-frames:v",
|
||||||
"1",
|
"1",
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue