ob链接语法特点
ob主要使用[[ ]]
链接其他文档,我考虑到了下面几种情况:
[[#xx]]
链接本文档的锚点[[文档名字]]
、[[文档名字.md]]
不一定是md文件。 这里我只考虑md文件。[[某文档#xx]]
链接到某文档的锚点- 支持cmd+鼠标悬浮的预览功能
[[ 文档 ]]
支持空格- 文件名不能包含
\/:
- 文件名包含
^|#[]
时,链接失效^
可控制具体跳转某一个位置|
用于起别名#
用于锚点跳转
1. 支持ob链接的改造
实现思路
vitepress中markdown由 markdown-it驱动,可以通过修改.vitepress/config.ts文件中的markdown属性来支持markdown扩展。因此我只需要编写markdown-it插件即可。
- 将满足条件的字符串替换为html标签,可以通过markdown-it修改原有的
renderer.rules.text
规则来实现 - 根据文件名匹配文件,并生成对应的路径地址
- 非贪心匹配
[[]]
中的任意字符/\[\[(.+?)]]/g;
- 优化: 过滤
\:
和[]
,/\[\[[^\[\]\\:]+?]]/g
, (需要这样的正则捕获不了文本出现的单个\
,其实也无关紧要,具体可参考这个javascript - 为何正则匹配捕获不了“\”反斜杠)
- 非贪心匹配
相关代码实现
定义一个全局变量存储文档地址
js
export const customConfiguration = {
docsFolder: 'docs'
}
export const customConfiguration = {
docsFolder: 'docs'
}
编写插件:
js
// @ts-ignore
import glob from 'glob';
// @ts-ignore
import {sep} from 'path';
import {customConfiguration} from "../../config";
const LOCAL_FILE_MAPPING = {};
/**
* 根据文件名获取所在目录, 可能还需要考虑 1.项目部署路径不是/的问题 2.docs指定未其他目录的问题
* @param fileName
*/
const findFilePath = (fileName) => {
if (Object.keys(LOCAL_FILE_MAPPING).length === 0) {
const fileList = glob.sync(`${customConfiguration.docsFolder}/**/*.md`);
for (const mdPath of fileList) {
// for fileName
const docsFileName = mdPath.split(sep).pop();
LOCAL_FILE_MAPPING[docsFileName] = mdPath;
LOCAL_FILE_MAPPING[docsFileName.replace(/\.md$/, '')] = mdPath;
// for path
const relativePath = mdPath.replace(new RegExp(`^${customConfiguration.docsFolder}/`), '')
LOCAL_FILE_MAPPING[relativePath] = mdPath;
LOCAL_FILE_MAPPING[relativePath.replace(/\.md$/, '')] = mdPath;
}
}
// obsidian支持通过| # ^, 添加额外信息,排除这些字符的影响
const splitFileName = fileName.split(/[|#^]/)[0]
// obsidian兼容了 / 开头的地址,这里去除一下
fileName = splitFileName.replace(/^\//, '');
const searchPath = LOCAL_FILE_MAPPING[fileName]
if (searchPath) {
// 路径不携带文档根目录名
const reg = new RegExp(`^${customConfiguration.docsFolder}`)
return searchPath.replace(reg, '').replace(/\.md$/, '.html')
}
return undefined;
}
export const markdownItObLink = (md: any) => {
const text = md.renderer.rules.text
md.renderer.rules.text = (...args) => {
let rawCode = text(...args);
const regex = /\[\[[^\[\]\\:]+?]]/g
const matches = rawCode.match(regex);
if (!matches || matches.length === 0) {
return rawCode;
}
for (const match of matches) {
let fileName = match.replace(/^\[\[/, '')
.replace(/]]$/, '').trim();
// 在ob中 `|` 用于起别名。另外和ob保持一致: [[a|b|c]] 显示别名为 bc if (fileName.includes('|')) {
fileName = fileName.substring(fileName.indexOf('|')).replace(/\|/g, '')
}
let mappingPath = findFilePath(fileName);
const anchorText = fileName.includes('#') ? `#${fileName.split('#')[1]}` : '';
if (mappingPath) {
// 在ob中 `#` 用于支持锚点
rawCode = rawCode.replace(match, `<span class="relative group inline-block"><a href="${mappingPath}${anchorText}" class="x-ob-link" rel="noopener">${fileName}</a></span>`);
} else {
// 页面不存在,也同样进行渲染。默认页面位置于根目录
rawCode = rawCode.replace(match, `<span class="relative group inline-block"><a href="${fileName}.html${anchorText}" class="x-ob-link opacity-60" rel="noopener">${fileName}</a></span>`);
}
}
return rawCode;
}
}
// @ts-ignore
import glob from 'glob';
// @ts-ignore
import {sep} from 'path';
import {customConfiguration} from "../../config";
const LOCAL_FILE_MAPPING = {};
/**
* 根据文件名获取所在目录, 可能还需要考虑 1.项目部署路径不是/的问题 2.docs指定未其他目录的问题
* @param fileName
*/
const findFilePath = (fileName) => {
if (Object.keys(LOCAL_FILE_MAPPING).length === 0) {
const fileList = glob.sync(`${customConfiguration.docsFolder}/**/*.md`);
for (const mdPath of fileList) {
// for fileName
const docsFileName = mdPath.split(sep).pop();
LOCAL_FILE_MAPPING[docsFileName] = mdPath;
LOCAL_FILE_MAPPING[docsFileName.replace(/\.md$/, '')] = mdPath;
// for path
const relativePath = mdPath.replace(new RegExp(`^${customConfiguration.docsFolder}/`), '')
LOCAL_FILE_MAPPING[relativePath] = mdPath;
LOCAL_FILE_MAPPING[relativePath.replace(/\.md$/, '')] = mdPath;
}
}
// obsidian支持通过| # ^, 添加额外信息,排除这些字符的影响
const splitFileName = fileName.split(/[|#^]/)[0]
// obsidian兼容了 / 开头的地址,这里去除一下
fileName = splitFileName.replace(/^\//, '');
const searchPath = LOCAL_FILE_MAPPING[fileName]
if (searchPath) {
// 路径不携带文档根目录名
const reg = new RegExp(`^${customConfiguration.docsFolder}`)
return searchPath.replace(reg, '').replace(/\.md$/, '.html')
}
return undefined;
}
export const markdownItObLink = (md: any) => {
const text = md.renderer.rules.text
md.renderer.rules.text = (...args) => {
let rawCode = text(...args);
const regex = /\[\[[^\[\]\\:]+?]]/g
const matches = rawCode.match(regex);
if (!matches || matches.length === 0) {
return rawCode;
}
for (const match of matches) {
let fileName = match.replace(/^\[\[/, '')
.replace(/]]$/, '').trim();
// 在ob中 `|` 用于起别名。另外和ob保持一致: [[a|b|c]] 显示别名为 bc if (fileName.includes('|')) {
fileName = fileName.substring(fileName.indexOf('|')).replace(/\|/g, '')
}
let mappingPath = findFilePath(fileName);
const anchorText = fileName.includes('#') ? `#${fileName.split('#')[1]}` : '';
if (mappingPath) {
// 在ob中 `#` 用于支持锚点
rawCode = rawCode.replace(match, `<span class="relative group inline-block"><a href="${mappingPath}${anchorText}" class="x-ob-link" rel="noopener">${fileName}</a></span>`);
} else {
// 页面不存在,也同样进行渲染。默认页面位置于根目录
rawCode = rawCode.replace(match, `<span class="relative group inline-block"><a href="${fileName}.html${anchorText}" class="x-ob-link opacity-60" rel="noopener">${fileName}</a></span>`);
}
}
return rawCode;
}
}
最后在配置中引入即可:
js
import {markdownItObLink} from "./extension/markdown/markdown-it-ob-link";
export default defineConfig({
...
markdown: {
config: (md) => {
md.use(markdownItObLink)
}
}
})
import {markdownItObLink} from "./extension/markdown/markdown-it-ob-link";
export default defineConfig({
...
markdown: {
config: (md) => {
md.use(markdownItObLink)
}
}
})
2. 如何使鼠标悬浮时展示预览弹窗
obsidian的部署的官方主题是支持链接悬浮时,展示预览弹窗的。我们要实现,差不多有三种思路:
- 方案1:鼠标悬浮时,根据当前的链接获取iframe
- iframe页面需要将顶部、侧栏都隐藏掉
- 缺点:加载很多不必要的资源
- 方案2:鼠标悬浮时,根据当前的链接动态请求md文件,再将md文件内容转换成html
- 开发时需要调整主题,将md源文件存放到某个固定目录中,例如assets目录中。 可用vite-plugin-static-copy插件解决。然后鼠标悬浮时,动态请求md字符串,再次渲染md成html。
找了下文档,似乎没找到vitepress动态解析md字符串的功能,目前采用单独引用独立的markdown-it渲染。 能展示个大概即可(舍去代码高亮等功能)缺点: 在浏览器环境使用独立的markdown-it渲染。展现效果和非弹窗有差异- 动态加载生成html文件,进行渲染
- 方案3:动态生成图片
- 应该非常影响打包效率,且无法复制,不做做考虑
方案2 实现基本说明
(1) 引入依赖- dependencies引入markdown-it- devDependencies引入viteStaticCopy(2) 利用viteStaticCopy的能力将md原始文件进行复制,例如复制到dist/asset文件,便于后面获取(3) 监听ob链接的class,并动态匹配到md文件的实际路径,请求到数据后,再动态解析markdown,并呈现在弹窗上。二次弹窗可使用 vite-plugin-generate-file插件生成映射文件完成 。