summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoraspizu <aspizu@protonmail.com>2025-03-10 02:58:09 +0530
committeraspizu <aspizu@protonmail.com>2025-03-10 02:58:09 +0530
commit8137b02b1ea25836f80e6d61fae7592ddb6837f2 (patch)
tree41c14a3a75bec0dc2a83c6de36b56f88aa1b4404 /src
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/index.ts41
-rw-r--r--src/renderer.ts77
-rw-r--r--src/vitePlugin.ts50
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,
+ }
+ },
+ }
+}