diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.ts | 41 | ||||
| -rw-r--r-- | src/renderer.ts | 77 | ||||
| -rw-r--r-- | src/vitePlugin.ts | 50 |
3 files changed, 168 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..27ed46a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +import type {AstroIntegration, ContentEntryType, HookParameters} from "astro" +import {fileURLToPath} from "url" +import gemtextVitePlugin from "./vitePlugin.js" + +export const ext = "gmi" + +type SetupHookParams = HookParameters<"astro:config:setup"> & { + // `addPageExtension` and `contentEntryType` are not a public APIs + // Add type defs here + addPageExtension: (extension: string) => void + addContentEntryType: (contentEntryType: ContentEntryType) => void +} + +export default function gemtext(): AstroIntegration { + return { + name: "astro-gemtext", + hooks: { + "astro:config:setup": async (_params) => { + const params = _params as SetupHookParams + params.addRenderer({ + name: "astro-gemtext", + serverEntrypoint: fileURLToPath( + new URL("./renderer.js", import.meta.url), + ), + }) + params.addPageExtension(ext) + params.addContentEntryType({ + extensions: [`.${ext}`], + getEntryInfo: async (params) => { + throw new Error("Not implemented") + }, + }) + params.updateConfig({ + vite: { + plugins: [gemtextVitePlugin()], + }, + }) + }, + }, + } +} diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 0000000..677c234 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,77 @@ +import type {NamedSSRLoadedRendererValue} from "astro" +import {AstroError} from "astro/errors" +import {AstroJSX, jsx} from "astro/jsx-runtime" +import {renderJSX} from "astro/runtime/server/index.js" + +const slotName = (str: string) => + str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()) + +// NOTE: In practice, MDX components are always tagged with `__astro_tag_component__`, so the right renderer +// is used directly, and this check is not often used to return true. +export async function check( + Component: any, + props: any, + {default: children = null, ...slotted} = {}, +) { + if (typeof Component !== "function") return false + const slots: Record<string, any> = {} + for (const [key, value] of Object.entries(slotted)) { + const name = slotName(key) + slots[name] = value + } + try { + const result = await Component({...props, ...slots, children}) + return result[AstroJSX] + } catch (e) { + throwEnhancedErrorIfMdxComponent(e as Error, Component) + } + return false +} + +export async function renderToStaticMarkup( + this: any, + Component: any, + props = {}, + {default: children = null, ...slotted} = {}, +) { + const slots: Record<string, any> = {} + for (const [key, value] of Object.entries(slotted)) { + const name = slotName(key) + slots[name] = value + } + + const {result} = this + try { + const html = await renderJSX( + result, + jsx(Component, {...props, ...slots, children}), + ) + return {html} + } catch (e) { + throwEnhancedErrorIfMdxComponent(e as Error, Component) + throw e + } +} + +function throwEnhancedErrorIfMdxComponent(error: Error, Component: any) { + // if the exception is from an mdx component + // throw an error + if (Component[Symbol.for("mdx-component")]) { + // if it's an existing AstroError, we don't need to re-throw, keep the original hint + if (AstroError.is(error)) + return // Mimic the fields of the internal `AstroError` class (not from `astro/errors`) to + // provide better title and hint for the error overlay + ;(error as any).title = error.name + ;(error as any).hint = + `This issue often occurs when your MDX component encounters runtime errors.` + throw error + } +} + +const renderer: NamedSSRLoadedRendererValue = { + name: "astro:jsx", + check, + renderToStaticMarkup, +} + +export default renderer diff --git a/src/vitePlugin.ts b/src/vitePlugin.ts new file mode 100644 index 0000000..df4433d --- /dev/null +++ b/src/vitePlugin.ts @@ -0,0 +1,50 @@ +import {HTMLRenderer, parse} from "gemtext" +import {pathToFileURL} from "url" +import type {Plugin} from "vite" +import {ext} from "./index.js" + +function transform(code: string, id: string) { + const result = parse(code) + const html = result.generate(HTMLRenderer) + return ` +import { + createComponent, + render, + renderComponent, + unescapeHTML, +} from "astro/runtime/server/index.js"; +import { Fragment, jsx as h } from "astro/jsx-runtime"; +import Layout from "../layouts/Layout.astro"; +export const name = "TypstComponent"; +export const html = ${JSON.stringify(html)}; +export const frontmatter = undefined; +export const file = ${JSON.stringify(id)}; +export const url = ${JSON.stringify(pathToFileURL(id))}; +export function compiledContent() { + return html; +} +export function getHeadings() { + return undefined; +} +export async function Content() { + const content = h(Fragment, {"set:html": html}); + return h(Layout, {title: url, children: content}); +} +export default Content; +` +} + +export default function gemtextVitePlugin(): Plugin { + // let server: ViteDevServer + return { + name: "astro-gemtext", + enforce: "pre", + transform(code, id) { + if (!id.endsWith(`.${ext}`)) return + return { + code: transform(code, id), + map: null, + } + }, + } +} |
