昨日の続き。
せっかく調べたのだし、もうちょっとアプリケーションらしいもので使ってみようと、前に作った 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, "<" ).replace( />/g, ">" );
}
</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 以外で。