JavaScript 遍歷文檔生成目錄結構

一、需求描述

在 Word 中編輯文檔的時候,可以在視圖中打開導航窗格來查看目錄樹

類似的,現在需要基於頁面上的文章,渲染出一個這樣的目錄結構

 

在網頁上這些標題都是通過 <h1> 這樣的標籤渲染的,而且段落與標題之間是兄弟節點的關係

所以第一步只需要獲取到文章的根節點,然後遍歷 <h1> 這樣的兄弟節點,就能拿到初步的目錄結構

 

但有一種特殊情況需要考慮:

可能文章中的第一個標題並不是 h1,而是更低層級的標題,比如 h3,但在顯示上依然需要作爲一級標題來展示,因爲在 h3 之前沒有更大的標題

同樣的,在 h1 下面如果先出現了 h3,緊接着又出現了 h2,那麼先出現的 h3 實際上和後面的 h2 處於一個層級

也就是說類似這樣的結構:

<h3>標題3</h3>
<h4>標題4</h4>
<h1>標題1</h1>
<h2>標題2</h2>
<h1>標題1</h1>
<h4>標題4</h4>
<h3>標題3</h3>
<h2>標題2</h2>

需要展示爲:

 

 

二、程序設計

雖然頁面上的文章是一棵 DOM 樹,但由於標題元素是塊級元素,所以實際上需要處理的樹節點是平鋪的,只有一個層級

也就是說,不管是怎樣的文檔,最終都能處理成這樣的結構:

const article = [
  { tag: 'h3',content: '標題3' },
  { tag: 'p', content: '這裏是第一部分的內容' },
  { tag: 'h4', content: '標題4' },
  { tag: 'p', content: '這裏是第二部分的內容' },
  { tag: 'p', content: '上面說得很好,接下來再補充一點' },
  { tag: 'h1', content: '標題1' },
  { tag: 'h2', content: '標題2' },
  { tag: 'h1', content: '標題1' },
  { tag: 'p', content: '剛纔有一點忘記說了' },
  { tag: 'p', content: '我話講完,誰贊成,誰反對' },
  { tag: 'h4', content: '標題4' },
  { tag: 'h3', content: '標題3' },
  { tag: 'p', content: '不好意思,你剛纔說什麼我沒聽清' },
  { tag: 'h2', content: '標題2' },
  { tag: 'p', content: '現在我再問一次,誰贊成,誰反對' },
]

所以對於文檔本身,只需要做一次遍歷即可

但是對於文檔目錄,由於最終計算的是一個相對層級,所以也不太方便使用固定長度的數組來記錄層級

所以最終的解決方案是維護一個棧來記錄標題的層級關係

在一開始的時候,對於標題節點無論是幾級標題,都直接壓棧

後面每次處理標題,都和棧尾的標題進行比較,如果當前的標題層級更深,則壓入棧內,否則清除棧尾,並比較前一位標題

 

在處理標題層級的同時,還需要另外維護一個記錄前綴的棧,這兩個棧是映射關係

最終可以通過這兩個棧,得到目錄的完整文案,甚至是縮進量,所以出參可以這樣的結構:

const result = [
  { title: '1 標題', indent: 0 },
  { title: '1.1 標題', indent: 1 },
]

 

 

三、代碼實現

function getHeadingList(list) {
    if (!Array.isArray(list)) {
        return;
    }

    const reg = /h(\d)/; // 使用正則來匹配標題節點
    const levelStack = []; // 記錄標題層級
    const prefixStack = []; // 記錄前綴

    return list.reduce((res, node) => {
        const { tag, content } = node || {};
        const tagSplited = reg.exec(tag);
        if (!tagSplited) return res;

        updateLevelList(levelStack, prefixStack, Number(tagSplited[1]));

        res.push({
            title: `${prefixStack.join(".")} ${content}`,
            indent: prefixStack.length - 1,
        });
        return res;
    }, []);
}

function updateLevelList(levelStack, prefixStack, current) {
    const idx = levelStack.length - 1;
    const lastLevel = levelStack[idx];
    if (!lastLevel || current > lastLevel) {
        // 當前爲最深層級,壓入棧尾
        levelStack.push(current);
        prefixStack.push(1);
        return;
    }

    if (current === lastLevel) {
        // 層級相等時,只修改前綴
        prefixStack[idx]++;
    } else if (current < lastLevel) {
        // 當前層級更高,先和上一層級對比
        const preIndex = idx - 1;
        const preLevel = levelStack[preIndex];
        if (current > preLevel) {
            // 如果上一層級比當前層級更高,即 [1, 3, 2] 這種情況
            prefixStack[idx]++;
            levelStack[idx] = current;
        } else {
       // 刪除棧尾,繼續遞歸 levelStack.splice(idx,
1); prefixStack.splice(idx, 1); updateLevelList(levelStack, prefixStack, current); } } }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章