122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
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<string, false | MarkdownPlugin> = {}) {
|
|
const resolvedPlugins: Record<string, false | MarkdownPlugin & { instance: any }> = {}
|
|
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$/, '')
|
|
)
|
|
} |