天天看點

.md 文檔自動編号 js 腳本

.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、環境

  1. 需要本機安裝了 node js,最好配置了國内鏡像源
  2. 代碼拷貝到 js 檔案中,執行以下指令:
# 忘了是不是這個指令初始化了
npm init
# 安裝依賴
npm install      

3.2、運作

3.2.1、指令行方式

3.2.1.1、傳入檔案參數

node autoNumMd.js ./test.md      
.md 文檔自動編号 js 腳本

編号效果:

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 編寫說明

  1. 分級标題要連續,按照二三四五六來,不要二級标題後接四五六級标題
  2. # 後面要接空格

5、總結

  1. js 的異步程式設計把我真是煩透了,很多的操作都是要同步的。。。,異步轉同步改了我好久阿,心累
  2. bat 腳本也畫了挺長時間的,這文法太怪了
  3. 此文檔的編号使用腳本生成