0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

簡単な HTMLParser のソースコードを読んでみる [Javascript]

Posted at

HTML を極めるって、どういうことなんですかね?
勉強で、簡単な HTML Parser を読んでみていたので、そのメモ。
間違いなどありましたら、ご指摘お願いします。

HTML パーサーとは?

HTML ドキュメント は、そのままの形式では描画されません。描画されるには、一度 HTML ドキュメントを機械が読み込める形式に変えてから、それをレンダリングエンジンというプログラムに通すことで、描画が行われます。
ここでは、「HTML ドキュメントを機械が読み込める形式に変える」部分を簡単なコードに落とした例があったので、そのソースコードを読んでみます (ファイルはこの1ファイルだけです)。また、このコードを書いた Googler は preact という 軽量版の react を書いた人でもあります。

ソースコード

では、早速読んでみましょう。
このコードでは継承などが多いので、まずは、そのデータ型の整理からしてみましょう。
まず、78行目から80行目にある

	function HTMLElement() {
		this.childNodes = [];
	}

が、基本となる HTMLElement の型です。
81行目からある extend の部分 (コードの12行目見れば分かるように上書きしている) で、prototype 部分に、textContent (要素に含まれる文字列) や getElementsByTagName・getElementById や getAttribute・setAttribute などが加わります。

次に、Document という最初の親要素になる要素です。

	function Document(){
		HTMLElement.call(this);
	}

これも、HTMLElement を継承し、{ nodeType : 9, nodeName: '#document'} で上書きされています。

次は、Node という HTMLElement の中身などを表すデータ型です。

	function Node(){}
	util.extend(Node.prototype, {
		toString : function(){ return this.textContent; }
	});

prototype の toString で、代わりに textContent という内容を返すようにしています。
この Node は、TextNode, CommentNode, CDATASectionNode の3つの型で継承されています。

では、データ型も分かったので、パースしている部分を読んでみましょう。
パースをする関数が始まっているのは 189行目になっていますが、216行目の部分の、

		while ( (token=domParserTokenizer.exec(str)) ) {

から、実際に 解析を行なっています。
この正規表現の domParserTokenizer を読んでみると、

		domParserTokenizer = /(?:<(\/?)([a-zA-Z][a-zA-Z0-9\:]*)(?:\s([^>]*?))?((?:\s*\/)?)>|(<\!\-\-)([\s\S]*?)(\-\->)|(<\!\[CDATA\[)([\s\S]*?)(\]\]>))/gm;

のようになっていて、グループ分けした配列にするとようは、["マッチした文字列", "/があるかどうか", "タグの名前", "Attribute", "/があるかどうか", "コメントアウト始まり", "コメントアウト内容", "コメントアウト終わり", "CDATA始まり", "CDATA始まり", "CDATA終わり"] にマッチします。
ここでマッチした内容が、while文 で繰り返し処理されています。

217行目からの部分は、コメントアウト・CDATA 用のもので、前に出た CommentNode か CDATASectionNode の textContent などを上書きしています。
上書きができたら、text があったら parent.ChildrenNode に追加し 、tags に タグ内容を push して、parentNode の ChildrenNode に追加します。

次は、コメントアウトではない場合かつ タグの始めに "/" が含まれない場合です (244行目)。

				tag = util.extend(new HTMLElement(), {
					nodeName : (token[2]+'').toLowerCase(),
					attributes : util.splitAttrs(token[3]),
					parentNode : parent,
					documentPosition : {
						openTag : {
							start : domParserTokenizer.lastIndex - token[0].length,
							end : domParserTokenizer.lastIndex
						}
					}
				});
				tag.className = tag.attributes['class'];
				tag.id = tag.attributes.id;
				tag.name = tag.attributes.name;

ここでは、nodeName や attributes などが上書きされた HTMLElement を作っています。終わった後は、コメントアウトと同じように、text があったら parent.ChildrenNode に追加し 、tags に タグ内容を push して、parentNode の ChildrenNode に追加しています。
その後は、img タグなど自分で閉じている形だったら closeTag を設定などして、それ以外だと parent を tag に 設定します。

最後に、タグが閉じるケースを書いているのが、272行目以降です。
まず 現在のタグの名前が parent の nodeName と一致した場合は、parent を tag の親要素にして tag に closeTag などを設定します。
次に 上の 自分で閉じている形でありながら、もう一度閉じてしまった場合は、tag の closeTag を上書きします。
それ以外の場合は、閉じられていないため、警告を出します。

内容は意外と読めたので、次は今度は実際に公開され利用されている HTML Parser を読んでみたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?