最近JavaScriptを始めました。右も左も分からない初心者です。どうかお手柔らかにお願いします!
筆者は、普段はC++で、デスクトップアプリやサーバーなどを作っています。C++が好きすぎて他の言語を習得するのが億劫になってしまう年頃(おっさん)です。これではいけない、最新のWeb系フロントエンドもやらなくては、というわけで、昔少しかじって挫折したJavaScriptを再開することにしました。
私は、JSONよりXMLが好きで、DOMが嫌いでSAXが好きです。自作WebアプリでXMLを使おうと思っています。JavaScriptで使えるSAX型のXMLパーサーを探してみましたが、いまいち、これというのが見つけられませんでした。
XMLパーサーにはDOM方式とSAX方式があります。DOMはXMLデータ全体を読み込んで木構造を構築します。SAXはXMLデータを先頭から逐次読み込んで、イベント駆動で処理します。
DOMのイメージ
- トップのタグの~
- 子供タグの~
- 子供タグの~
- 子供タグの~
- データをくれ~
SAXのイメージ
- ドキュメント開始ですよー
- タグが開始しましたよー
- 文字データですよー
- タグ終了ですよー
- ドキュメント終了ですよー
こんな感じです。DOMは木構造の枝を辿って、分岐しながら、目的の値を拾ってきます。SAXは頭からデータを読んでいって、順番に処理していきます。「△△が来ましたよー」というの一つ一つがイベントです。
最初のパーサー
パースする機能をパーサーといいますね。日本語だと構文解析器などといいます。
では、最初の叩き台です。
<html>
<body>
<div id="hoge"></div>
</body>
<script>
var result = ""
var at = window.document.getElementById("hoge")
function parse(src)
{
var result = ""
var pos = 0
while (true) {
var c = -1;
if (pos < src.length) {
c = src[pos]
result += c.toUpperCase()
pos++;
} else {
break;
}
}
return result
}
var src = "Hello, world"
result = parse(src)
at.innerHTML = result
</script>
</html>
parse
関数の中にwhile
ループがあって、1文字ずつ読み込んでresult
を更新していきます。入力データの終端に達したら、ループを抜けて結果を返します。
タグとテキストを区別してパースする
<html>
<body>
<div id="hoge"></div>
</body>
<script>
var result = ""
var at = window.document.getElementById("hoge")
function parse(src)
{
var result = ""
const State_Normal = 0
const State_TagStart = 1
const State_TagName = 2
var isalnum = function(c) {
if (c >= 'a' && c <= 'z') return true
if (c >= 'A' && c <= 'Z') return true
if (c >= '0' && c <= '9') return true
return false
}
var pos = 0;
var state = State_Normal
var text = ""
var tagname = ""
while (true) {
var c = -1;
if (pos < src.length) {
c = src[pos]
pos++;
}
if (c < 0) break
switch (state) {
case State_Normal:
if (c == "<") {
if (text.length > 0) {
result += "chr: " + text + "<br>"
}
text = ""
state = State_TagStart
} else {
text += c
}
break
case State_TagStart:
if (isalnum(c) || c == '/') {
text += c
state = State_TagName
}
break
case State_TagName:
if (isalnum(c)) {
text += c
} else if (c == ">") {
result += "tag: " + text + "<br>"
state = State_Normal
text = ""
}
break
}
}
return result
}
var src = "<tag>text</tag>"
result = parse(src)
at.innerHTML = result
</script>
</html>
これを実行すると、次のような結果が得られます。
tag: tag
chr: text
tag: /tag
<
や>
が現れた時に内部状態(state
)を変更して、タグかテキストデータかを区別するようにしました。
単純なXMLに対応する
<html>
<body>
<div id="hoge"></div>
</body>
<script>
var result = ""
var at = window.document.getElementById("hoge")
SAXParser = function () {
this.onStartElement = function(text) {}
this.onEndElement = function(text) {}
this.onCharacters = function(text) {}
}
SAXParser.prototype.parse = function(src) {
const State_Normal = 0
const State_TagStart = 1
const State_TagName = 2
const State_AttrNameStart = 3
const State_AttrName = 4
const State_AttrValueStart = 4
const State_AttrValue = 5
const State_TagEnd = 6
const TagType_Start = 0
const TagType_StartEnd = 1
const TagType_End = 2
const TagType_Question = 3
const TagType_Exclamation = 4
var my = this
var pos = 0;
var state = State_Normal
var text = ""
var tagname = ""
var tagtype = ""
var entity = ""
var istagsym = function(c) {
if (c >= 'a' && c <= 'z') return true
if (c >= 'A' && c <= 'Z') return true
if (c >= '0' && c <= '9') return true
if (c == '-') return true
if (c == '_') return true
if (c == ':') return true
if (c >= 0x100) return true
return false
}
var dispatch = function() {
if (tagtype == TagType_Start || tagtype == TagType_StartEnd) {
my.onStartElement(tagname)
}
if (tagtype == TagType_End || tagtype == TagType_StartEnd) {
my.onEndElement(tagname)
}
}
var puttext = function(c) {
if (entity == "") {
if (c == '&') {
entity += c
} else {
text += c
}
} else {
entity += c
if (c == ';') {
if (entity.length >= 5 && entity[1] == '#') {
if (entity[2] == 'x' || entity[2] == 'X') {
var s = entity.substr(3, 2)
var i = parseInt(s, 16)
text += String.fromCharCode(i)
}
} else if (entity == "<") {
text += '<'
} else if (entity == ">") {
text += '>'
} else if (entity == "&") {
text += '&'
} else if (entity == """) {
text += '\"'
}
entity = ""
}
}
}
while (true) {
var c = -1;
if (pos < src.length) {
c = src[pos]
pos++;
}
if (c < 0) break
switch (state) {
case State_Normal:
if (c == "<") {
if (text.length > 0) {
this.onCharacters(text)
}
text = ""
state = State_TagStart
tagtype = TagType_Start
} else {
puttext(c)
}
break
case State_TagStart:
if (istagsym(c)) {
text += c
state = State_TagName
} else if (c == "/") {
state = State_TagName
tagtype = TagType_End
} else if (c == "?") {
state = State_TagName
tagtype = TagType_Question
} else if (c == "!") {
state = State_TagName
tagtype = TagType_Exclamation
}
break
case State_TagName:
if (state == "Exclamation") {
} else {
if (istagsym(c)) {
text += c
} else {
tagname = text
text = ""
if (c == "?") {
} else if (c == ">") {
dispatch()
state = State_Normal
}
}
}
break
case State_AttrNameStart:
break
case State_AttrName:
break
case State_AttrValueStart:
break
case State_AttrValue:
break
case State_TagEnd:
break
}
}
}
var src = "<item>Hello, world</item>"
var parser = new SAXParser()
parser.onStartElement = function(text) {
result += "[start element] " + text + "<br>"
}
parser.onEndElement = function(text) {
result += "[end element] " + text + "<br>"
}
parser.onCharacters = function(text) {
result += "[characters] " + text + "<br>"
}
parser.parse(src)
at.innerHTML = result
</script>
</html>
<item>Hello, world</item>
というXMLをパースして、下記のような結果を得られるようになりました。
[start element] item
[characters] Hello, world
[end element] item
SAXParser
という名前でオブジェクト化して、onStartElement
、onEndElement
、onCharacters
というコールバック関数で、各イベントに応答できるようにしてみました。
文字実体参照にも対応しましたので、<&>
の様に書けば<&>
に変換されます。
今後
まず、最低でも属性値の取得に対応しないと、実用になりません。そして、コメントや<![CDATA[~~~]]>
記法も実装する必要があります。あと、特殊なタグで!
や?
から始まるものがあるので、それらに対応する必要があります。