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

结束

反向解析要详细讲比较繁琐,这是第一部分,预计分三章讲完。

相关文章
相关标签/搜索