import type { MarkdownOptions, MarkdownPlugin, MarkdownParsedContent } from '@nuxt/content/dist/runtime/types' import { defineTransformer } from '@nuxt/content/transformers' import slugify from 'slugify' import { withoutTrailingSlash, withLeadingSlash } from 'ufo' import { parseMarkdown } from '@nuxtjs/mdc/dist/runtime' import { type State } from 'mdast-util-to-hast' import { normalizeUri } from 'micromark-util-sanitize-uri' import { type Properties, type Element } from 'hast' import { type Link } from 'mdast' import { isRelative } from 'ufo' export default defineTransformer({ name: 'canvas', extensions: ['.canvas'], async parse(_id, rawContent, options) { const config = { ...options } as MarkdownOptions config.rehypePlugins = await importPlugins(config.rehypePlugins) config.remarkPlugins = await importPlugins(config.remarkPlugins) await Promise.all(rawContent.nodes?.map(async (e: any) => { if(e.text !== undefined) { e.text = await parseMarkdown(e.text as string, { remark: { plugins: config.remarkPlugins }, rehype: { options: { handlers: { link: link as any } }, plugins: config.rehypePlugins } }) } })); return { _id, body: rawContent, _type: 'canvas', } } }) async function importPlugins(plugins: Record = {}) { const resolvedPlugins: Record = {} for (const [name, plugin] of Object.entries(plugins)) { if (plugin) { resolvedPlugins[name] = { instance: plugin.instance || await import(/* @vite-ignore */ name).then(m => m.default || m), options: plugin } } else { resolvedPlugins[name] = false } } return resolvedPlugins } function link(state: State, node: Link & { attributes?: Properties }) { const properties: Properties = { ...((node.attributes || {})), href: normalizeUri(normalizeLink(node.url)) } if (node.title !== null && node.title !== undefined) { properties.title = node.title } const result: Element = { type: 'element', tagName: 'a', properties, children: state.all(node) } state.patch(node, result) return state.applyData(node, result) } function normalizeLink(link: string) { const match = link.match(/#.+$/) const hash = match ? match[0] : '' if (link.replace(/#.+$/, '').endsWith('.md') && (isRelative(link) || (!/^https?/.test(link) && !link.startsWith('/')))) { return (generatePath(link.replace('.md' + hash, ''), { forceLeadingSlash: false }) + hash) } else { return link } } const generatePath = (path: string, { forceLeadingSlash = true, respectPathCase = false } = {}): string => { path = path.split('/').map(part => slugify(refineUrlPart(part), { lower: !respectPathCase })).join('/') return forceLeadingSlash ? withLeadingSlash(withoutTrailingSlash(path)) : path } const SEMVER_REGEX = /^(\d+)(\.\d+)*(\.x)?$/ function refineUrlPart(name: string): string { name = name.split(/[/:]/).pop()! // Match 1, 1.2, 1.x, 1.2.x, 1.2.3.x, if (SEMVER_REGEX.test(name)) { return name } return ( name /** * Remove numbering */ .replace(/(\d+\.)?(.*)/, '$2') /** * Remove index keyword */ .replace(/^index(\.draft)?$/, '') /** * Remove draft keyword */ .replace(/\.draft$/, '') ) }