JavaScriptでXMLをイベント駆動（SAX）でパースする

最近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
<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
<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
<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 == "&lt;") {
                            text += '<'
                        } else if (entity == "&gt;") {
                            text += '>'
                        } else if (entity == "&amp;") {
                            text += '&'
                        } else if (entity == "&quot;") {
                            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`というコールバック関数で、各イベントに応答できるようにしてみました。

文字実体参照にも対応しましたので、`&lt;&amp;&gt;`の様に書けば`<&>`に変換されます。

### 今後

まず、最低でも属性値の取得に対応しないと、実用になりません。そして、コメントや`<![CDATA[～～～]]>`記法も実装する必要があります。あと、特殊なタグで`!`や`?`から始まるものがあるので、それらに対応する必要があります。