1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
x20 x20 x20 x20 x20 x20 x20 x20 x20 x49 x49 x49 x49 x49 x49 x20 x49 x49 x35 x35 x35 x1 x1 x35 x35 x35 x35 x35 x35 x49 x2 x2 x2 x2 x2 x2 x2 x20 x20 x20 x20 x20 x3 x3 x3 x3 x3 x3 x1 x1 x1 x1 x1 x1 x2 x3 x20 |
I I |
// Imports
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"
/**
* Markdown renderer.
*/
export class Renderer {
/** Constructor. */
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)
}
/** Renderer processor. */
#processor: Processor
/** Plugins storage by render id. */
protected storage = {} as Record<PropertyKey, Record<PropertyKey, unknown>>
/**
* Render markdown content into an HTML string.
*
* ```ts
* import { Renderer } from "./renderer.ts"
* await Renderer.render("# Hello, world!")
* ```
* ```ts
* import { Renderer } from "./renderer.ts"
* import { gfm, highlighting, math, markers, wikilinks, sanitize } from "./plugins/mod.ts"
* const renderer = new Renderer({ plugins: [ gfm, highlighting, math, markers, wikilinks, sanitize ] })
* await renderer.render("# Hello, world!")
* ```
*/
async render(content: string, options?: { metadata?: false }): Promise<string>
/**
* Render markdown content into an HTML string with parsed metadata.
*
* ```ts
* import { Renderer } from "./renderer.ts"
* import pluginGfm from "./plugins/gfm.ts"
* import pluginFrontmatter from "./plugins/frontmatter.ts"
*
* const renderer = new Renderer({ plugins: [ pluginGfm, pluginFrontmatter ] })
* await renderer.render(`
* ---
* title: Hello, world!
* ---
* Lorem ipsum dolor sit amet.
* `.trim())
* ```
*/
async render(content: string, options?: { metadata: true }): Promise<{ value: string; metadata: Record<PropertyKey, unknown> }>
/**
* Render markdown content.
*/
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]
}
}
/**
* Render markdown content into an HTML string.
*
* ```ts
* import { Renderer } from "./renderer.ts"
* await Renderer.render("# Hello, world!")
* ```
* ```ts
* import { Renderer } from "./renderer.ts"
* import { gfm, highlighting, math, markers, wikilinks, sanitize } from "./plugins/mod.ts"
* const renderer = new Renderer({ plugins: [ gfm, highlighting, math, markers, wikilinks, sanitize ] })
* await renderer.render("# Hello, world!")
* ```
*/
renderSync(content: string, options?: { metadata?: false }): string
/**
* Render markdown content into an HTML string with parsed metadata.
*
* ```ts
* import { Renderer } from "./renderer.ts"
* import pluginGfm from "./plugins/gfm.ts"
* import pluginFrontmatter from "./plugins/frontmatter.ts"
*
* const renderer = new Renderer({ plugins: [ pluginGfm, pluginFrontmatter ] })
* await renderer.render(`
* ---
* title: Hello, world!
* ---
* Lorem ipsum dolor sit amet.
* `.trim())
* ```
*/
renderSync(content: string, options?: { metadata: true }): { value: string; metadata: Record<PropertyKey, unknown> }
/**
* Render markdown content.
*/
renderSync(content: string, { metadata = false } = {} as { metadata?: boolean }): string | { value: string; metadata: Record<PropertyKey, unknown> } {
const id = (this.#id++) % Number.MAX_SAFE_INTEGER
try {
if (metadata) {
this.storage[id] ??= {}
}
const value = `${this.#processor.processSync({ id, value: content, cwd: "" })}`
return metadata ? { value, metadata: { ...this.storage[id] } } : value
} finally {
delete this.storage[id]
}
}
/** Render request id counter. */
#id = 0
/** Default renderer instance. */
static default = new Renderer({ plugins: [pluginGfm, pluginSanitize] }) as Renderer
/** See {@link Renderer.render}. */
static render = this.default.render.bind(this.default) as typeof Renderer.prototype.render
/** See {@link Renderer.renderSync}. */
static renderSync = this.default.renderSync.bind(this.default) as typeof Renderer.prototype.renderSync
/**
* Instantiate a new renderer with specified plugins.
*
* Plugins may be specified as a URL or string path to a module, or as an already import {@link Plugin} object.
*
* ```ts
* import { Renderer } from "./renderer.ts"
* import frontmatter from "./plugins/frontmatter.ts"
*
* const renderer = await Renderer.with({
* plugins: [
* frontmatter,
* "./plugins/gfm.ts",
* new URL("./plugins/sanitize.ts", import.meta.url),
* ]
* })
* await renderer.render("# foo")
* ```
*/
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 })
}
}
/** {@link Renderer} with exposed protected properties. */
export type FriendlyRenderer = Renderer & { storage: Record<PropertyKey, Record<PropertyKey, unknown>> }
/** Markdown processor. */
// deno-lint-ignore no-explicit-any
export type Processor = _Processor<any, any, any, any, any>
/** Markdown plugin. */
export type Plugin = {
remark?: (processor: Processor, renderer: FriendlyRenderer) => Processor
rehype?: (processor: Processor, renderer: FriendlyRenderer) => Processor
}
|