盘古之白 - 如何更优雅地写 wiki

用了一段时间 mediawiki,对自带的搜索还是不太满意,主要表现为:

  1. 全文搜索的中文是按照单字分词,结果比较杂乱,同页面有多个匹配时预览显示不全就算了,有些页面一个都不显示。至今还没摸出规律
  2. Cargo 的 drill 页的标题快速过滤对中文支持太差,英文也仅支持“单词样式”的(也即单词在标题头尾或者前后有空格)。这也是写这篇心得的主要原因
  3. 英文的全文搜索结果只有在“单词样式”下才比较直观
  4. 等了很久的elastic升级也没有什么进展

所以改造之路就此开始,既然只能依赖简单粗暴的数据库搜索,规范化写作就是唯一一条路,其中最重要的要算 中英文混排用空格隔开 这一条。然后就发现了宝藏工具 Pangu.js。安装打包好的浏览器插件测试,功能比较简单,就一个按键,然后可以选择是否加载网页时自动排版。这个JS库针对的是纯文本或者html,不支持markdown以及其它标记型语言。最郁闷的是,VisualEditor 下基本不起作用。

然后就找到了一个用了 Panju.js 的 wiki 站,移植过来,感觉有戏。最初的设想是通过组合快捷键触发 JS 读取选中文本,格式化好之后写入剪切板,前端显示通知然后手动粘贴。想法很美好,然而就在快完工时突然意识到一个严重漏洞:这个只针对纯文本的内容好使,碰到带格式的这么一转换肯定撸的干干净净。就此开启挖坑之旅。

首先想到的是选中文本读取为 html 格式,转换后再以 html 粘贴。碰到的第一个问题就是: Pangu.js 在 VisualEditor 下根本不起作用,而浏览器插件是可以的。研究源码发现问题出在 spacingPageBody 可用而 spacingNode 不可用。于是开始了无聊的逐句对比debug,找到根源在于 Xpath :

key: "spacingNode",
value: function spacingNode(contextNode) {
var xPathQuery = './/*/text()[normalize-space(.)]';
if (true || contextNode.children && contextNode.children.length === 0) {
// always TRUE!
xPathQuery = './/text()[normalize-space(.)]';

虽然不知道 .//*/ 意义是什么,至少可以读取到 Node 中的内容了。然而这还不够,作者在核心代码里屏蔽掉了 textarea 和带 contentEditable 属性的内容(debug到吐)。所以:

key: "isContentEditable",
value: function isContentEditable(node) {
return false && node.isContentEditable || node.getAttribute && node.getAttribute('g_editable') === 'true';
// always FALSE!

接下来就是获取选中 Node,然而又遇到另外一个问题,如果选取在段落边界,相邻的 element 可能会乱入。还有一个问题是如何保留格式再粘贴回来。所以暂时放弃这个办法,依赖 Pangu.js 自带的DOM操作逻辑直接修改原位格式化。之后就碰到了最郁闷的事:每次格式化段落之前要把光标点击到这个段落,否则再点回来就会自动还原。注意是点击,键盘移动或者 JS 模拟点击都不行。追踪还原事件发现这个写在 VE 的核心代码里,这样的话就不能全文批量格式化了。找了很多可能的解决办法,最后的决定是:妥协。回归最原始的方式:点击快捷键时通过 getSelection 函数获取光标或者选区所在的段落,执行格式化。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
document.addEventListener('keydown',(e)=>{
if(e.ctrlKey&&e.shiftKey&&e.altKey){
try {
var sel = window.getSelection();
var pos = sel.getRangeAt(0).commonAncestorContainer;
if (pos.nodeType == 3) pos = pos.parentNode;
// support textarea!
if (pos.firstElementChild && pos.firstElementChild.tagName == "TEXTAREA") {
pos = pos.firstElementChild;
pangu.spacingText(pos.value, (error, newText) => {
pos.value = newText;
pos.select()
});
}else{
pos = pos.closest('p');
pangu.spacingNode(pos);
var range = document.createRange();
range.selectNodeContents(pos);
sel.removeAllRanges();
sel.addRange(range);
}
} catch (e) {
mw.notify($( '<span><b>Fail</b> to format current section</span>'));
return
}
mw.notify($( '<span>Success to format current section</span>'));
}
})

另外,VE 有个插件系统,这个需求通过这个 Gadget 实现应该会更好些。然鹅研究了半天就逐渐失去耐心,主要原因还是找不到方法直接操作文本或者 DOM,只能做些简单的文本替换。这个先这样凑活着用,有时间再去折腾。

参考

elastic · Gerrit Code Review (wikimedia.org)

中英文混排的“Social Distancing” - Fing’s Blog

vinta/pangu.js: Paranoid text spacing in JavaScript (github.com)

View source for MediaWiki:Pangu.js | Fandom Developers Wiki

Adding JavaScript to Wiki Pages - MediaWiki

Bubble notifications - MediaWiki

javascript获取选中的文本/html - ArthurPatten - 博客园 (cnblogs.com)

javascript - JS: Get array of all selected nodes in contentEditable div - Stack Overflow

getselection - How to get selected html text with javascript? - Stack Overflow

VisualEditor - Documentation

VisualEditor/Gadgets - MediaWiki