前几天在 你好Astro! 中提到想把Obsidian的文章集成在博客中来,并实现CMS和Obsidian之间通过Wikilink的互相引用。
不过因为Ghost的API只提供了文章的HTML字符串,Remark衍生的WikiLink插件肯定都是无法使用的,而且目前Github上能找到的WikiLink相关的案例基本上都是整对于Markdown语法去解析的,对于我来说基本上没什么参考价值。
所以我只能走另外的路子了,不过方法无非就是以下几种
昨天晚上我倒是找到了 loveminimal 大佬写的一个姑且能用方案,但是这段代码有个不大不小的缺陷,就是正则会将所有标签内的 [[]]
内容都进行解析转换,所以 pre
代码块内的内容也会被解析成Wikilink,这意味着会无法控制的出现在任何你可能不想他出现的地方,所以目前这种方案暂时待选。
const wikilinks = (innerHtml) => {
let _innerHtml = innerHtml;
if (_innerHtml.indexOf('[[') > -1) {
let _re = /!\[\[(([\/\-\.\*\$\&]|\w|\s|[^\x00-\xff])*\.\w+)\s*\|?\s*(\d*)\]\]/g;
let _str = _innerHtml.replace(_re, '<img src="/$1" alt="$1" width="$3" />');
// 2. 后匹配替换链接
let _reLink = /\[\[(([\/\-\.\*\$\:\#]|\w|\s|[^\x00-\xff])*)\|?(([\/\-\.\*\$]|\w|\s|[^\x00-\xff])*)\]\]/g;
// let _strLink = _str.match(_reLink);
// let _strLink = _str.replace(_reLink, '<a href="$1">$3</a>');
let _strLink = _str.replace(_reLink, (val) => {
val = val.replace(/[\[\]]/g, '');
let _arr = val.split(/\s*\|\s*/);
let _relLink = _arr[0];
let _desc = _arr[1] ? _arr[1] : _arr[0];
// 检查链接描述是否包含 #锚点,形式有(我们假设当前文章名称为 test ,它有一个章节 ttt):
// - 2.1. 孙子兵法#军争篇 - 此类可以正常识别
// - 2.2. cpu-是如何制造出来的#18.-等级测试 - https://example.com/cpu-是如何制造出来的#18.-等级测试 ,
// 此类锚点中包含特殊符号 `.` ,在新标签中打开,且无法正确定位到锚点
// - 2.3 test#ttt - https://example.com/test#ttt 默认会在新标签页中打开,需要优化为在当前页面滚动
// - 2.4 #ttt - 不能正常,会翻译为 https://example.com/#ttt ,丢失了当前页面路径
let _idx = _desc.indexOf('#');
if (_idx > -1) {
// 2.4
if (_idx == 0) {
_relLink = location.pathname.slice(1) + _desc;
} else {
// 2.3
_relLink = _desc.replace('#', '/#');
// 2.2
_relLink = _relLink.replace(/[\.\、]/g, '');
}
}
// console.log(_arr);
// console.log(_desc);
// return `<a href="${_arr[0]}">${_desc}</a>`
return `<a href="/${_relLink.replace(/\s/g, '-').toLowerCase()}">${_desc}</a>`;
// });
});
return _strLink;
}
};
这个方式就是在Nodejs里将HTML字符串虚拟化成DOM,再去对DOM进行操作,虽然理论上可行的,但是效率应该是极低的,这个我是不愿意使用的,所以直接排除掉了。
真的没有其他方案了吗?
我今天这样想着,抱着试一试的心态换着各种关键字在Github上搜索,运气不错的让我找到了这个个库:html-parse-stringify ,他可以将html字符串快速抽象成一个AST语法树,我的想法是通过遍历这个语法树来找出文本内容,再反序列化成html字符串不就好了?
所以我试着按着我的思路写了个Demo,好像的确可行,而且还可以配置那些情况下包含的 [[]]
不进行渲染。
import HTML from 'html-parse-stringify';
var ast = HTML.parse(page?.html); // [!code highlight]
let fatherList = '';
function findTextNodes(node, text) {
let result = [];
fatherList += text; // 记录节点路径
// 检查当前节点,并做wikilink的识别和路径排除,这里做排除名单应该很方便
if (node.type === 'text' && node.content.startsWith('[[') && !fatherList.endsWith('precode')) // [!code highlight]{
node.content = node.content.replace('[[', '<a>test').replace(']]', '</a>');
result.push(node);
}
// 如果有子节点,递归遍历
if (node.children) {
node.children.forEach((child) => {
result = result.concat(findTextNodes(child, node.name));
});
}
return result;
}
const result = [];
ast.forEach((node) => {
const temp = findTextNodes(node, node.name);
let fatherList = '';
});
const html = HTML.stringify(ast);
这是我目前能找到的最优方案了,不过这个方案应该也是偏向于解析成dom,不过这个库没有进行更多消耗资源的操作,应该是比传统的解析库性能更优秀的。
[[note2]]
[[note4]]
[[note6]]
[[note7]]
[[note8]]
https://cms.1900.live/ni-hao-astro/
测试一下文字包裹[[note9]]后的解析情况
已经初步完成联动,目前的考虑的策略顺序如下,
博客中
Title
一致的文章Obsidian中
如Obsidian中的 About
这篇文章,在博客中以 [[About]]
写入,最终文章内就会解析成 [[About]] 。
因为Astro的本地路由都是基于网站根目录,所以我们只要将博客和Obsidian的文件分别进行静态生成,并在生成过程干涉 [[]]
的转换成 <a href=''><a/>
标签,然后给出正确的slug,便可以很方便的实现SPA形式的页面跳转。
完。