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 (!preLevel || current > preLevel) { // 如果preLevel不存在,則代表當前層級比頂層更高,即 [2, 3, 1] 這種情況 // 如果preLevel比當前層級更高,即 [1, 3, 2] 這種情況
      prefixStack[idx]++; levelStack[idx] = current; } else { // 刪除棧尾,繼續遞歸
      levelStack.splice(idx, 1); prefixStack.splice(idx, 1); updateLevelList(levelStack, prefixStack, current); } } }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM