obsidian-visualiser/transformer/canvas/transformer.ts

136 lines
4.6 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'
import type { CanvasEdge, CanvasGroup, CanvasNode } from '~/types/canvas'
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
}
})
}
}));
rawContent.groups = getGroups(rawContent);
return {
_id,
body: rawContent,
_type: 'canvas',
}
}
});
function contains(group: CanvasNode, node: CanvasNode): boolean
{
return group.x < node.x && group.y < node.y && group.x + group.width > node.x + node.width && group.y + group.height > node.y + node.height;
}
function getGroups(content: { nodes: CanvasNode[], edges: CanvasEdge[] }): CanvasGroup[]
{
const groups = content.nodes.filter(e => e.type === "group");
return groups.map(group => { return { name: group.label!, nodes: [group.id, ...content.nodes.filter(node => node.type !== "group" && contains(group, node)).map(e => e.id)] }} );
}
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$/, '')
)
}