title: HTML逆向生成Markdown -- Part 1

HTML逆向生成Markdown -- Part 1

以前想作一個可以提取網頁中文章並轉換爲Markdown格式的Chrome插件,因此纔有了這個項目。 結果我低估瞭解析的難度,花了十幾天才作出來一個半成品。遂放棄,記錄下實現的思路,等之後水平提高了再來完善。javascript

解析過程分爲四個階段。如下是各個階段的簡要說明。html

  1. 分詞:將HTML原始文本分割爲HTML標籤
  2. 生成虛擬DOM節點:將分割後的HTML標籤轉換成對應的節點
  3. 構建虛擬DOM樹:將節點根據其順序生成相應的DOM樹
  4. 生成Markdown文本:根據預先定義HTML To Markdown的轉換規則,對DOM樹進行轉換。我參考的轉換規則

下面這段HTML文本將做爲解析的樣例文本:java

<h2 id="逆向解析HTMl">逆向解析HTMl</h2>
<p><a href="https://www.baidu.com" rel="nofollow" target="_blank">Markdown</a>解析過程分爲四個階段</p>
<ul>
<li>分詞</li>
<li>生成虛擬DOM節點</li>
<li>構建虛擬DOM樹</li>
<li><p>生成Markdown文本</p></li>
</ul>
複製代碼

分詞

咱們將源HTML文本按照HTML元素的語法,分解爲Opening tag Closing tag Enclosed text contentnode

由於HTML元素內部極可能還有嵌套的元素,因此還要繼續分割Enclosed text content直到只剩下文字文本數組

從上圖來看很明顯,Opening tagClosing tag都是由< >這兩個符號包裹的,那咱們只須要對願HTML文本進行一次搜索,將被< >包裹起來的字符串提取出來,放入一個數組中。搜索結束後數組就是咱們分詞的結果。 原始文本通過分割後,以下所示:markdown

const result = [
  '<h2 id="逆向解析HTMl">',
  '逆向解析HTMl',
  '</h2>',
  '<p>',
  '<a href="https://www.baidu.com" rel="nofollow" target="_blank">',
  'Markdown',
  '</a>',
  '解析過程分爲四個階段',
  '</p>',
  '<ul>',
  '<li>',
  '分詞',
  '</li>',
  '<li>',
  '生成虛擬DOM節點',
  '</li>',
  '<li>',
  '構建虛擬DOM樹',
  '</li>',
  '<li>',
  '<p>',
  '生成Markdown文本',
  '</p>',
  '</li>',
  '</ul>'
]
複製代碼

須要注意的是,html標籤中的屬性值是容許出現<>這兩個符號的,也就是說會出現相似<div data-demo="<demo>asd</demo>">這樣的文本。 這裏要注意的是不能直接從頭至尾搜索< >而後提取裏面的字符串,否則會出現提取到<div data-demo="<demo>這樣的結果。ide

我實現的方法比較簡單,是利用棧來判斷HTML標籤的開始和結束。
  1. 首先從下標0開始,遍歷字符串
    1. 若是當前的字符是<,則將其壓入棧中。
    2. 若是當前的字符是>,且棧頂是<,則表示一個HTML標籤的結束。 而後將開始符號<和結束符號>之間的字符串提取出來保存到結果數組就行了。
    3. 若是當前的字符是",且棧頂不是",則將其壓入棧中。
    4. 若是當前的字符是",且棧頂是",則將棧頂元素彈出。

具體實現見lexer.jsui

生成虛擬DOM節點

在這一階段,主要要對節點的屬性的過濾,HTML標籤內部的大部分屬性都是不須要的。除了a img等幾個HTML元素。 獲得分詞後的結果以後,就能夠解析HTML標籤字符串生成一個個包含HTML標籤信息的對象。 對象類型以下:spa

const obj = {
    // 固定屬性
    tag,            // HTML標籤名。如`div`, `span`
    type,           // 自定義的HTML標籤名所對應的數字。
    position,       // 標籤所在的位置。開始標籤(Opening tag):1,結束標籤(Closing tag):2,空元素(empty tag)和文本節點(text node):3
    // 可選屬性
    attr,           // 標籤內屬性的鍵值對,這是一個對象。一些須要保留屬性的元素如`a`元素須要保留`href` `title`用來生成Markdown文本。
    content         // 文本節點特有,用來保存文本
}
複製代碼

這一過程獲得的結果以下(有點多,這裏只截取前6個比較有表明性的):插件

const result = [
    {
        tag: 'h2',
        type: 42,           // 不要在乎`type`屬性,這是自定義的,42表明`h2`元素對應數字
        position: 1
    },
    {
        tag: 'textNode',
        type: 1,
        position: 3,
        content: '逆向解析HTMl'
    },
    {
        tag: 'h2',
        type: 42,
        position: 2
    },
    {
        tag: 'p',
        type: 6,
        position: 1
    },
    {
        tag: 'a',
        type: 2,
        position: 1,
        attr: {
            href: 'https://www.baidu.com'
        }
    },
    {
        tag: 'textNode',
        type: 1,
        position: 3,
        content: 'Markdown'
    },
]
複製代碼
這部分的實現思路也比較簡單,基本上都是字符串處理。
  1. tag:HTML標籤的結構很簡單,大體就如下幾種:(最後一種不須要處理,能夠忽略)

    1. <tagName attrKey="attrValue" attrKey> <tagName attrKey="attrValue" attrKey >
    2. <tagName/> <tagName />
    3. </tagName> (忽略)

    很容易發現要想獲得tagName只須要找到在<和(空格/)之間的字符串就能夠了。

  2. type:這個屬性是爲了方便以後的類型處理添加的,畢竟數字相對字符串來講更好處理。

    我在配置文件裏寫了一個映射表(配置文件),以tag做爲key對應數字做爲value。這樣就能很方便的對應起來。

  3. position:這個屬性雖然叫position,其實type才更適合它,由於它標識了開始標籤(Opening tag):1,結束標籤(Closing tag):2,空元素(empty tag)和文本節點(text node):3

    position的判斷我寫的比較簡單,只考慮到了上文tag所列的幾種狀況(但也已經能包括大部分狀況了)。從上面那幾種狀況來講。 只要判斷tag開始位置的下標索引是/不是1,就能知道是/不是Opening tag了。

    關於文本節點的判斷:文本節點是沒有tag的,若是沒法搜索到tag,就能夠將節點標識爲文本節點。

  4. attrattr裏面保存着解析成Markdown文本所須要的一些屬性。得益於Markdown語法的簡潔,HTML標籤大部分的屬性都是能夠忽略的。基本上只須要srctitlealtid這幾種,下面是相對應的語法:

    1. Markdown規範中的與連接有關的語法(Links Images Heading IDs Footnotes)。
      1. Linkssrc title
      2. Image: src title alt
      3. Heading IDsid
      4. Footnotesid
  5. content:是文本節點獨有的屬性,表示文本節點的內容。

具體實現見parser.js

結束

反向解析要詳細講比較繁瑣,這是第一部分,預計分三章講完。

相關文章
相關標籤/搜索