/**
* Wiki Link 是属于 obsidian 官方扩展的 markdown 语法
*
* [[文件名]] [[文件名#标题]] [[文件名#标题#标题]] [[文件名#标题|别名]]
*
* @see - https://obsidian.md/zh/help/links
*
* 插件提供的是对该语法的兼容性支持,并非实现其完全的功能。
*/
import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
import { sortBy } from '@pengzhanbo/utils'
import { ensureLeadingSlash, isLinkHttp, removeLeadingSlash } from 'vuepress/shared'
import { path } from 'vuepress/utils'
import { resolvePaths } from '../enhance/links.js'
import { slugify } from '../utils/slugify.js'
interface WikiLinkMeta {
filename: string
alias: string
titles: string[]
}
const wikiLinkDef: RuleInline = (state, silent) => {
let found = false
const max = state.posMax
const start = state.pos
if (
state.src.charCodeAt(start) !== 0x5B
|| state.src.charCodeAt(start + 1) !== 0x5B
) {
return false
}
/* istanbul ignore if -- @preserve */
if (silent)
return false
// - [[]]
if (max - start < 5)
return false
state.pos = start + 2
// 查找 ]]
while (state.pos < max) {
if (state.src.charCodeAt(state.pos) === 0x5D
&& state.src.charCodeAt(state.pos + 1) === 0x5D) {
found = true
break
}
state.md.inline.skipToken(state)
}
if (!found || start + 2 === state.pos) {
state.pos = start
return false
}
// [[xxxx]]
// ^^^^ <- content
const content = state.src.slice(start + 2, state.pos).trim()
// found!
state.posMax = state.pos
state.pos = start + 2
const [file, alias] = content.split('|')
const [filename, ...titles] = file.trim().split('#')
const token = state.push('obsidian_wiki_link', '', 0)
token.markup = '[[]]'
token.meta = {
filename: filename.trim(),
titles: titles.map(title => title.trim()),
alias: alias?.trim(),
} as WikiLinkMeta
token.content = content
state.pos = state.posMax + 2
state.posMax = max
return true
}
export function wikiLinkPlugin(md: Markdown, app: App) {
md.inline.ruler.before('emphasis', 'obsidian_wiki_link', wikiLinkDef)
md.renderer.rules.obsidian_wiki_link = (tokens, idx, _, env: MarkdownEnv) => {
const token = tokens[idx]
const { filename, titles, alias } = token.meta as WikiLinkMeta
const anchor = titles.at(-1)
const slug = anchor ? `#${slugify(anchor)}` : ''
// external link
if (isLinkHttp(filename)) {
const text = alias || (filename + (titles.length ? ` > ${titles.join(' > ')}` : ''))
return `${
md.utils.escapeHtml(text)
}`
}
// internal hash link
if (!filename) { // internal page hash link
return `${md.utils.escapeHtml(alias) || (titles.length ? `${md.utils.escapeHtml(` > ${titles.join(' > ')}`)}` : '')}`
}
const internal = findFirstPage(app, filename, env.filePathRelative ?? '')
if (internal) {
const { absolutePath, relativePath } = resolvePaths(
internal.filePathRelative!,
env.base || '/',
env.filePathRelative ?? null,
)
;(env.links ??= []).push({
raw: internal.filePathRelative!,
absolute: absolutePath,
relative: relativePath,
})
return `${md.utils.escapeHtml(alias) || (titles.length ? `${md.utils.escapeHtml(` > ${titles.join(' > ')}`)}` : '')}`
}
// other asset url
const url = ensureLeadingSlash(filename[0] === '.' ? path.join(path.dirname(env.filePathRelative ?? ''), filename) : filename)
const text = alias || (filename + (titles.length ? ` > ${titles.join(' > ')}` : ''))
return `${
md.utils.escapeHtml(text)
}`
}
}
export function findFirstPage(app: App, filename: string, relativePath: string) {
const dirname = path.dirname(relativePath)
const withExt = path.extname(filename) ? filename : `${filename}.md`
const sorted = sortBy(app.pages ?? [], page => page.filePathRelative?.split('/').length ?? Infinity)
return sorted.find((page) => {
const title = page.title || page.frontmatter?.title || page.data.title
// 匹配标题, 优先从最短路径开始匹配
if (title === filename)
return true
const relative = page.filePathRelative
/* istanbul ignore if -- @preserve */
if (!relative)
return false
const filepath = filename[0] === '.' ? path.join(dirname, filename) : removeLeadingSlash(filename)
// 精确匹配
if ((filepath.slice(-1) === '/' && (relative === `${filepath}README.md` || relative === `${filepath}index.html`)) || relative === withExt) {
return true
}
// 模糊匹配,优先从最短路径匹配,sorted 已按照路径长度排序
return (filepath.slice(-1) === '/' && (relative.endsWith(`${filepath}README.md`) || relative.endsWith(`${filepath}index.html`))) || relative.endsWith(withExt)
})
}