構文解析または日曜大工DOMの技術

こんにちは、Habr! 最近、マークダウンのような簡単なマークアップ言語を作成するというアイデアを思いつきました。これは、私のタスクに最適です。つまり、1つのキーボードのみを使用して、書式設定と数式を「オンザフライ」で挿入する機能を備えた講義をすばやく書くことです。 この形式で書かれたテキストを、たとえばLibreOffice Writerドキュメントなどのより理解しやすい形式に変換するには、パーサー 、つまりパーサーが必要です。 私は自転車を作ったので、「パーサーの例」、「htmlからDOM」、「htmlの解析方法」などのクエリを使用して検索エンジンに行きました。降下、またはflex、bison、llvm、yaccなどの既製のソリューションが使用されました。 厳密に定義された言語(gumbo、jsoup、rapidjson、Qtツールなど)を解析するためのライブラリがさらにありました。どちらも、標準ライブラリのみを使用してC ++で独自のマークアップパーサーを作成する計画の一部ではなかったため、電子リソースの代わりに、技術研究所のマニュアルが構文解析の技術に関する知識の源になりました。 テキストを取得し、そこから構築する方法(抽象構文ツリー)、プロセスで遭遇した落とし穴のいくつかについて、今日起こりうるエラーについて説明します。



すぐに予約してください-あなたの目標があなた自身のスクリプト言語またはさらに複雑なものである場合、この記事はその実装には十分ではありません。 理想的には、オートマトンと離散構造の理論を完全によく知る必要があります。 しかし、出発点として、当分の間、私は自分の経験に自分自身を制限することができます。 これは私が最初に意図したものではありませんが、例としては理想的です。 HTMLをシンプルで使い慣れた言語として解析します。



まず第一に、構文解析、または構文解析は、テキストをオブジェクトモデルに変換する完全なプロセスの同義語ではありません。 プロセス自体は2つの段階で構成されています。



  1. テキストのトークンへの字句解析は、特定の構文上の意味を持つこのテキストの小さな断片です。
  2. 解析は、 抽象構文ツリー (AST-抽象構文ツリー)またはドキュメントオブジェクトモデル (DOM-ドキュメントオブジェクトモデル)の値に基づいたトークンの構築です。


しかし、順番に見てみましょう。 お気に入りのIDEを開いてコードを書く前に、将来の言語の文法を開発する必要があります。 正式な文脈自由文法のうち、最も有名なものはバッカスナウア(BNF) 形式拡張バッカスナウア形式です。 私は彼らの共生を利用し、両方の形を最大限に活用しました。 次のように、他の式を介して任意の式を定義できます。



<> = <_1> <_> <_2>
      
      





ここで、1つの式は次々に続く3つの他の式によって定義されます。 同様に、これらは「3番目」の表現などでも表さなければなりません。



いつ停止しますか?



正式な文法の任意の言語の構文の記述は、2種類のトークンで構成されます: 端末非端末非終端記号は、定義する必要がある式です。



 <_1> = <> (<_> | <_>) <>
      
      





端末は自給自足型であり、定義する必要はありません。 上記の例は次のように書くことができます。



 <> = <_1> "+" <_2> <_1> = <> ("*" | "/") <>
      
      





ここで、「+」、「*」、「/」は端末です。

文法からすぐにターミナルを選択する必要があります。メイン定義の下部にある別のリストにターミナルを書くこともできます。ターミナルは後で便利になります。



BNFの詳細な説明は、Wikipediaのこちらこちらでご覧いただけます 。 言語の文法をコンパイルすることは、軽薄さを許容しない言語を作成する重要な段階です。 その中の1つの誤りは、完全に動作不能なコードにつながる可能性があり、それはゼロから書き直さなければなりません。 したがって、次の手順を実行する前に、コンパイル済みの文法に問題となる問題がないことを確認してください。 モニターが2台ある場合は、残りの作業のために1つのモニターを文法文書で占有しておくと便利です。これにより、コーディング時に目を素早く動かすことができます。 私を信じて、あなたはいつもこれをしなければなりません。 これが私のコンパイル済み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)には、入力ストリームから1文字を読み取り、この文字を処理するプロセス(const char&c)プロシージャに送信するループを含める必要があります。 プロセス手順には、現在のトークンのタイプに応じて、各キーシンボルに独自の機能があるswitch©を含める必要があるようです。 実際、逆のことが当てはまります。スイッチを使用してトークンのタイプを確認し、文字の関数を定義する方が適切です。 さらに、現在のトークンは多くの場合、不定のタイプを持っています。 たとえば、山かっこを開くと、次のように表示されます。開始、終了、空のタグ、HTMLスタイルのコメントまたはマクロタグ(「<?...?>」で囲まれたPHPスクリプト。このようなすべてのユニオンには、独自のケースが必要です。実装しますか?ビットフラグを使用します。有限数のタイプのトークンを指定します(字句解析プログラムのタスクは構文にできる限り少ない作業を残すことであるため、より良いです)。各タイプに対して、次数2の一意の数が指定されます(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; } } }
      
      





スイッチで予想されるトークンのタイプを確認し、各ケース内で各主要端末の手順を決定します。 多くの機能はありません。誰もが簡単なアクションを実行します。バッファに文字を追加するか、バッファを次のトークンにダンプするか、予想されるトークンのタイプを変更するか、例外をスローします。 検索可能なテキストエディタを使用して、上記の文法で目的の手順を決定するのは簡単です。 他の式の定義に予想されるトークン(トークン)がすべて含まれていることを確認してから、これらの式を「3番目」に含めるなどしてください。 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ドキュメントテンプレートの1つにコメントを付け、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="" /> <!--[if lt IE 9]> <script src="http://html5shiv.googlecode.com/svn/trunk/html5-els.js"></script> <![endif]--> </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]
 [「charset」: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]
 ["telephone = no":DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 ["
	 ":TEXT]
 [「メタ」:EMPTY_TAG_NAME]
 ["http-equiv":ATTRIBUTE_NAME]
 [「x-rim-auto-match」:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 [「コンテンツ」:ATTRIBUTE_NAME]
 ["telephone = none":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]
 ["[if lt 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]
 [「nav」:OPENING_BLOCK_TAG_NAME]
 ["
		 ":TEXT]
 [「ul」:OPENING_BLOCK_TAG_NAME]
 [「id」:ATTRIBUTE_NAME]
 [「nav」:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 ["
			 ":TEXT]
 [「li」:OPENING_BLOCK_TAG_NAME]
 [「クラス」:ATTRIBUTE_NAME]
 [「nav」: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]
 [「nav」:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 [「a」:OPENING_BLOCK_TAG_NAME]
 [「href」:ATTRIBUTE_NAME]
 ["#":DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 [「レビュー」:テキスト]
 [「a」:CLOSING_BLOCK_TAG_NAME]
 [「li」:CLOSING_BLOCK_TAG_NAME]
 ["
			 ":TEXT]
 [「li」:OPENING_BLOCK_TAG_NAME]
 [「クラス」:ATTRIBUTE_NAME]
 [「nav」:DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 [「a」:OPENING_BLOCK_TAG_NAME]
 [「href」:ATTRIBUTE_NAME]
 ["":DOUBLE_QUOTED_ATTRIBUTE_VALUE]
 [「ヘルプ」:テキスト]
 [「a」:CLOSING_BLOCK_TAG_NAME]
 [「li」:CLOSING_BLOCK_TAG_NAME]
 ["
		 ":TEXT]
 [「ul」:CLOSING_BLOCK_TAG_NAME]
 ["
	 ":TEXT]
 [「nav」: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。無断複写・転載を禁じます。」  :テキスト]
 [「小」:​​CLOSING_BLOCK_TAG_NAME]
 ["
	 ":TEXT]
 [「フッター」:CLOSING_BLOCK_TAG_NAME]
 ["
 ":TEXT]
 ["body":CLOSING_BLOCK_TAG_NAME]
 ["

 ":TEXT]
 ["html":CLOSING_BLOCK_TAG_NAME]
 ["
 ":TEXT]
 ["":END]






これで、2番目の部分である構文ツリーの構築を開始する準備ができました。 タグには属性があるため、ツリーノードには、他のノードとの通信に加えて、キーと値のペアの配列が含まれます。 結果として得られる構造は、記事のタイトルで言及されているDOMドキュメントのオブジェクトモデルと正しく呼ぶことができます。



HTML要素のすべてのプロパティを実装するには、いくつのクラスが必要ですか?



理想的には、カスケードスタイルシートを定義できるように要素ごとに1つのクラスがありますが、空の「Node」タグ、継承された「Block」ブロック(2つのペアタグで囲まれたコンテンツ)、およびルートツリーのルートを持つ彼。 また、パーサーで<p>、<li>、<strong>などのテキストを含む可能性のあるタグの配列を定義して、配置されていないテキストのトークンを取り除きます。 今では小さなものです。 字句アナライザーでうまく機能している場合、構文上のタスクは、トークンを吸収し、開いているノードで3つの操作のいずれかを実行することです:空のノードを追加する、新しいノードを開く、または親にポインターを返すことによって自分で閉じる 後者では、ベースノードから始まるすべてのクラスに、要素の作成時に取得したポインターが含まれている必要があります。 このプロセスは、 トップダウン解析と呼ばれます



解析手順:



 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); } }
      
      





以上です! すべてを正しく行った場合、結果のツリーを表示できます。



 | 
 +-<ROOT>
   | 
   +-<!DOCTYPE>
   | 
   +-<html>
     | 
     +-<head>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<メタ>
     |  | 
     |  +-<タイトル>
     |  | 
     |  +-<リンク>
     |  | 
     |  +-<リンク>
     |  | 
     |  +-<コメント>
     | 
     +-<body>
       | 
       +-<ヘッダー>
       |  | 
       |  +-<div>
       | 
       +-<nav>
       |  | 
       |  +-<ul>
       |  | 
       |  +-<li>
       |  |  | 
       |  |  +-<a>
       |  | 
       |  +-<li>
       |  |  | 
       |  |  +-<a>
       |  | 
       |  +-<li>
       |  | 
       |  +-<a>
       | 
       +-<メイン>
       |  | 
       |  +-<マクロ>
       | 
       +-<フッター>
         | 
         +-<hr>
         | 
         +-<小>




ただし、結果のツリーは実際にはDOMと呼ばれますが、パーサーは完全なjQuery、Jsoup、beautifulsoup、またはGumboとはほど遠いです。特に、ペアの<style>タグと<script>タグの間にあるテキストを正しく処理できないため、ソース持ってくるまで しかし、ハブロフスクの住民がそのような願望を表明するならば、私は間違いなく付け加えます。 成功。



PSパブリックアクセスのソースコードを埋めました。 私見、生なので、完全なライブラリを計画します。



PSS 2番目の部分。



All Articles