function assert(condition: any, message?: string): asserts condition { if (!condition) { throw new Error(message || "Assertion failed"); } } export const customRules = defaultRules.clone(); /** A question mention, formatted like `#112233445566` */ customRules.insertBefore("em", { name: "mentionQuestion", match: inlineRegex(/^#([0-9]{10})(?:[0-9]{2})?/), parse(capture) { return { id: capture[1], }; }, }); /** A question mention, formatted like `@pheonix-write` */ customRules.insertBefore("em", { name: "mentionArtifact", match: inlineRegex(/^@([a-z0-9-]+)/i), parse(capture) { return { id: capture[1], }; }, }); /** The censor bar used for past deadnaming: #name# and #username# */ customRules.insertBefore("em", { name: "deadname", match: inlineRegex(/^#(name|username)#/i), parse(capture) { return { kind: capture[1], }; }, }); /** html block. allows arbitrary html, but rn only as an answer. formatted like `@html content here` */ customRules.insertBefore("paragraph", { name: "html", match: blockRegex(/^@html ((?:[^\n]|\n(?! *\n))+)(?:\n *)+\n/), parse(capture) { return { data: capture[1], }; }, }); /** for server-rendered search, mark can highlight the seen word */ customRules.insertBefore("paragraph", { name: "mark", match: inlineRegex(/^(.*?)/), parse: parseCaptureInline, }); /** Adjust autolinking to use my own components if possible */ customRules.insertBefore("url", { name: "autolink_mention", match: inlineRegex( /^https?:\/\/(?:paper(?:dave|clover)\.net|localhost:\d+)\/([^\s<]+[^<.,:;"')\]\s])/, ), parse(capture) { const qaMatch = capture[1].match(/^q\+a\/([0-9]{10}(?:[0-9]{2})?)/); if (qaMatch) { return { type: "mentionQuestion", id: qaMatch[1], }; } return { type: "link", content: [ { type: "text", content: capture[1], }, ], target: capture[1], title: undefined, }; }, }); export const questionRules = customRules.clone(); questionRules.remove("heading"); /** Input paragraphs. any paragraph prefixed with `i:` */ questionRules.insertBefore("paragraph", { name: "question", match: blockRegex(/^(?:q: [^\n]+(?:\n|$))+\s*/), parse(capture, parse, state) { const lines = capture[0] .trim() .split("\n") .map((line) => line.trim()) .filter((x) => x.length > 0) .map((x) => { assert(x.startsWith("q: ")); return x.slice(3); }); const content = lines.map((line, i) => { const parsed = parseInline(parse, line, state); if (i < lines.length - 1) { parsed.push({ type: "br" }); } return parsed; }).flat(); return { content, }; }, }); const cloverParser = createParser(questionRules); const basicNodeTagNames: Record = { mark: "mark", question: "q-", paragraph: "p", hr: "hr", blockQuote: "blockquote", em: "em", strong: "strong", u: "u", del: "del", inlineCode: "code", br: "br", }; function BasicNode(node: ASTNode, children: any) { const E = basicNodeTagNames[node.type]; return {children}; } function ListRenderer(node: ASTNode, children: any[]) { const T = node.ordered ? "ol" : "ul"; return {children.map((child) =>
  • {child}
  • )}
    ; } function MarkdownLink(node: ASTNode, children: any[]) { return {children}; } function Deadname(node: ASTNode) { return old {node.kind}; } function QuestionRef(node: ASTNode) { if (node.id.length === 12) node.id = node.id.slice(0, 10); const timestamp = questionIdToTimestamp(node.id); if (!timestamp) { console.error(`Invalid question ref: ${node.id}`); return [Invalid Question Ref: #{node.id}]; } return ( {formatQuestionTimestamp(timestamp)} ); } function ArtifactRef(node: ASTNode) { const artifact = artifactMap[node.id]; if (!artifact) { return [Invalid Artifact Ref: @{node.id}]; } const [title, url, type] = artifact; if (!url) { return {title}; } return {title}; } const br =
    ; const ssrFunctions: any = { // Default Renders // heading: HeadingRenderer, hr: BasicNode, mark: BasicNode, blockQuote: BasicNode, // codeBlock: MarkdownCodeBlock, list: ListRenderer, // table: TableRenderer, paragraph: BasicNode, // tableSeparator: ???, link: MarkdownLink, // image: MarkdownImage, em: BasicNode, strong: BasicNode, u: BasicNode, del: BasicNode, inlineCode: BasicNode, br: () => br, newline: () => br, deadname: Deadname, // Custom Elements question: BasicNode, html: (node: any) => ssr.html(node.data), mentionQuestion: QuestionRef, mentionArtifact: ArtifactRef, }; function renderNode(node: string | ASTNode | ASTNode[]): any { if (typeof node === "string") { return node; } else if (Array.isArray(node)) { return node.map((item) => renderNode(item)); } else if (node.type === "text") { return node.content; } else { const render = ssrFunctions[node.type]; if (!render) { throw new Error(`No renderer for node type "${node.type}"`); } const content = node.content as any; return render(node, content ? renderNode(content) : undefined); } } export namespace CloverMarkdown { export interface Props { text: string; inline?: boolean; } } export function CloverMarkdown(props: CloverMarkdown.Props) { const ast = cloverParser(props.text, { inline: !!props.inline }); return renderNode(ast); } import { artifactMap } from "./artifacts.ts"; import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts"; import { type ASTNode, blockRegex, createParser, defaultRules, inlineRegex, parseCaptureInline, parseInline, } from "./simple-markdown.ts"; import * as ssr from "#ssr";