这是一篇过期文章 240412

本篇文章已过期,但方案仍然可工作,此处归档以备后续参考。

  • 本文为文章 Hexo 博客适配 Obsidian 新语法 中双链语法部分的分离,强烈建议使用新方法适配双向链接。
  • 在没有合适插件的情况下,我自己探索了一套方法实现这个效果。但是这套方法安装插件较多,依赖性复杂,不易维护。于是我自己写了一个更好的插件完成了这个效果,更多故事详见 【开发杂记】第一次写 Hexo 插件
  • 这套工作链服务了半年,成功见证了数十篇文章诞生。截至光荣退休时它还能工作😀。

文章介绍了在 Hexo 中成功渲染双向链接的方法。

相关插件安装

对于 OFM 语法中的 [[ ]],需安装以下三个插件。

确保安装了 Hexo 第三方插件 hexo-abbrlinkhexo-abbrlink 是一个生成文章永久链接的插件,平时写博客就很建议你使用。后续插件将基于 hexo-abbrlink 进行工作。

仓库地址:rozbo/hexo-abbrlink: create one and only link for every post for hexo (github.com)

Obsidian 第三方插件 link-info-server

从 GitHub 仓库下载压缩包:moelody/link-to-server (github.com)(理论上应该在 Obsidian 第三方插件市场找,但是找不到)。安装了插件之后记得启用。

Hexo 第三方插件 hexo-link-obsidian

仓库地址:moelody/hexo-link-obsidian (github.com)。此插件还支持更多双向链接功能,具体可查看仓库说明文档。

该插件目前存在的问题:

  • 关于网络图片的问题 · Issue #3
  • 关于生成的链接问题 · Issue #5
  • 不支持 webp 图像的识别 Pull Request #6
  • 必须保证 Obsidian 软件的运行(临时解决方法:详看下一小节)
  • 混淆了接口类型的图片(临时解决方法:修改源代码中的图片正则匹配,彻底弃用该插件的图片功能)
  • 不支持形如 [](![]()) 的格式识别。纯文本中存在这种字符就报错,就比如这里我用的是粗体加反斜杠转义而不是使用行内代码格式表示。(临时解决方法:修改源代码中的图片正则匹配,彻底弃用该插件的图片功能)

【可选】自写脚本实现 hexo 静态网页生成前自动检查

注意到,要想运行正常,得先打开 Obsidian 软件启动 3333 端口。为了避免疏忽造成链接生成的失误,我写了一个脚本对相关端口和插件进行了检查。

写好 js 脚本

使用脚本前需要安装本脚本依赖的 npm 包。也就一个 npm inastall -g yamljs(全局安装还是局部安装自己看着办),读 yaml 文件用的。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Writer: uuanqin
// At: 2023.07.27

const { exit } = require('process');
YAML = require('yamljs');
var exec = require('child_process').exec;
const package_json = require('./package.json')

function execute(cmd){
return new Promise((resolve, reject)=>{
exec(cmd, function(error, stdout, stderr) {
if(error){
console.error(error);
}
else{
// console.log(stdout)
console.log(`PRE-CHECK: excuse command - ${cmd}`)
return resolve(stdout)
}
return reject("err")
})
})
}

// 参考
// https://github.com/moelody/hexo-link-obsidian
// https://github.com/moelody/link-to-server
// 插件默认监听的端口号
var port = 3333

// 查看有没有自定义port
function update_custom_port(){

// Load yaml file using YAML.load
nativeObject = YAML.load('_config.yml');
jsonstr = JSON.stringify(nativeObject);
jsonTemp = JSON.parse(jsonstr, null);

// 有没有自定义过监听端口,有则改
if(jsonTemp.easy_images!==undefined && jsonTemp.easy_images.port){
port = jsonTemp.easy_images.port
}
}

function main(){
var pid_arr,pid_port_open_arr;
execute(`tasklist /FI "IMAGENAME eq Obsidian.exe" /NH`)
// PID 检查程序是否启动
.then((str)=>{
const reg1 = RegExp(/obsidian.exe/gi);
if(str.search(reg1)<0){
throw "Obsidian.exe is not running."
}
console.log(str)
pid_arr = str.match(/(?<=obsidian.exe\s+)[0-9]+(?=\s)/gi)
})
// Hexo插件安装检查
.then(()=>{
var version = package_json.dependencies["hexo-link-obsidian"]
if(version !== undefined){
console.log(`PRE-CHECK: Your hexo-link-obsidian version - ${version}`)
return;
}else{
throw "You haven't install hexo-link-obsidian plugin or just install it in global."
}
})
// 目标端口更新
.then(()=>{
update_custom_port()
console.log("PRE-CHECK: Target port - ",port)
})
// 查询目标端口对应开放的PID
.then(()=>{
// console.log(pid_arr)
return execute(`netstat -ano | findstr ":${port}"`)
})
// PID_PID 匹配
.then((str)=>{
console.log(str)
pid_port_open_arr = str.match(/(?<=\s+)[0-9]+(?=\r)/g)
// console.log(pid_port_open_arr)
const pid_set = new Set(pid_arr)
// console.log(pid_set)
for (num of pid_port_open_arr){
if (pid_set.has(num)){
return;
}
}
throw "Target port is not listening by Obsidian. Please check your settings."
})
.then(()=>{
console.log("PRE-CHECK: All checks are done.")
exit(0);
})
.catch((error)=>{
// 命令执行抛出err
if(error==="err"){
console.error(`ERROR: Please contact developer to seek for solutions.`)
}
else{
console.error("ERROR: ",error)
}
exit(1);
})
}

main()

这个脚本主要做以下事情:

  • 检查 Obsidian 程序是否启动
  • 检查 hexo-link-obsidian 插件是否安装
  • 确认 hexo-link-obsidian 插件监听的端口
  • 查询对应端口占用的进程是否为 Obsidian 程序

在这里,我将脚本命名为 check_obsidian.js

package.json 的配置

package.json 中的 script 有以下特点:

  • 具有钩子性质
  • 其中一条命令异常退出(返回非零值)后续命令则终止

由于在每一次本地部署和网站发布前我们都需要检查一下 Obsidian 是否打开,我们可以这样设置钩子。

什么是钩子?比如,我们设置的本地部署命令为 hexo 三连 "start": "hexo clean && hexo g && hexo s",那么我们可以新建一个命令:"prestart": "node check_obsidian.js"。当我们每次执行 npm run start 时,都会先执行 prestart、再执行 start。

当 prestart 过程出现错误时,start 就不会执行。这样我们就实现了部署前的检查。

分享一下我目前的 script 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
"scripts": {
"build": "hexo generate",
"clean": "hexo clean",
"deploy": "hexo deploy",
"server": "hexo server",
"prepublish": "npm run check-obsidian && npm run check-markdown-filename",
"publish": "hexo clean && hexo g && npm run gen-cosma && hexo d",
"postpublish": "hexo algolia && npm run flush-cdn",
"prestart": "npm run check-obsidian && npm run check-markdown-filename",
"start": "hexo clean && hexo g && npm run gen-cosma && hexo s",
"check-obsidian": "node my_scripts/check_obsidian.js",
...
},
注意

hexo algolia 放在 hexo d -g 后,否则报错

提示

&& 表示与,根据“短路效应”。只有前面的命令执行成功,后面的命令才会继续执行。
& 表示两条命令同时执行。

【可选】自写脚本检查文件标题与文件内 front-matter 中 title 属性是否相等

经过博主本人长时间使用验证,此脚本运行稳定。

编写好以下脚本后,可在 package.json 配置运行。package.json 参考配置看上一小节。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""
非侵入式检查指定文件夹下 Markdown 文件中的 Front-matter title 是否与文件名一致。

作者:wuanqin
邮箱:[email protected]
博客:https://uusnqin.top/
日期:2023.08.15
许可证:MIT
"""

import os
import re
import sys
from pathlib import Path

all_file_num = 0
no_pass = []
def walk_posts_dir(folder):
"""
遍历指定目录
:param folder:
:return:
"""
global all_file_num, no_pass
for root, dirs, files in os.walk(folder):
for file in files:
# 过滤 Markdown 文件
if not file.endswith(".md"):
continue
file_name = Path(file).stem
# 打开具体文件提取Frontmatter
file_path = os.path.join(root, file)
with open(file_path,"r",encoding="utf-8") as f:
content = f.read()
front_matter, _ = parse_yaml_front_matter(content)
file_name_front_matter = front_matter['title']
# 判断文件名与Front-matter中的title字段是否相等
if file_name == file_name_front_matter:
print(f"[File Name Check] PASS: {file}")
else:
no_pass.append(file_path)
print(f"[File Name Check] NO PASS: {file}")
all_file_num = all_file_num + 1
# 直到检查完输出结果
else:
print(f"[File Name Check] Done. {all_file_num} files Total.")
if len(no_pass)>0:
for e in no_pass:
print(f"[File Name Check] Conflict - {e}")
# 返回错误码以终止后续进程
print(f"[File Name Check] Exit 1.")
exit(1)

def parse_yaml_front_matter(content):
"""
Parses YAML front matter from a Markdown file (More general).
Now it can recognize Block Mode and Flow Mode of the YAML specification (See https://cosma.arthurperret.fr/user-manual.html#metadata).
Also, it can process Line Folding (See https://yaml.org/spec/1.2.2/).
"""
match = re.match(r"^---\n(.*?)\n---(.*)", content, re.DOTALL)
data = {}
if match:
# Divide a markdown file to two parts: the front-matter and the remaining content.
front_matter = match.group(1)
content = match.group(2)
block_mode_attribute = None
line_fold_attribute = None
# Parse yaml by lines.
for line in front_matter.split("\n"):
# If the line is the first line of an attribute.
if re.match(r"^([a-zA-Z-_]+):(.*)$", line):
match_obj = re.match(r"^([a-zA-Z-_]+):(.*)$", line, re.DOTALL)
key = match_obj.group(1)
value = match_obj.group(2)
value = value.strip(" \'\"")
value = convert2num_if_possible(value)
# A line like 'attribute: ', which means that it uses Block Mode.
if not value:
# A mark to indicate that we should treat the next line as one of the parameter of the attribute.
block_mode_attribute = key
data[key] = []
continue
# A line like 'attribute: >-', which means that it uses Line Folding.
elif value == '>-':
# A mark to indicate that we should treat the next line as one of the parameter of the attribute.
line_fold_attribute = key
data[key] = ''
continue
# Otherwise, treat the line as a common key-value pair.
# Parse a line that is in Block Mode.
elif block_mode_attribute and re.match(r"^\s*-\s(.*)$", line):
match_obj = re.match(r"^\s*-\s(.*)$", line)
data[block_mode_attribute].append(match_obj.group(1).strip())
continue
# Parse a line that is in Line Folding.
elif line_fold_attribute:
data[line_fold_attribute] = data[line_fold_attribute] + line.strip(" \'\"")
continue
else:
print(f"[WARNING] Unknown YAML line: {line}")
continue

block_mode_attribute = None
line_fold_attribute = None

# Parse a line that is in Flow Mode.
if isinstance(value, str) and re.match(r"\[(.*)\]", value, re.DOTALL):
match_obj = re.match(r"\[(.*)\]", value, re.DOTALL)
value = match_obj.group(1)
# Remove the brackets from the string
value = value.strip("[]")
# Split the string on commas and store the resulting list of tags
value = value.split(",")
# Strip any leading or trailing whitespace from each tag
value = [convert2num_if_possible(v.strip()) for v in value]
data[key] = value
return data, content

def convert2num_if_possible(value):
""" Parse values as integers or floats if possible, otherwise keep as string"""
try:
value = int(value)
except ValueError:
try:
# Check the number to avoid the peculiar bugs occur, such as '13e4' converts to 130000.0
if str(float(value)) != value:
raise ValueError
else:
value = float(value)
except ValueError:
pass
return value

def convert_dict2_yaml_front_matter(d: dict) -> str:
"""Convert a dictionary of Python to the front matter (YAML)"""
front_matter = "---\n"
for k, v in d.items():
front_matter = front_matter + f"{k}: {str(v)}\n"
return front_matter + "---\n"

if __name__ == "__main__":
walk_posts_dir(sys.argv[1])

本文参考