哈Ha! 最近,我想到了创建一种简单的标记语言(如markdown)的想法,该语言非常适合我的任务,即快速编写具有格式的讲义以及仅使用一个键盘即可“即时”插入数学公式的功能。 要将以这种格式编写的文本转换成更易于理解的形式(例如LibreOffice Writer文档),您需要
一个解析器 ,换句话说就是
解析器 。 由于我以前经常骑自行车,所以我去搜索引擎查询“解析器示例”,“ html转换为DOM”,“如何解析html”等。令我失望的是,在所有找到的资源上,要么是基本示例(例如带有递归的Straustrup计算器)或使用现成的解决方案,例如flex,bison,llvm和yacc。 还有更多的库用于解析严格定义的语言(gumbo,jsoup,rapidjson,Qt工具等)。我都不打算仅使用标准库来用C ++编写自己的标记解析器,而是我的计划的一部分。技术学院的手册代替了电子资源,而成为了解析技术知识的来源。 关于如何获取文本并从其构建AST(抽象语法树),关于我在此过程中遇到的一些陷阱,我将告诉您今天可能出现的错误。
我将立即进行保留-如果您的目标是使用自己的脚本语言甚至更复杂,那么本文不足以实现。 理想情况下,您需要完全了解自动机和离散结构的理论。 但暂时,我只能将自己的经验限制在自己的经验上,我会慷慨地分享自己的经验。 这与我最初的意图不完全相同,但是它是一个理想的示例。 我们将把HTML解析为一种简单而熟悉的语言。
首先,解析不是将文本转换为对象模型的完整过程的同义词。 该过程本身包括两个阶段:
- 对文本进行标记的词法分析只是其中的一小部分,具有一定的句法含义。
- 解析是根据令牌的抽象语法树 (AST-抽象语法树)或文档对象模型 (DOM-文档对象模型)的值来构造令牌。
但是让我们按顺序进行。 在打开您喜欢的IDE并编写代码之前,您需要开发一种未来语言的语法。 在正式的无上下文语法中,最著名的是
Backus-Naur(BNF) 形式和
扩展的Backus-Naur形式 。 我利用了它们的共生,同时兼顾了两种形式。 可以通过其他表达式定义任何表达式,如下所示:
<> = <_1> <_> <_2>
在这里,一个表达式是通过三个其他表达式接一个定义的。 反过来,它们也必须通过“第三”表达式等表示。
什么时候停止?
形式语法中任何语言的语法描述都包含两种类型的标记:
terminal和
non-terminals 。
非终结符是需要定义以下内容的表达式:
<_1> = <> (<_> | <_>) <>
终端是自给自足的,不需要定义它们。 上面的例子可以这样写:
<> = <_1> "+" <_2> <_1> = <> ("*" | "/") <>
其中“ +”,“ *”,“ /”是端子。
您需要立即从语法中选择终端,甚至可以将它们写在主要定义底部的单独列表中-稍后将派上用场。
有关BNF的完整说明,请参见Wikipedia
此处和
此处 。 编写语言语法是创建不容忍轻浮的语言的重要阶段。 其中一个错误可能导致代码完全失效,必须重新编写代码。 因此,在进行下一步之前,请确保编译后的语法中没有争议性的问题。 如果您有两个监视器,那么在剩下的工作中,用一个语法文件占用一个监视器将很方便,这样您在编写代码时就可以很快将视线移到它上面。 相信我,您必须一直这样做。 这是我编译的HTML5 BNF语法:
(* document *) <document> = {<document_part>} <END> <document_part> = <block> | <empty_tag> | <comment> | <macro_tag> | <text> <block> = <opening_tag> {<document_part>} <closing_tag> (* tags *) <opening_tag> = "<" {<ws>} <block_tag_name> [<attributes_list>] {<ws>} ">" <closing_tag> = "<" "/" {<ws>} <block_tag_name> {<ws>} ">" <empty_tag> = "<" "!" {<ws>} <empty_tag_name> [<attributes_list] {<ws>} ["/"] ">" <comment> = "<" "!" "--" <comment_text> "--" ">" <macro_tag> = "<" "?" <macro_text> "?" ">" <block_tag_name> = "a" | "abbr" | "address" | "article" | "aside" | "audio" | "b" | "bdo" | "blockquote" | "body" | "button" | "canvas" | "caption" | "cite" | "code" | "colgroup" | "data" | "datalist" | "dd" | "del" | "details" | "dfn" | "dialog" | "div" | "dl" | "dt" | "em" | "fieldset" | "figcaption" | "figure" | "footer" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "header" | "html" | "i" | "iframe" | "ins" | "kbd" | "label" | "legend" | "li" | "main" | "map" | "mark" | "meter" | "nav" | "noscript" | "object" | "ol" | "optgroup" | "option" | "output" | "p" | "picture" | "pre" | "progress" | "q" | "ruby" | "rb" | "rt" | "rtc" | "rp" | "s" | "samp" | "script" | "section" | "select" | "small" | "span" | "strong" | "style" | "sub" | "summary" | "sup" | "table" | "tbody" | "td" | "template" | "textarea" | "tfoot" | "th" | "thead" | "time" | "title" | "tr" | "track" | "u" | "ul" | "var" | "video" <empty_tag_name> = "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "menuitem" | "meta" | "param" | "source" | "track" | "wbr" (* attributes *) <attributes_list> = <ws> {<ws>} <attribute> {<ws> {<ws>} <attribute>} <attribute> = <empty_attribute> | <unquoted_attribute> | <single_quoted_attribute> | <double_quoted_attribute> <empty_attribute> = <attribute_name> <unquoted_attribute> = <attribute_name> {<ws>} "=" {<ws>} <unquoted_attribute_value> <single_quoted_attribute> = <attribute_name> {<ws>} "=" {<ws>} "'" <single_quoted_attribute_value> "'" <double_quoted_attribute> = <attribute_name> {<ws>} "=" {<ws>} "\"" <double_quoted_attribute_value> "\"" <attribute_name> = (<letter> | <digit>) {<letter> | <digit>} {* attribute values *) <unquoted_attribute_value> = /^[\s"'=<>/]/ {/^[\s"'=<>/]/} <single_quoted_attribute_value> = /^[']/ {/^[']/} <double_quoted_attribute_value> = /^["]/ {/^["]/} (* nonterminals *) <text> = {/^[<>]/} <comment_text> = ... <macro_text> = ... <letter> = /[a-zA-Z]/ <digit> = /[0-9]/ <ws> = " " | "\t" | "\n" (* terminals *) "<", ">", "/", "!", "?", " ", "\t", "\n"
语法准备就绪后,您可以继续进行词法分析器(词法分析器的另一个名称,因为它除了进行分析外,还可以识别文档中的词法错误)。 乍看之下,一切都很简单:吸收字符,写入缓冲区,并在检测到密钥终端时将接收到的令牌确定为具有某种类型的令牌,对吗? 是的,只有令牌的类型比符号更重要。 我现在解释。 当然,反汇编(ifsteam和file)过程应该包含一个循环,该循环从输入流中读取一个字符,并将其发送到处理该字符的进程(const char&c)过程。 似乎该处理过程需要包含开关©,其中每个键符号都有其自己的功能,具体取决于当前令牌的类型。 实际上,情况恰恰相反:最好使用开关来检查令牌的类型并定义字符的功能。 此外,当前令牌通常具有不确定类型,是许多类型之一。 例如,打开尖括号后,您会看到:打开,关闭,空标签以及HTML样式的注释或宏标签(PHP脚本包含在“ <?...?>”中。对于所有此类联合,您都需要自己使用大小写)。使用位标志。给出有限数量的令牌类型(越好越好,因为词法分析器的任务是使语法工作尽可能少)。对于每种类型,给出唯一的二阶数(1、2、4、8等),然后以二进制格式显示如下:0001,0010,0 100等,并且按位添加任何类型的任何数字,将获得唯一的数字。
enum Token_type { END = 1, TEXT = 2, OPENING_BLOCK_TAG_NAME = 4, CLOSING_BLOCK_TAG_NAME = 8, EMPTY_TAG_NAME = 16, COMMENT = 32, MACRO_TAG = 64, ATTRIBUTE_NAME = 128, UNQUOTED_ATTRIBUTE_VALUE = 256, SINGLE_QUOTED_ATTRIBUTE_VALUE = 512, DOUBLE_QUOTED_ATTRIBUTE_VALUE = 1024 };
截断的程序过程:
void Lexer::process (const char &c) { switch (curr_token_type) { case END: { throw string("unexpected ending!"); break; } case TEXT: { if (c == '>') throw string("unexpected symbol: \">\"!"); else if (c == '<') { if (!buffer.empty()) { add(buffer, TEXT); buffer.clear(); } curr_token_type = OPENING_BLOCK_TAG_NAME | CLOSING_BLOCK_TAG_NAME | EMPTY_TAG_NAME | COMMENT | MACRO_TAG; } else buffer.push_back(c); break; } case OPENING_BLOCK_TAG_NAME: { throw string("error!"); break; } case CLOSING_BLOCK_TAG_NAME: { if (c == '<') throw string("unexpected symbol: \"<\"!"); else if (c == '/') throw string("unexpected symbol: \"<\"!"); else if (c == '!') throw string("unexpected symbol: \"!\"!"); else if (c == '?') throw string("unexpected symbol: \"?\"!"); else if (c == ' ') throw string("unexpected symbol: \" \"!"); else if (c == '\t') throw string("unexpected symbol: \"\\t\"!"); else if (c == '\n') throw string("unexpected symbol: \"\\n\"!"); else if (c == '>') { for (unsigned int i(0); i < BLOCK_TAGS_COUNT; i++) if (buffer == block_tags[i]) { add(buffer, CLOSING_BLOCK_TAG_NAME); buffer.clear(); curr_token_type = TEXT; break; } } else buffer.push_back(c); break; } case EMPTY_TAG_NAME: { throw string("error!"); break; } case COMMENT: { ... break; } case MACRO_TAG: { ... break; } case OPENING_BLOCK_TAG_NAME | CLOSING_BLOCK_TAG_NAME | EMPTY_TAG_NAME | COMMENT | MACRO_TAG: { ... break; } case EMPTY_TAG_NAME | COMMENT: { ... break; } case ATTRIBUTE_NAME: { ... break; } case ATTRIBUTE_NAME | UNQUOTED_ATTRIBUTE_VALUE | SINGLE_QUOTED_ATTRIBUTE_VALUE | DOUBLE_QUOTED_ATTRIBUTE_VALUE: { ... break; } case UNQUOTED_ATTRIBUTE_VALUE | SINGLE_QUOTED_ATTRIBUTE_VALUE | DOUBLE_QUOTED_ATTRIBUTE_VALUE: { ... break; } case UNQUOTED_ATTRIBUTE_VALUE: { ... break; } case SINGLE_QUOTED_ATTRIBUTE_VALUE: { ... break; } case DOUBLE_QUOTED_ATTRIBUTE_VALUE: { ... break; } } }
我们用开关检查期望令牌(或令牌)的类型,并在每种情况下确定每个关键终端的程序。 功能并不多,每个人都执行简单的操作:将字符添加到缓冲区中,或将缓冲区转储到下一个令牌中,或更改所需的令牌类型,或引发异常。 使用可搜索的文本编辑器使用上面编写的语法很容易确定所需的过程。 只需在其他表达式的定义中查找期望标记(标记)的所有包含物,然后在“第三”中包含这些表达式,等等。 这是gedit文本编辑器中的开始标记的示例:

首先,导航语法很困难,但是随着时间的流逝和经验的积累,它变得比划分列更复杂。 这是反汇编程序:
void Lexer::disassemble (ifstream &file) { tokens_count = 0; curr_token_type = 0; unsigned long line(1), pos(1); try { char c; curr_token_type = TEXT; while ((c = file.get()) != EOF) { if (c == '\n') { pos = 1; line++; } else pos++; process(c); } if (buffer.size() != 0) { if (!(curr_token_type | TEXT)) throw string("text was expected!"); add(buffer, TEXT); buffer.clear(); } add("", END); } catch (const string &error) { throw string("lexer: " + to_string(line) + "," + to_string(pos) + ": " + error); } }
将类型设置为TEXT显然是第一个预期的标记,然后在结尾添加END类型的标记以及任何文本(或为空,如此处所示)。
例如,我拿了一个带注释的HTML文档模板,在其中添加了一个伪PHP脚本,使用词法分析器对其进行了处理,并以“ [“ <token_text>”:<token_type>]“的格式显示了令牌列表。 这是发生了什么:
文件本身 <!DOCTYPE html> <html lang="ru"> <head> <meta http-equiv="content-type" content="text/html" charset="utf-8" /> <meta name="author" content="Interquadro" /> <meta name="description" content="" /> <meta name="keywords" content=""> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="format-detection" content="telephone=no" /> <meta http-equiv="x-rim-auto-match" content="telephone=none" /> <meta name="referrer" content="no-referrer" /> <meta name="_suburl" content="" /> <title></title> <link rel="shortcut icon" href=".ico" /> <link rel="stylesheet" type="text/css" href=".css" title="" /> </head> <body> <header> <div id="intro"> </div> </header> <nav> <ul id="nav"> <li class="nav"><a href="#"> </a></li> <li class="nav"><a href="#"> </a></li> <li class="nav"><a href=""> </a></li> </ul> </nav> <main id="content"> <?php ?> </main> <footer> <hr /> <small id="copyright">Copyright © 2019. .</small> </footer> </body> </html>
代币列表 [“!DOCTYPE”:EMPTY_TAG_NAME]
[“ html”:ATTRIBUTE_NAME]
[“
“:TEXT]
[“ html”:OPENING_BLOCK_TAG_NAME]
[“ lang”:ATTRIBUTE_NAME]
[“ en”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“头”:OPENING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“ http-equiv”:ATTRIBUTE_NAME]
[“内容类型”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“ text / html”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“字符集”:ATTRIBUTE_NAME]
[“ utf-8”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“作者”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“ Interquadro”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“说明”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“关键字”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“视口”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“宽度=设备宽度,初始比例= 1”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“格式检测”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“电话=否”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“ http-equiv”:ATTRIBUTE_NAME]
[“ x-rim-auto-match”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“电话=无”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“引荐来源”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“无推荐人”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“元”:EMPTY_TAG_NAME]
[“名称”:ATTRIBUTE_NAME]
[“ _suburl”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“内容”:ATTRIBUTE_NAME]
[“”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“标题”:OPENING_BLOCK_TAG_NAME]
[“标题”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“链接”:EMPTY_TAG_NAME]
[“ rel”:ATTRIBUTE_NAME]
[“快捷方式图标”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“ href”:ATTRIBUTE_NAME]
[“ .ico”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“链接”:EMPTY_TAG_NAME]
[“ rel”:ATTRIBUTE_NAME]
[“样式表”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“类型”:ATTRIBUTE_NAME]
[“ text / css”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“ href”:ATTRIBUTE_NAME]
[“ .css”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“标题”:ATTRIBUTE_NAME]
[“”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“ [如果是IE 9]>
<script src =“ http://html5shiv.googlecode.com/svn/trunk/html5-els.js”> </ script>
<![endif]“:评论
[“
“:TEXT]
[“头”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ body”:OPENING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“标题”:OPENING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ div”:OPENING_BLOCK_TAG_NAME]
[“ id”:ATTRIBUTE_NAME]
[“简介”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“ div”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“标题”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“导航”:OPENING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ ul”:OPENING_BLOCK_TAG_NAME]
[“ id”:ATTRIBUTE_NAME]
[“导航”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“ li”:OPENING_BLOCK_TAG_NAME]
[“课程”:ATTRIBUTE_NAME]
[“导航”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“ a”:OPENING_BLOCK_TAG_NAME]
[“ href”:ATTRIBUTE_NAME]
[“#”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“首页”:TEXT]
[“ a”:CLOSING_BLOCK_TAG_NAME]
[“ li”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ li”:OPENING_BLOCK_TAG_NAME]
[“课程”:ATTRIBUTE_NAME]
[“导航”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“ a”:OPENING_BLOCK_TAG_NAME]
[“ href”:ATTRIBUTE_NAME]
[“#”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“评论”:TEXT]
[“ a”:CLOSING_BLOCK_TAG_NAME]
[“ li”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ li”:OPENING_BLOCK_TAG_NAME]
[“课程”:ATTRIBUTE_NAME]
[“导航”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“ a”:OPENING_BLOCK_TAG_NAME]
[“ href”:ATTRIBUTE_NAME]
[“”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“帮助”:TEXT]
[“ a”:CLOSING_BLOCK_TAG_NAME]
[“ li”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ ul”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“导航”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“主要”:OPENING_BLOCK_TAG_NAME]
[“ id”:ATTRIBUTE_NAME]
[“内容”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“
“:TEXT]
[“ php”:MACRO_TAG]
[“
“:TEXT]
[“主要”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“页脚”:OPENING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ hr”:EMPTY_TAG_NAME]
[“
“:TEXT]
[“小”:OPENING_BLOCK_TAG_NAME]
[“ id”:ATTRIBUTE_NAME]
[“版权”:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
[“版权所有©2019。保留所有权利。” :TEXT]
[“小”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“页脚”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ body”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“ html”:CLOSING_BLOCK_TAG_NAME]
[“
“:TEXT]
[“”:END]
现在我们准备开始第二部分-语法树的构建。 由于我们的标签具有属性,因此树节点除与其他节点通信外,还将包含键值对数组。 可以将最终的构造称为本文标题中提到的DOM文档的对象模型。
您需要多少个类来实现HTML元素的所有属性?
理想情况下,每个元素都有一个类,以便可以为它们定义级联样式表,但是我们将自己限制为三个-一个空的“ Node”标签,一个继承的“ Block”块(内容包含在两个成对标签之间)以及从他与根树的根。 我们还在解析器中定义了一个标签数组,其中可能包含文本,例如<p>,<li>,<strong>等,以清除带有未放置文本的标记。 现在由小决定。 如果您在词法分析器上工作得很好,那么语法上的任务就是吸收标记并在打开的节点中执行以下三种操作之一:向其添加一个空节点,打开一个新节点或通过将指针返回给父节点自己关闭它。 对于后者,要求所有类(从基本Node开始)都包含在创建元素时获得的此类指针。 此过程称为自
顶向下解析 。
解析过程:
void Parser::parse (const Lexer &lexer) { Block * open_block = (Block*) tree; Node * last_node = (Node*) tree; try { unsigned long long size = lexer.count(); for (unsigned long long i(0); i < size-2; i++) { switch (lexer[i].type) { case Lexer::TEXT: { for (unsigned int j(0); j < TEXT_TAGS_COUNT; j++) if (open_block->get_name() == text_tags[j]) last_node = open_block->add("TEXT", lexer[i].lexeme); break; } case Lexer::OPENING_BLOCK_TAG_NAME: { last_node = open_block = open_block->open(lexer[i].lexeme); break; } case Lexer::CLOSING_BLOCK_TAG_NAME: { if (lexer[i].lexeme != open_block->get_name()) throw string("unexpected closing tag: </" + lexer[i].lexeme + ">"); open_block = open_block->close(); break; } case Lexer::EMPTY_TAG_NAME: { last_node = open_block->add(lexer[i].lexeme); break; } case Lexer::COMMENT: { last_node = open_block->add("COMMENT", lexer[i].lexeme); break; } case Lexer::MACRO_TAG: { last_node = open_block->add("MACRO", lexer[i].lexeme); break; } case Lexer::ATTRIBUTE_NAME: { last_node->add_attr(lexer[i].lexeme, lexer[i].lexeme); break; } case Lexer::UNQUOTED_ATTRIBUTE_VALUE: { last_node->set_last_attr(lexer[i].lexeme); break; } case Lexer::SINGLE_QUOTED_ATTRIBUTE_VALUE: { last_node->set_last_attr(lexer[i].lexeme); break; } case Lexer::DOUBLE_QUOTED_ATTRIBUTE_VALUE: { last_node->set_last_attr(lexer[i].lexeme); break; } case Lexer::END: { if (open_block->get_type() != Node::ROOT) throw string("unexpected ending!"); open_block->close(); } } } } catch (const string &error) { throw string("parser: " + error); } }
仅此而已! 如果正确完成所有操作,则可以显示结果树:
|
+-<根目录>
|
+-<!DOCTYPE>
|
+-<html>
|
+-<头>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<元>
| |
| +-<标题>
| |
| +-<链接>
| |
| +-<链接>
| |
| +-<评论>
|
+-<body>
|
+-<标题>
| |
| +-<div>
|
+-<导航
| |
| +-<ul>
| |
| +-<li>
| | |
| | +-<a>
| |
| +-<li>
| | |
| | +-<a>
| |
| +-<li>
| |
| +-<a>
|
+-<主要>
| |
| +-<宏>
|
+-<页脚>
|
+-<hr>
|
+-<小>
但是,尽管生成的树实际上可以称为DOM,但我们的解析器与完整的jQuery,Jsoup,beautifulsoup或Gumbo相比还很遥远,特别是因为它无法正确处理位于成对的<style>和<script>标记之间的文本,因此也无法获取源代码。直到我带来它。 但是,如果哈布罗夫斯克居民表达了这样的愿望,我肯定会补充。 成功。
PS在公共访问中填充了
源代码 。 恕我直言,原始的,所以我打算计划一个完整的图书馆。
PSS
第二部分。