import { type Processor as _Processor, unified } from "unified"
import remarkRehype from "remark-rehype"
import remarkParse from "remark-parse"
import rehypeRaw from "rehype-raw"
import rehypeStringify from "rehype-stringify"
import pluginGfm from "./plugins/gfm.ts"
import pluginSanitize from "./plugins/sanitize.ts"
export class Renderer {
constructor({ plugins = [] } = {} as { plugins: Plugin[] }) {
this.#processor = unified().use(remarkParse)
plugins.filter(({ remark }) => remark).forEach(({ remark }) => this.#processor = remark!(this.#processor, this as unknown as FriendlyRenderer))
this.#processor = this.#processor.use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw)
plugins.filter(({ rehype }) => rehype).forEach(({ rehype }) => this.#processor = rehype!(this.#processor, this as unknown as FriendlyRenderer))
this.#processor = this.#processor.use(rehypeStringify)
}
#processor: Processor
protected storage = {} as Record<PropertyKey, Record<PropertyKey, unknown>>
async render(content: string, options?: { metadata?: false }): Promise<string>
async render(content: string, options?: { metadata: true }): Promise<{ value: string; metadata: Record<PropertyKey, unknown> }>
async render(content: string, { metadata = false } = {} as { metadata?: boolean }): Promise<string | { value: string; metadata: Record<PropertyKey, unknown> }> {
const id = (this.#id++) % Number.MAX_SAFE_INTEGER
try {
if (metadata) {
this.storage[id] ??= {}
}
const value = `${await this.#processor.process({ id, value: content, cwd: "" })}`
return metadata ? { value, metadata: { ...this.storage[id] } } : value
} finally {
delete this.storage[id]
}
}
#id = 0
static default = new Renderer({ plugins: [pluginGfm, pluginSanitize] }) as Renderer
static render = this.default.render.bind(this.default) as typeof Renderer.prototype.render
static async with({ plugins = [], throw: throws = true }: { plugins: Array<Plugin | URL | string>; throw?: boolean }): Promise<Renderer> {
plugins = plugins.map((plugin) => plugin instanceof URL ? plugin.href : plugin)
const imported = await Promise.allSettled(plugins.map((plugin) => (typeof plugin === "string") ? import(plugin) : { default: plugin }))
const resolved = imported
.filter((result): result is PromiseFulfilledResult<{ default: Plugin }> => result.status === "fulfilled")
.map(({ value }) => value.default)
if (throws && (imported.some(({ status }) => status === "rejected"))) {
const errors = imported
.filter((result): result is PromiseRejectedResult => result.status === "rejected")
.map(({ reason }) => reason)
.join(", ")
throw new ReferenceError(`Failed to import some plugins: ${errors}`)
}
return new Renderer({ plugins: resolved })
}
}
export type FriendlyRenderer = Renderer & { storage: Record<PropertyKey, Record<PropertyKey, unknown>> }
export type Processor = _Processor<any, any, any, any, any>
export type Plugin = {
remark?: (processor: Processor, renderer: FriendlyRenderer) => Processor
rehype?: (processor: Processor, renderer: FriendlyRenderer) => Processor
}
|