.md 文档自动编号 js 脚本

.md 文档自动编号 js 脚本

1、契机

在使用 typora 的时候,没有自动编号,每一级标题要手动编号,比较累。之前有找过修改主题的 css 来实现自动编号的,但是只是个样式而已,没有真正的编号,而且导出的 pdf 中是没有编号的。这次找的脚本是直接修改 md 文档的,根据行首的 # 来判断是不是标题。

md 文档一共有六级标题,不对一级标题编号,二级、三级、四级、五级、六级标题进行级联编号。

根据这篇文章的 java 代码修改而来,并做了一点小小的改进,用 java 感觉有点重,最近喜欢用 js 写点脚本,比如一些字符串处理的,挺方便的。

文章链接:

https://blog.csdn.net/oneby1314/article/details/107311743

2、脚本代码

js 代码如下:

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
const fs = require('fs');
const readline = require('readline');
const {once} = require('node:events')
const ncp = require("copy-paste");
const path = require('path')

/**
* 执行标题自动编号
*
* @param destMdFilePath MD 文件路径
*/
function doTitleAutoNumbering(destMdFilePath) {
// 获取标题自动编号的MD文件内容
const mdFileContent = getAutoTitledMdContent(destMdFilePath);
mdFileContent.then((res) => {
// 执行保存(覆盖原文件)
saveMdContentToFile(destMdFilePath, res);
})
}

/**
* 获取标题自动编号的MD文件内容
*
* @param destMdFilePath MD 文件路径
* @return
*/
async function getAutoTitledMdContent(destMdFilePath) {
// 标题编号
/*
标题编号规则:
- 一级标题为文章的题目,不对一级标题编号
- 二级、三级、四级、五级、六级标题需要级联编号
*/
let titleNumber = [0, 0, 0, 0, 0]
// 存储md文件内容
let mdContent = ''

const md = readline.createInterface({
input: fs.createReadStream(destMdFilePath),
output: process.stdout,
terminal: false
});

// 一行一行读取数据
md.on('line', (line) => {
// 判断是否为标题行,如果是标题,是几级标题
const curTitleLevel = calcTitleLevel(line);
if (curTitleLevel !== -1) {

// 插入标题序号
line = insertTitleNumber(line, titleNumber);

// 重新计算标题计数器
reCalcTitleCounter(curTitleLevel, titleNumber);

}
mdContent = mdContent.concat(line, '\r\n')
})

// 等待监听事件完成
await once(md, 'close')
return mdContent;
}

/**
* 计算当前标题等级
*
* @param curLine 当前行的内容
* @return -1 :非标题行;大于等于 2 的正数:当前行的标题等级
*/
function calcTitleLevel(curLine) {
// 由于一级标题无需编号,所以从二级标题开始判断
let isTitle = curLine.startsWith("##");
if (!isTitle) {
// 返回 -1 表示非标题行
return -1;
}

// 现在来看看是几级标题
return curLine.indexOf(" ");
}

/**
* 向标题行中插入标题序号
*
* @param curLine 当前行内容
* @param titleNumber 标题计数器
* @return
*/
function insertTitleNumber(curLine, titleNumber) {
// 标题等级(以空格分隔的前提是 Typora 开启严格模式)
let titleLevel = curLine.indexOf(" ");
// 标题等级部分
let titleLevelStr = curLine.substring(0, titleLevel);
// 标题内容部分
let titleContent = curLine.substring(titleLevel + 1);
// 先去除之前的编号
titleContent = removePreviousTitleNumber(titleContent);
// 标题等级递增
let titleIndex = titleLevel - 2;
titleNumber[titleIndex] += 1;
// 标题序号
let titleNumberStr = "";
switch (titleLevel) {
case 2:
titleNumberStr = `${titleNumber[0]}`;
break;
case 3:
titleNumberStr = `${titleNumber[0]}.${titleNumber[1]}`
break;
case 4:
titleNumberStr = `${titleNumber[0]}.${titleNumber[1]}.${titleNumber[2]}`
break;
case 5:
titleNumberStr = `${titleNumber[0]}.${titleNumber[1]}.${titleNumber[2]}.${titleNumber[3]}`
break;
case 6:
titleNumberStr = `${titleNumber[0]}.${titleNumber[1]}.${titleNumber[2]}.${titleNumber[3]}.${titleNumber[4]}`
break;
}
titleNumberStr += "、";
// 插入标题序号
titleContent = titleNumberStr + titleContent;
// 返回带序号的标题
curLine = titleLevelStr + " " + titleContent;
return curLine;
}

/**
* 去除之前标题的编号
* @param titleContent 标题内容
* @return 去除标题编号之后的标题内容
*/
function removePreviousTitleNumber(titleContent) {
// 寻找标题中的 、 字符
let index = titleContent.indexOf("、");
if (index > 0 && index < 10) {
// 之前已经进行过标号
return titleContent.substring(index + 1);
} else {
// 之前未进行过标号,直接返回
return titleContent;
}
}

/**
* 重新计算标题计数器的值
*
* @param titleLevel 当前行的标题等级
* @param titleNumber 标题计数器
*/
function reCalcTitleCounter(titleLevel, titleNumber) {
// 二级标题更新时,三级及三级以下的标题序号重置为 0
let startIndex = titleLevel - 1;
for (let i = startIndex; i < titleNumber.length; i++) {
titleNumber[i] = 0;
}
}

/**
* 保存MD文件
*
* @param destMdFilePath MD文件路径
* @param mdFileContent MD文件内容
*/
function saveMdContentToFile(destMdFilePath, mdFileContent) {
// 不保存空文件
if (mdFileContent == null || mdFileContent === "") {
return;
}
// 执行保存
fs.writeFile(destMdFilePath, mdFileContent, (err) => {
if (err) {
return console.log(err)
}
console.log('数据写入成功!')
})
}

function getNcpPath() {
return new Promise((resolve, reject) => {
ncp.paste((err, p) => {
if (err) {
reject(err)
} else {
if (typeof p === 'string') {
resolve(p)
}
}
})
})
}

(async () => {
const arguments = process.argv;
let mdPath = ''
// 可以使用循环迭代所有的命令行参数(包括node路径和文件路径)
// 命令行输入参数的情况
if (arguments.length >= 3) {
// 解决路径带空格的情况
for (let i = 2; i < arguments.length; i++) {
mdPath = mdPath.concat(arguments[i], ' ')
}
mdPath = mdPath.trim()
}

// 没有输入参数的情况,则去粘贴板寻找是否有文件路径
if (arguments.length < 3) {
mdPath = await getNcpPath()
}

let stat = null
try {
// 路径带空格,需要输入双引号
stat = fs.lstatSync(mdPath)
} catch (err) {
console.log('参数错误,文件不存在')
return
}
if (stat.isFile()) {
if (!mdPath.endsWith('.md')) {
console.log('参数错误,请输入md文件的路径')
return
}
// 执行标题自动编号
doTitleAutoNumbering(mdPath)
}
if (stat.isDirectory()) {
let dirFiles = fs.readdirSync(mdPath);
dirFiles.forEach((item) => {
let filePath = path.join(mdPath, item)
if (filePath.endsWith('.md')) {
// 执行标题自动编号
doTitleAutoNumbering(filePath)
}
})
}
})()

3、使用说明

3.1、环境

  1. 需要本机安装了 node js,最好配置了国内镜像源

  2. 代码拷贝到 js 文件中,执行以下命令:

    1
    2
    3
    4
    # 忘了是不是这个命令初始化了
    npm init
    # 安装依赖
    npm install

3.2、运行

3.2.1、命令行方式

3.2.1.1、传入文件参数
1
node autoNumMd.js ./test.md

传入文件参数

编号效果:

编号效果

3.2.1.2、传入文件夹参数
1
2
3
# 是文件夹的话,就将文件夹下的 md 文档全部编号
# 但是不会递归子文件夹
node autoNumMd.js C:\...\_posts

image-20221106101249420

3.2.1.3、无参数的情况
1
2
# 没有传入参数,则会去粘贴板查看是否有复制路径
node autoNumMd.js

3.2.2、bat 脚本

新建 .bat 文件,写入以下内容,注意 node Absolute path 需要替换为本机 node 的绝对路径,js 文件也是,输入文件夹、文件路径或者直接回车(读取粘贴板路径)。

1
2
3
4
5
6
7
8
setlocal EnableDelayedExpansion
set /p val=Please enter the .md file path or folder path:
echo %val%
if "%val%" == "" (
node Absolute path "autoNumMd.js Absolute path"
) else (
node Absolute path "autoNumMd.js Absolute path" %val%
)

4、md 编写说明

  1. 分级标题要连续,按照二三四五六来,不要二级标题后接四五六级标题
  2. # 后面要接空格

5、总结

  1. js 的异步编程把我真是烦透了,很多的操作都是要同步的。。。,异步转同步改了我好久阿,心累
  2. bat 脚本也画了挺长时间的,这语法太怪了
  3. 此文档的编号使用脚本生成
  • Copyrights © 2022-2023 hqz

请我喝杯咖啡吧~

支付宝
微信