initial commit
This commit is contained in:
@@ -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()],
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user