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