Webfeed.htmlを改善してフィードをツリー表示できるようにする

前置き

Opera 9.6より前では、Opera はスタイルの付いてない XML 文書を開くと、文字列をザーっと表示するだけだった。

そのため、XML をツリー表示させるユーザー JavaScript なりブックマークレットなりがたくさんあった。前にも何度かに分けて紹介したが、ここらへん。

Opera 9.6 になって、RSS/Atom の場合はスタイルが付くようになって、視覚的には良くなった。

(どうでもいいけど、最初からスタイルの付いている RSS/Atom にもスタイルが付くのは仕様か? そうじゃなかったら詰めが甘すぎ)

その反面、ツリー表示する方法がことごとく死んでしまった。

なんせ document.documentElement とかやると、Opera によって整形されたほうの HTML を返すのだから。

Webfeed.htmlを弄る

Webfeed.html (opera:config#Webfeeds HTML Template File) を見てみると、フィード URL 内でしか使えない opera.feeds というオブジェクトを使って整形していることが分かる。

これを使って整形できるかと思って色々テストしてみたが、思いの他不自由なオブジェクトだったので、早々に諦めた。

途方に暮れていたところ、「XMLHttpRequest で自分自身を取得してこればいいんじゃん」と気付いたので、それを使って実装してみた。

下を右クリックしてリンク先を保存。

元々の Webfeeds.html は弄らずに、上のやつを適当なところに置いて opera:config のほうで指定するほうがいいと思う。


ソースは一番下に書いたので、それをまるまるコピーして、Webfeeds.html の中で以下の部分を探してきて、ペーストすれば同じものになる。

<script type="text/javascript">
//<![CDATA[

/*ココに挿入*/

String.prototype.trim = function()

または、ソースをブックマークレットとして実行することもできる。


そうすると、図のように Tree View というボタンが出来るので、クリックするだけ。


タグ名をクリックしたら折り畳むこともできる。

HTML 部分は色付けできてないし、折り畳むこともできないのだけど、これは仕様。( CDATAの中に入ってるから XSLT では仕方無いかも)


content:encoded として CDATA に入ってる場合 (はてなとか) などは整形されず、

<content xml:lang="en" type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"

とかいう宣言付き XHTML のとき (my.opera とか) はちゃんと整形してくれるっぽい。

やっぱスタイル付けたいと思ったら直してみようかな。
元に戻したいときは、今のところリロードするしかない。

スタイルの付け方はこちらを参考に XSLT を使ってやったので、けっこう高速だと思う。(というかほぼコピペ)

ソース

Operaが整形してくれた RSS/Atom 文書でブックマークレットとして使う場合は頭に javascript: を付ける。

そうじゃないページで使っても、エラー処理とかをちゃんとしてないし、サーバーに余計な命令を飛ばすことになるのでしないでね。

(function () {
var xhr=new XMLHttpRequest();
xhr.open('GET', location.href);
xhr.ready = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    return true;
  }else{
    return false;
  }
}
xhr.wait = function(){
    setTimeout(function(){
        if(xhr.ready()){
            var originalXMLDocument=(new DOMParser()).parseFromString(xhr.responseText,"application/xml");

            var button = document.createElement('button');
            button.textContent = 'Tree View';
            button.onclick = function(){parseTree(originalXMLDocument)};
            var heading = document.getElementById('heading');
            heading.appendChild(button);
        }else{
            xhr.wait();
        }
    },100);
}
xhr.send(null);
xhr.wait();

var parseTree=function(doc){

var tree = {
	create: function() {
		var processor = new XSLTProcessor();
		processor.importStylesheet(tree.getStylesheet());
		processor.setParameter(null, 'xml-declaration', tree.getXmlDeclaration());
		processor.setParameter(null, 'doctype', tree.getDoctype());
		document.replaceChild(processor.transformToDocument(doc).documentElement, document.documentElement);
		tree.init();
	},

	init: function () {
		function higlight() {
			this.parentNode.className += ' tag-hover';
		}
		function unhiglight() {
			this.parentNode.className = this.parentNode.className.replace('tag-hover', '');
		}
		function toggle() {
			if (this.parentNode.className.indexOf('closed') > -1) {
				this.parentNode.className =  this.parentNode.className.replace('closed', '');
			} else {
				this.parentNode.className += ' closed';
			}
		}
		var result = document.evaluate("//[contains(@class, 'tag-start') or  contains(@class, 'tag-end')]", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		var node, i = 0;
		while (node = result.snapshotItem(i++)) {
			node.onmouseover = higlight;
			node.onmouseout = unhiglight;
			node.onclick = toggle;
		}
	},

	getDoctype: function() {
		var dt = '';
		if (doc.doctype) {
			var dt = '<!DOCTYPE ' + doc.doctype.name;
			if (doc.doctype.publicId) {
				dt += ' "' + doc.doctype.publicId + '"';
			}
			if (doc.doctype.systemId) {
				dt += ' "' + doc.doctype.systemId + '"';
			}
			if (doc.doctype.internalSubset) {
				dt += ' [' + doc.doctype.internalSubset + ']';
			}
			dt += '>';
		}
		return dt;
	},

	getXmlDeclaration: function() {
		var xmlDecl = new XMLSerializer().serializeToString(doc).match(/^(\s*)(<\?xml.+?\?>)/i);
		return xmlDecl[2];
	},

	getStylesheet: function() {
        return new DOMParser().parseFromString(tree.xslt, "text/xml");
	},

	xslt: '<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0"> <xsl:output encoding="utf-8" method="xml" indent="no" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"/> <xsl:param name="doctype"/> <xsl:param name="xml-declaration"/> <xsl:template match="/"> <html> <head> <title>Pretty XML tree</title> <style type="text/css"> body, html { margin: 0; padding: 0; } #info { background: #ccc; border-bottom: 3px solid #000; padding: 1em; margin-bottom: 2em; } #xml-declaration { font-weight: bold; } #doctype { font-weight: bold; color: green; margin: 0; } #tree { font: 13px/1.2 monospace; padding-left: .4em; } .ele { margin: 2px 0 5px; border-left: 1px dotted #fff; } .ele .ele { margin-left: 40px } .content { display: inline; } div.inline, div.inline * { display: inline; margin: 0; border-left: none; } .name, .prefix { color: purple; font-weight: bold; } .a-value { color: blue; } .a-name { font-weight: bold; } .comment { color: green; font-style: italic; } .text { white-space: pre; color: #484848; } .pi { color: orange; font-weight: bold; } .tag { color: #000; } .tag-start, .tag-end { cursor: pointer; } .tag-hover > .tag:last-child, .tag-hover > .tag:first-child { background: #eee; } .tag-hover { border-left-style: solid; border-left-color: #ccc; } .closed > .content { display: none; } .closed > .tag-start:after { content: \'...\'; background: lime; } </style> </head> <body> <div id="tree"> <div id="xml-declaration"> <xsl:value-of select="$xml-declaration"/> </div> <xsl:apply-templates select="processing-instruction()" /> <xsl:if test="$doctype"> <pre id="doctype"> <xsl:value-of select="$doctype"/> </pre> </xsl:if> <xsl:apply-templates select="node()[not(self::processing-instruction())]" /> </div> </body> </html> </xsl:template> <xsl:template match="a[@class = \'x-opera-anchorized\']"> <xsl:apply-templates select="text()"/> </xsl:template> <xsl:template match="*"> <div class="ele"> <xsl:if test="(preceding-sibling::text()[normalize-space(.)] or following-sibling::text()[normalize-space(.)]) and not(*)"> <xsl:attribute name="class"> <xsl:text> inline</xsl:text> </xsl:attribute> </xsl:if> <xsl:if test="namespace-uri(.)"> <xsl:attribute name="title"> <xsl:value-of select="namespace-uri(.)"/> </xsl:attribute> </xsl:if> <xsl:variable name="tag"> <xsl:if test="contains(name(.), \':\')"> <span class="prefix"> <xsl:value-of select="substring-before(name(.), \':\')"/> </span> <xsl:text>:</xsl:text> </xsl:if> <span class="name"> <xsl:value-of select="local-name(.)"/> </span> </xsl:variable> <span id="start-{generate-id(.)}"> <xsl:attribute name="class"> <xsl:text>tag tag-</xsl:text> <xsl:choose> <xsl:when test="node()">start</xsl:when> <xsl:otherwise>self-close</xsl:otherwise> </xsl:choose> </xsl:attribute> <xsl:text>&lt;</xsl:text> <xsl:copy-of select="$tag"/> <xsl:apply-templates select="@*"/> <xsl:if test="not(node())"> <xsl:text> /</xsl:text> </xsl:if> <xsl:text>&gt;</xsl:text> </span> <xsl:if test="node()"> <div class="content"> <xsl:apply-templates /> </div> <span class="tag tag-end" id="end-{generate-id(.)}"> <xsl:text>&lt;</xsl:text> <xsl:copy-of select="$tag"/> <xsl:text>&gt;</xsl:text> </span> </xsl:if> </div> </xsl:template> <xsl:template match="@*"> <xsl:text> </xsl:text> <span class="a-name"> <xsl:value-of select="name(.)"/> </span> <xsl:text>=</xsl:text> <span class="a-value"> <xsl:value-of select="concat(\'&quot;\', ., \'&quot;\')"/> </span> </xsl:template> <xsl:template match="text()"> <xsl:if test="normalize-space(.)"> <span class="text"> <xsl:value-of select="."/> </span> </xsl:if> </xsl:template> <xsl:template match="comment()"> <span class="comment"> <xsl:text>&lt;--</xsl:text> <xsl:value-of select="."/> <xsl:text>--&gt;</xsl:text> </span> </xsl:template> <xsl:template match="processing-instruction()"> <div class="pi"> <xsl:text>&lt;?</xsl:text> <xsl:value-of select="name(.)"/> <xsl:text> </xsl:text> <xsl:value-of select="."/> <xsl:text>?&gt;</xsl:text> </div> </xsl:template> </xsl:stylesheet>'
}

	tree.create();
}
}) ();