一、需求描述
在 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); } } }