昨日の続き。
やり残していること、というのは
だが、これらに対応した結果の一応の完成版が以下。 あと、いくつか問題があったのも修正している。
xml.hta
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>XML</title>
<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 = xmlToHtml( loadXml( getLoadFilename() ) );
_id( "btnSave" ).disabled = false;
_id( "btnSearch" ).disabled = false;
_id( "txtQuery" ).value = "";
} catch ( e ) {
alert( e );
}
function getLoadFilename() {
return _idValue( "txtFile", "filename" );
}
function loadXml( filename ) {
var encoding = _encoding( loadFromFile( "Shift_JIS", -2 ) );
return loadFromFile( encoding, -1 );
function loadFromFile( encoding, mode ) {
var strm = new ActiveXObject( "ADODB.Stream" );
strm.Charset = encoding;
strm.Open();
strm.LoadFromFile( filename );
var buf = strm.ReadText( mode );
strm.Close();
return buf;
}
}
function xmlToHtml( xml ) {
var rInstruction = new RegExp( /^<\?(.+?)\?>/ ); // 1:宣言内部
var rDoctype = new RegExp( /^<!DOCTYPE\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 = null;
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.innerText ) ) {
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 strm = new ActiveXObject( "ADODB.Stream" );
strm.Charset = _encoding( _tag.call( _id( "base" ), "div" )[ 0 ].innerText );
strm.Open();
strm.WriteText( htmlToXml( _id( "base" ) ), 0 );
strm.SaveToFile( saveFilename, 2 );
strm.Close();
alert( "fin" );
}
} catch ( e ) {
alert( e );
}
function getSaveFilename() {
var partName = prompt( ".xml を .入力値.xml に変えて出力する。", "1" );
return partName === null ? null : _id( "txtFile" ).value.replace( /\.xml$/, "." + partName + ".xml" );
}
function htmlToXml( rootNode ) {
var xmlAry = [];
Array.prototype.forEach.call( rootNode.childNodes, function ( node ) {
if ( node.nodeName === "DIV" ) {
dispatchHandler( node, "" );
}
} );
return xmlAry.join( "" );
function dispatchHandler( div, indent ) {
if ( div.currentStyle.display === "block" ) {
xmlAry.push( indent );
if ( hasChildDiv( div ) ) {
treeHandler( div, indent );
} else {
leafHandler( div, indent );
}
xmlAry.push( "\r\n" );
}
}
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 _get.call( this, id, "getElementById" );
}
function _tag( tag ) {
return _get.call( this, tag, "getElementsByTagName" );
}
function _get( key, func ) {
if ( this === window ) {
return document[ func ]( key );
} else {
return this[ func ]( key );
}
}
function _encoding( s ) {
var matched = s.match( /\s+encoding="([^"]+)"/ );
return matched ? matched[ 1 ] : "Shift_JIS";
}
function _esc( s ) {
return s.replace( /</g, "<" ).replace( />/g, ">" );
}
</script>
</head>
<body>
<form>
<input type="file" maxlength="256" id="txtFile" value="" accept=".xml" />
<input type="button" value="load" id="btnLoad" onclick="doLoad()" />
<input type="button" value="save" id="btnSave" onclick="doSave()" disabled />
</form>
<form>
<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>
このところ、何か作る時にまず考えるのが、どうオブジェクトに分割するかということ。 それは全然悪いことじゃないのだが、そればっかりってのも何だか面白くないので、今回はオブジェクト(クラス)を作らないという縛りを設定してみた。 その結果がこれなのだが、出来上がってから眺めてみると、縛りが上っ面にしか効いてない気がするな。
以下、昨日から変わったところ。
該当部分を、cssで定義しているクラス名を設定したspanで囲むようにしている。 具体的には、 doLoad() の中の xmlToHtml() の中の cDataHandler() と setClassToAttribute() の中で。
HTMLの要素が増えるので、メモリの使用量が増えるとか遅くなるとかの弊害があるかとちょっと心配だったのだが、実際にやってみると、どちらも若干効いているって程度だった。 影響が無いわけじゃないが、心配する程のものでもない。
ファイルがloadされていない状態だと、searchもsaveも意味が無いので、それらのボタンを押せないようにしておくのが自然だろう。 またsearchされていない状態だとresetの意味が無いし、resetの実行後にまたすぐresetするのも無駄。 ということで、各ボタンを以下のように制御している。
いつでも使える。 制御無し。
初期状態はHTMLの属性としてdisabledを設定。 状態変化は doLoad() の最後で、loadのたびに操作可能に設定している。
初期状態はHTMLの属性としてdisabledを設定。 操作可能は doSearch() の最後で、操作不可は doReset() の最後で設定している。
これらに併せて、searchする文字列の入力欄もloadとresetでクリアするようにしている。 現在表示しているのが、何かの検索結果なのかそうじゃないのかが見てすぐ判るように。
長い名前、具体的にはgetElementByIdやgetElementsByTagNameだが、これを短い名前のfunctionに置き換えている。 多くの人が当たり前のように使っているjQueryのアレ相当だが、俺の中では名前を$にするのに今尚抵抗があって、ここではそれぞれ _id() と _tag() にしている。
使うのがdocumentのメソッドばかりじゃ無いので、さらに下請の _get() を置いてthisの切り替えができるようにしてみたのだが、実際に使う時にはcallを介するという実装がなんか微妙だな。 というかこれ、一番最初に作って使わないと、存在意義が半減だよな。
load時のHTML変換のためのinnerHTML文字列作成処理で、半角不等号を 「<」 や 「>」 と書いていた。 またデータ中にこれらがあるかもしれないところでは、これらを置換するように書いていた。
でもこれだとごちゃごちゃしていて見難いし醜いので、 _esc() を作ってその中に変換を隠蔽した。
読みやすさはかなり向上したと思うが、しかしこれも _id() と同じで、最初に作って使わないと存在意義が半減なんだよな。
性能対策で xmlToHtml() を少し書き直し。 match() の実行を存在確率の高い要素から実行するようにし、かつその要素が存在して処理した場合は次のループに進むようにした。 コンパイル済み正規表現を同レベルのfunction内にずらずら並べているのも性能対策の一環だったのだけど、コンパイル済みとはいえループ毎に全部マッチするか判定するのも無駄だし。
単純にコードの整理をするなら、例えば
while ( buf !== "" ) {
...
buf = handleInstruction( buf );
...
}
function handleInstruction( s ) {
return match( s, /^<\?(.+?)\?>/, function ( s1 ) {
htmlAry.push( "<div>" + _esc( "<?" ) + s1 + _esc( "?>" ) + "</div>" );
} );
}
みたいにして、正規表現なんかはそれぞれのfunction内に隠蔽した方が良いんだろうけど、ちょっと遅くなるんだよな。 実際に試してみると、XMLファイルの中身に依存するが、ある程度大きなファイルだとだいたい5〜8割遅くなる。
まあ、比率で言うとかなりの違いがあるように聞こえるけど、実際はこの部分の処理はそんなに時間がかかってなくて、1秒が1.5秒になるぐらいなんだけどね。
あともう一つ、コンパイル済み正規表現をずらずら並べた方がXMLの要素判定でなにをしているかの見通しが良くなる気がするのも、現状のスタイルにしている理由。
昨日のコードの writeTree() 内のswitch文の2番目のcase以降と writeLeaf() のswitch文内がほぼ同じ処理になっていたので、共通する部分を抜き出して elementHandler() とした。 ついでに、子要素としてdivをもっているかどうかの判定も、実質1行だけど、そうだと判る名前の hasChildDiv() でやるようにした。
elementHandler() の中がごちゃごちゃしているのは問題点の修正のため。 これについては後述。
その4をやってる最中に気が変わって、ADODB.Streamへの書き出しは doSave() のトップレベルだけでやるようにし、他は全て書き出し用データの配列を作成するようにした。 最終的にこの配列を結合してXML出力用の文字列を作り、ファイル出力する。
若干メモリの圧迫は増えるが、この方がloadとsaveとで処理の対応が取れて、何をやっているかが判り易い気がする。
これに合わせてfunctionの名前も writeFromRoot() → htmlToXml()、 dispatchWriter() → dispatchHandler()、 writeTree() → treeHandler()、 writeLeaf() → leafHandler() と変更している。
コードの整理で一番やりたかったのは doSave() 内で改行やインデントを出力している部分の改善だった。 なぜそこで改行やインデントの出力が必要なのかをコードを見て判るようにしたかったのだが、これは俺にはどうすることもできなかったよ。
IEのXML表示に倣って、CDATAの表示を開始タグと中身と終了タグで行を分けて
<![CDATA[
データ
]]>
としているのだが、このためにCDATA全体で一つのdivを作り、その中身をまたdivにしていた。
これをファイルに出力するとき、このままの形でファイル出力しようとするので、CDATA内のデータは、元が
<![CDATA[データ]]>
だったとしても、出力結果は
<![CDATA[改行 + インデント + データ + 改行 + 一つ少ないインデント]]>
となってしまう。 こうして出力したXMLをloadすると、CDATA内は改行も含めてそのまま表示しようとするので、今度は
<![CDATA[
データ
]]>
となってしまう。 以降、saveしてloadしてを繰り返すごとに間延びしていく。 これは駄目だろう。
多分ほとんどの場合に先頭や末尾の連続する空白文字が意味を持つことは無いが、パーサーはCDATAをそのままプログラムに渡すのが仕様である以上、そもそも勝手に変えちゃ駄目なのだ。
ということで、
とした。
これで、ファイル出力したときもCDATA内は改行含めて元のファイルのままとなる。 いや、完全にそのままって訳じゃないか。 CDATA内にあった改行は、元がCRLFというwindowsスタイルだったとしてもUNIX形式のLFだけになってしまう。 けどまあ、これはXMLがそういう仕様なんだから仕方ないよな。
XMLファイル内では、テキストノードなどの半角不等号があってはまずい場所にこれらを書く場合は 「<」 や 「>」 に置き換えなければならない。
HTMLでも事情は同じだが、ここでは表示するデータがすでに置き換えられているので、何の問題もなく 「<」 や 「>」 で表示される。
この表示しているHTMLをファイル出力する時、ファイル出力データとして処理対象要素のinnerTextを取得しているのだが、ここには画面表示のままに 「<」 や 「>」 がくるのだな。 「<」 や 「>」 ではなくて。 これをそのままファイル出力していたので、出力結果のXMLファイルは、あっては駄目な所に 「<」 や 「>」 が紛れ込んだXMLとしては不正な形式になってしまっていた。
幸か不幸か、そういった場合でも表示だけはできるようにとテキストノードの読み込み時に _esc() で変換していたために、saveしたファイルをloadしても問題に気付かないでいた。 MSXMLを使っていればエラーになることで気づいたはずなのだが、自作のSAXもどきでは構造のエラーはするするスルーだし、HTMLも勝手に終了タグを補完したりするし。
動作確認の中でsaveして作ったファイルをIEで表示しようとして、ようやくこの問題に気付いたのだった。
で、対応。
この問題に対処する必要があるのはテキストノードだけ。 タグや属性には不等号は入らないし、CDATAは逆に不等号などはそのまま出力する必要がある。 ということで、
とした。
先日、天気予報のゲストで出てきたガチャピンが、クロマキー合成の緑と同じ色だったせいで透明になったりおどろおどろしくなったりしていたのを 「ちょっと考えれば分かるようなことなんだから、ちょっとぐらい考えろよ」 なんて思いながら見ていたのだが、俺も人のことは言えないレベルだった。 最後に修正した問題2点とか、ちょっと考えれば気付きそうなものだが。
あと、IEはもっと何とかならないものか。
大きなXMLを表示させると、先頭の方はすぐに表示されるのだが、後ろの方はタグ無しのテキストだけがまず表示されて、それが前から順に後追いで整形されていく。 この間、CPU使用率が跳ね上がって、スクロールもままならない。 そうまでCPUを使ってるのに、整形のスピードは目で追える程度ってどうよ。 しかも途中で力尽きるのか、最後まで表示しきれず途中から空白になる時がある。 firefoxだと、そう時間もかからずに最後までちゃんと表示されるのに。
実体がIEのHTAも同様。 なんとか途中の処理を改善してメモリの使用量を抑えても、最後にHTML化するところでメモリ不足になったり、メモリ不足にならなくても途中から空白になってたりして、がっかりも半端無い。
手軽な分だけ制約もあるってことなんだろうが、それにしても、ねえ。 まあ今回色々やって上限が大体掴めたし、これを今後に生かせばいいのかな。