diff --git a/framework/lib/markdown.tsx b/framework/lib/markdown.tsx new file mode 100644 index 0000000..d2c86e9 --- /dev/null +++ b/framework/lib/markdown.tsx @@ -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 = {}) { + +} + +/* 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; + 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; + 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 }; + }, + }, + emphasis: {}, + link: { + match: /(?{parseInline(textSrc, opts)}, + rest, + }; + }, + }, + image: {}, + autolink: {}, + html: {}, + br: { + match: / +\n|\\\n/, + parse() { + return { result:
}; + }, + }, +}; + +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"; + diff --git a/src/file-viewer/transcode-rules.ts b/src/file-viewer/transcode-rules.ts index bb91c6f..4ea0b9e 100644 --- a/src/file-viewer/transcode-rules.ts +++ b/src/file-viewer/transcode-rules.ts @@ -120,6 +120,7 @@ export const imagePresets = [ "-effort", "9", "-update", + "1", "-frames:v", "1", ],