2016 07 05

XML再び

昨日の続き。

せっかく調べたのだし、もうちょっとアプリケーションらしいもので使ってみようと、前に作った XMLを表示するHTA をHTMLに置き換えてみた。

xml.html

<!DOCTYPE html> <html> <head> <title>TODO supply a title</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> * { margin : 0; padding : 0; box-sizing : border-box; line-height : 1; font-size : inherit; font-family : inherit; } body { font-size : 10pt; font-family : sans-serif; } form { background : #f0f0f0; width : 100%; height : auto; padding : 0.3em 0.6em; margin-bottom : 2px; } form * { display : inline-block; } input[type="file"] { width : 50%; } input[type="text"] { width : 50%; font-family : monospace; } input[type="button"] { width : 6em; border : 1px solid #808080; background : #e0e0e0; padding : 0.16em 0; } input[type="button"]:disabled { color : #a0a0a0; border-color : #c0c0c0; } input[type="button"]:enabled:hover { background : #f8f8f8; } #base div { margin-left : 1em; font-family : monospace; } .cdata { display : block; margin-left : 1em; white-space : pre; color : #0000f0; } .attributeName { color : #f08080; } .attributeValue { color : #0000f0; } </style> <script> function doLoad() { try { _id( "base" ).innerHTML = ""; _idValue( "txtFile", "filename" ); var f = _id( "txtFile" ).files[ 0 ]; var reader = new FileReader(); reader.onerror = function ( evnt ) { var code = evnt.target.error.code || "-"; var name = evnt.target.error.name || "-"; var message = evnt.target.error.message || "-"; alert( "ERROR : " + code + " : " + name + " : " + message ); }; reader.onload = function ( evnt0 ) { try { var matched = evnt0.target.result.match( /\s+encoding="([^\"]+)"/ ); var encoding = matched ? matched[ 1 ] : "UTF-8"; reader.onload = function ( evnt1 ) { try { _id( "base" ).innerHTML = xmlToHtml( evnt1.target.result ); _id( "btnSave" ).disabled = false; _id( "btnSearch" ).disabled = false; } catch ( e ) { alert( e ); } }; reader.readAsText( f, encoding ); } catch ( e ) { alert( e ); } }; reader.readAsText( f.slice( 0, 100 ), "UTF-8" ); } catch ( e ) { alert( e ); } function xmlToHtml( xml ) { var rInstruction = new RegExp( /^<\?(.+?)\?>/ ); // 1:宣言内部 var rDoctype = new RegExp( /^<!DOCTYPE\s+(([^\s].+?)(\s+[^\s>]+)*?)>/i ); // 1:宣言内部 var rComment = new RegExp( /^<!--([\s\S]*?)-->/ ); // 1:コメント var rCData = new RegExp( /^<!\[CDATA\[([\s\S]+?)\]\]>/i ); // 1:CDATA内部 var rText = new RegExp( /^(([^<]|[*s])+)/ ); // 1:テキスト全体 var rTag = new RegExp( /^<([^\?!][^>]*(\s*("[^\"]*")*\s*)*?)>/ ); // 1:<>内部 var rNotBlank = new RegExp( /[^\s]/m ); var rAttributes = new RegExp( /\s+([^\s="]+)\s*=\s*"([^"]+)"/g ); // 1:属性名, 2:属性値 var htmlAry = []; var matched = false; var buf = xml; while ( buf !== "" ) { // 出現する可能性の高い順に処理する。 buf = match( buf, rTag, tagHandler ); if ( matched ) continue; buf = match( buf, rText, textHandler ); if ( matched ) continue; buf = match( buf, rCData, cDataHandler ); if ( matched ) continue; buf = match( buf, rComment, ignore ); if ( matched ) continue; buf = match( buf, rInstruction, instructionHandler ); if ( matched ) continue; buf = match( buf, rDoctype, doctypeHandler ); if ( matched ) continue; // どれにも該当しないのはおかしい。 throw "ERROR : unsupported element"; } return htmlAry.join( "" ); function match( s, regexp, handler ) { matched = s.match( regexp ); if ( matched ) { handler( matched[ 1 ] ); return s.substr( matched[ 0 ].length ); } return s; } function ignore() {} function instructionHandler( s ) { htmlAry.push( "<div>" + _esc( "<?" ) + s + _esc( "?>" ) + "</div>" ); } function doctypeHandler( s ) { htmlAry.push( "<div>" + _esc( "<!DOCTYPE " ) + s + _esc( ">" ) + "</div>" ); } function cDataHandler( s ) { htmlAry.push("<div>" + _esc( "<![CDATA[" ) + "<span class='cdata'>" + _esc( s ) + "</span>" + _esc( "]]>" ) + "</div>"); } function textHandler( s ) { if ( rNotBlank.test( s ) ) { htmlAry.push( "<span class='text'>" + _esc( s ) + "</span>" ); } } function tagHandler( s ) { if ( s.charAt( 0 ) === "/" ) { endTagHandler( s ); } else if( s.charAt( s.length - 1 ) === "/" ) { emptyTagHandler( s ); } else { startTagHandler( s ); } } function startTagHandler( s ) { htmlAry.push( "<div>" + _esc( "<" ) + setClassToAttribute( s ) + _esc( ">" ) ); } function endTagHandler( s ) { htmlAry.push( _esc( "<" ) + s + _esc( ">" ) + "</div>" ); } function emptyTagHandler( s ) { htmlAry.push( "<div>" + _esc( "<" ) + setClassToAttribute( s ) + _esc( ">" ) + "</div>" ); } function setClassToAttribute( s ) { if ( rAttributes.test( s ) ) { return s.replace( rAttributes, ' <span class="attributeName">$1</span>="<span class="attributeValue">$2</span>"' ); } return s; } } } function doSearch() { try { var qry = getQryRegExp(); Array.prototype.forEach.call( _tag.call( _id( "base" ), "div" ), function ( div ) { if ( qry.test( div.textContent ) ) { div.style.display = "block"; } else { div.style.display = "none"; } } ); _tag.call( _id( "base" ), "div" )[ 0 ].style.display = "block"; _id( "btnReset" ).disabled = false; } catch ( e ) { alert( e ); } function getQryRegExp() { return new RegExp( _idValue( "txtQuery", "query" ) ); } } function doReset() { try { Array.prototype.forEach.call( _tag.call( _id( "base" ), "div" ), function ( div ) { div.style.display = "block"; } ); _id( "btnReset" ).disabled = true; _id( "txtQuery" ).value = ""; } catch ( e ) { alert( e ); } } function doSave() { try { var saveFilename = getSaveFilename(); if ( saveFilename ) { var xml = htmlToXml( _id( "base" ) ); var blob = new Blob( [ xml ], { "type" : "text/xml" } ); if ( window.navigator.msSaveBlob) { // IE window.navigator.msSaveBlob( blob, saveFilename ); } else { var a = document.createElement( "a" ); a.download = saveFilename; a.href = window.URL.createObjectURL( blob ); var ctrl1 = _id( "ctrl1" ); ctrl1.appendChild( a ); a.click(); ctrl1.removeChild( a ); } } } catch ( e ) { alert( e ); } function getSaveFilename() { var partName = prompt( ".xml を .入力値.xml に変えて出力する。", "1" ); return partName === null ? null : _id( "txtFile" ).value.replace( /^.*[\/]/, "").replace( /^(.+)\.xml$/, "$1." + partName + ".xml" ); } function htmlToXml( rootNode ) { var xmlAry = []; Array.prototype.forEach.call( rootNode.childNodes, function ( node ) { if ( node.nodeName === "DIV" ) { dispatchHandler( node, "" ); } } ); setEncoding(); return xmlAry.join( "" ); function setEncoding() { var rEncoding = new RegExp( /(encoding\s*=\s*)"([^"]+)"/ ); matched = xmlAry[ 0 ].match( rEncoding ); if ( matched ) { if ( matched[ 2 ] !== "UTF-8" ) { xmlAry[ 0 ] = xmlAry[ 0 ].replace( rEncoding, "$1\"UTF-8\""); } } else { xmlAry[ 0 ] = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + xmlAry[ 0 ]; } } function dispatchHandler( div, indent ) { if ( getDisplay( div ) === "block" ) { if ( indent ) { xmlAry.push( indent ); } if ( hasChildDiv( div ) ) { treeHandler( div, indent ); } else { leafHandler( div, indent ); } xmlAry.push( "\r\n" ); } function getDisplay( elmnt ) { if ( elmnt.currentStyle ) { // IE return elmnt.currentStyle.display; } else { return document.defaultView.getComputedStyle( elmnt, null ).display; } } } function hasChildDiv( div ) { return _tag.call( div, "div" )[ 0 ]; } function leafHandler( div, indent ) { Array.prototype.forEach.call( div.childNodes, elementHandler ); } function treeHandler( div, indent ) { var isAfterDiv = false; Array.prototype.forEach.call( div.childNodes, function ( node ) { if ( node.nodeName === "DIV" ) { if ( !isAfterDiv ) { xmlAry.push( "\r\n" ); } dispatchHandler( node, indent + " " ) ; isAfterDiv = true; } else { if ( isAfterDiv ) { xmlAry.push( indent ); isAfterDiv = false; } elementHandler( node ); } } ); } function elementHandler( node ) { switch ( node.nodeName ) { case "SPAN": switch ( node.className ) { case "cdata": xmlAry.push( node.textContent ); break; case "text": xmlAry.push( _esc( node.innerText ) ); break; default: xmlAry.push( node.innerText ); } break; case "#text": xmlAry.push( node.nodeValue ); break; default: throw "ERROR : unsupported node : " + node.nodeName; } } } } function _idValue( id, msg ) { var obj = _id( id ); if ( !obj.value ) { throw "ERROR : " + msg; } return obj.value; } function _id( id ) { return document.getElementById( id ); } function _tag( tag ) { return this.getElementsByTagName( tag ); } function _esc( s ) { return s.replace( /</g, "&lt;" ).replace( />/g, "&gt;" ); } </script> </head> <body> <form id="ctrl1"> <input type="file" id="txtFile" accept=".xml" /> <input type="button" value="load" id="btnLoad" onclick="doLoad()" /> <input type="button" value="save" id="btnSave" onclick="doSave()" disabled /> </form> <form id="ctrl2"> <input type="text" maxlength="256" id="txtQuery" value="" placeholder="検索文字列の正規表現" /> <input type="button" value="search" id="btnSearch" onclick="doSearch()" disabled /> <input type="button" value="reset" id="btnReset" onclick="doReset()" disabled /> </form> <div id="base"></div> </body> </html>

考え方はHTA版とほとんど同じ。 なのでプログラムのコードも大体同じ。 いやまあ、ファイル入出力とブラウザ依存の対応以外は何も変えてないので、大体同じなのが当然なのだが。

ファイルの読み込みでイベント処理が二重になっているのは、まずエンコーディングを取得し、そのエンコーディングでファイル全体を読むため。

方針はHTA版と同じなのだが、FileAPIには1行だけを読むメソッドが用意されていないので、とりあえずUTF-8として100文字読んでいる。 XMLのしきたりに則っているなら先頭にXML宣言があるはずだし、XML宣言にはASCII文字だけのはずだし、これで大体いけるだろう。

そういえば、一昨日はファイルの一部だけを読むことについて 「それらの使い所が思いつかない」 なんて言ってたんだよな。 その舌の根の乾かぬうちにこの様。

ところで、慣用句だから何となく受け入れているけど、考えてみれば舌の根って乾かないよな。 冬とかの乾燥が厳しい時期に口を開けて寝てると、朝起きた時の口の中はかなり乾いている感覚だけど、それでも舌の根はそこそこ湿っている状態。 本当に乾くのは死後相当の時間が経った場合で、生きている間は乾くことはない。 これが 「すぐに」 相当で使われるのは何故だろう。

ファイルの書き込みでは、書き込み処理そのものがブラウザ依存だが、HTML化したことで書き込むためのデータの取得でもブラウザの種類を考慮する必要がでてきた。 getDisplay() の中の分岐がそう。 IEの場合は currentStyle 属性を使い、その他の場合は getComputedStyle() を使う。

それと、これはブラウザには無関係だが、出力がUTF-8固定なので、元のファイルにエンコーディングの指定が無い場合は、先頭にUTF-8として宣言部を追加出力している。 エンコーディングの指定があり、かつそれがUTF-8でない場合は、UTF-8に置き換えて出力。

とまあ、地味に面倒な作業があったりしたのだが、そうした苦労の成果は大きかった。

速い!

ただし IE と Safari 以外。

手元の環境でテストした中では Chrome が最速。 少々大きなファイルでも一瞬で表示完了する。 FireFox も十分速いが、Chrome より少し遅い感じ。 IE と Safari は Chrome の20〜30倍ぐらい遅い。 やる気を疑うレベル。 それでもHTA版よりは少し速いのだが。

IEでは、メモリ使用の上限の制約がHTA版よりもHTML版の方が緩い感じ。 同じエンジンを使っているのかと思ったが、どうやらそうではないらしい。

メジャーなブラウザの最新版ならほとんど動くので、環境を選ばないのも大きい。 HTA版を作っている時は、Macで使いたくなったらMac用に何か作ればいいと思っていたが、まさかそれがHTMLで解決するとはね。 便利な世の中になったなぁ。

ただし出力したいなら Safari 以外で。