Files
supabase/apps/www/scripts/lib/mdxToMarkdown.mjs

115 lines
3.6 KiB
JavaScript

// @ts-check
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import remarkMdx from 'remark-mdx'
import { SKIP, visit } from 'unist-util-visit'
function getJsxAttr(node, name) {
const attr = (node.attributes ?? []).find((a) => a.type === 'mdxJsxAttribute' && a.name === name)
return typeof attr?.value === 'string' ? attr.value : ''
}
function flattenChildText(nodes) {
const parts = []
for (const node of nodes) {
if (node.type === 'text' || node.type === 'inlineCode') parts.push(node.value)
else if (node.children) parts.push(flattenChildText(node.children))
}
return parts.join('')
}
const text = (value) => ({ type: 'text', value })
const para = (children) => ({ type: 'paragraph', children })
const strong = (children) => ({ type: 'strong', children })
const emph = (children) => ({ type: 'emphasis', children })
const blockquote = (children) => ({ type: 'blockquote', children })
// Unknown JSX must not become a `paragraph` (block) inside an inline parent.
function unwrap(node) {
const children = node.children ?? []
if (node.type === 'mdxJsxTextElement') return text(flattenChildText(children))
return children.length > 0 ? para(children) : text('')
}
const REWRITES = {
Img: (node) => {
const src = getJsxAttr(node, 'src')
return src ? { type: 'image', url: src, alt: getJsxAttr(node, 'alt'), title: null } : text('')
},
img: (node) => REWRITES.Img(node),
Admonition: (node) => {
const label = (getJsxAttr(node, 'type') || 'note').toUpperCase()
return blockquote([para([strong([text(label)])]), ...(node.children ?? [])])
},
Quote: (node) => {
const caption = getJsxAttr(node, 'caption')
const attribution = caption ? [para([emph([text(`${caption}`)])])] : []
return blockquote([...(node.children ?? []), ...attribution])
},
BlogCollapsible: (node) => {
const title = getJsxAttr(node, 'title')
const heading = title ? [para([strong([text(title)])])] : []
return blockquote([...heading, ...(node.children ?? [])])
},
Link: (node) => {
const href = getJsxAttr(node, 'href')
if (!href) return unwrap(node)
return { type: 'link', url: href, title: null, children: node.children ?? [] }
},
a: (node) => REWRITES.Link(node),
Subtitle: (node) => strong(node.children ?? []),
Badge: (node) => strong(node.children ?? []),
Avatar: (node) => (node.type === 'mdxJsxTextElement' ? text('') : para([])),
}
function rewriteJsxNode(node) {
return REWRITES[node.name]?.(node) ?? unwrap(node)
}
function transformMdxNodes() {
return (tree, file) => {
const unknownNames = new Set()
visit(tree, (node, index, parent) => {
if (parent == null || index == null) return
if (
node.type === 'mdxFlowExpression' ||
node.type === 'mdxTextExpression' ||
node.type === 'mdxjsEsm'
) {
parent.children.splice(index, 1)
return [SKIP, index]
}
if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
if (node.name && !(node.name in REWRITES)) unknownNames.add(node.name)
parent.children[index] = rewriteJsxNode(node)
return [SKIP, index]
})
if (unknownNames.size > 0) {
file.message(`Unknown JSX components stripped: ${[...unknownNames].sort().join(', ')}`)
}
}
}
const processor = remark().use(remarkMdx).use(remarkGfm).use(transformMdxNodes).data('settings', {
bullet: '-',
fences: true,
incrementListMarker: false,
rule: '-',
})
export async function mdxBodyToMarkdown(rawBody) {
const file = await processor.process(rawBody)
for (const msg of file.messages) console.warn(`${msg.message}`)
return String(file)
}