.md 文檔自動編号 js 腳本
1、契機
在使用 typora 的時候,沒有自動編号,每一級标題要手動編号,比較累。之前有找過修改主題的 css 來實作自動編号的,但是隻是個樣式而已,沒有真正的編号,而且導出的 pdf 中是沒有編号的。這次找的腳本是直接修改 md 文檔的,根據行首的
#
來判斷是不是标題。
md 文檔一共有六級标題,不對一級标題編号,二級、三級、四級、五級、六級标題進行級聯編号。
根據這篇文章的 java 代碼修改而來,并做了一點小小的改進,用 java 感覺有點重,最近喜歡用 js 寫點腳本,比如一些字元串處理的,挺友善的。
文章連結:
https://blog.csdn.net/oneby1314/article/details/107311743
2、腳本代碼
js 代碼如下:
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、環境
- 需要本機安裝了 node js,最好配置了國内鏡像源
- 代碼拷貝到 js 檔案中,執行以下指令:
# 忘了是不是這個指令初始化了
npm init
# 安裝依賴
npm install
3.2、運作
3.2.1、指令行方式
3.2.1.1、傳入檔案參數
node autoNumMd.js ./test.md

編号效果:
3.2.1.2、傳入檔案夾參數
# 是檔案夾的話,就将檔案夾下的 md 文檔全部編号
# 但是不會遞歸子檔案夾
node autoNumMd.js C:\...\_posts
3.2.1.3、無參數的情況
# 沒有傳入參數,則會去粘貼闆檢視是否有複制路徑
node autoNumMd.js
3.2.2、bat 腳本
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 編寫說明
- 分級标題要連續,按照二三四五六來,不要二級标題後接四五六級标題
- # 後面要接空格
5、總結
- js 的異步程式設計把我真是煩透了,很多的操作都是要同步的。。。,異步轉同步改了我好久阿,心累
- bat 腳本也畫了挺長時間的,這文法太怪了
- 此文檔的編号使用腳本生成