如何快速解析HTML里的WikiLink?

如何快速解析HTML里的WikiLink?

2024-09-12
#编码

前几天在 你好Astro! 中提到想把Obsidian的文章集成在博客中来,并实现CMS和Obsidian之间通过Wikilink的互相引用。

不过因为Ghost的API只提供了文章的HTML字符串,Remark衍生的WikiLink插件肯定都是无法使用的,而且目前Github上能找到的WikiLink相关的案例基本上都是整对于Markdown语法去解析的,对于我来说基本上没什么参考价值。

所以我只能走另外的路子了,不过方法无非就是以下几种

  • 正则过滤替换
  • 生成虚拟DOM处理

正则过滤

昨天晚上我倒是找到了 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;
}
};

虚拟化DOM

这个方式就是在Nodejs里将HTML字符串虚拟化成DOM,再去对DOM进行操作,虽然理论上可行的,但是效率应该是极低的,这个我是不愿意使用的,所以直接排除掉了。

没有其他方案了吗?

真的没有其他方案了吗?

我今天这样想着,抱着试一试的心态换着各种关键字在Github上搜索,运气不错的让我找到了这个个库:html-parse-stringify ,他可以将html字符串快速抽象成一个AST语法树,我的想法是通过遍历这个语法树来找出文本内容,再反序列化成html字符串不就好了?

所以我试着按着我的思路写了个Demo,好像的确可行,而且还可以配置那些情况下包含的 [[]] 不进行渲染。


import HTML from 'html-parse-stringify';
var ast = HTML.parse(page?.html);
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,不过这个库没有进行更多消耗资源的操作,应该是比传统的解析库性能更优秀的。

各种标签测试

WikiLink测试

note1

[[note2]]

note3

note4
  • note5

note6

note7

note8

@1900’Blog
All work and no play makes Jack a dull boy

https://cms.1900.live/ni-hao-astro/

测试一下文字包裹note9后的解析情况

note9

实际引用测试

已经初步完成联动,目前的考虑的策略顺序如下,

博客中

  1. 优先从CMS中去匹配文章 Title 一致的文章
  2. 如果博客内没有一直的则去Obsidian中匹配文件Title字段一致的文章
  3. 如果有多条数据只返回最前面一条
  4. 如果都没有则不进行转换

Obsidian中

  1. 优先匹配Obsidian中Title一致的文章
  2. 如果没有则去博客内匹配Title一致的文章
  3. 如果有多条数据只返回最前面一条
  4. 如果都没有则不进行转换

如Obsidian中的 About 这篇文章,在博客中以 [[About]] 写入,最终文章内就会解析成 About

因为Astro的本地路由都是基于网站根目录,所以我们只要将博客和Obsidian的文件分别进行静态生成,并在生成过程干涉 [[]] 的转换成 <a href=''><a/> 标签,然后给出正确的slug,便可以很方便的实现SPA形式的页面跳转。

完。

加入评论