Webfeed.htmlを改善してフィードをツリー表示できるようにする
前置き
Opera 9.6より前では、Opera はスタイルの付いてない XML 文書を開くと、文字列をザーっと表示するだけだった。
そのため、XML をツリー表示させるユーザー JavaScript なりブックマークレットなりがたくさんあった。前にも何度かに分けて紹介したが、ここらへん。
- Tools - Opera Developer Community
- XML tree - UserJS.org
- View page's DOM in a tree view - User JavaScript - Opera Community
- PUBlog » Pretty XML tree view for Opera
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 とか) はちゃんと整形してくれるっぽい。
やっぱスタイル付けたいと思ったら直してみようかな。
元に戻したいときは、今のところリロードするしかない。
ソース
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><</xsl:text> <xsl:copy-of select="$tag"/> <xsl:apply-templates select="@*"/> <xsl:if test="not(node())"> <xsl:text> /</xsl:text> </xsl:if> <xsl:text>></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><</xsl:text> <xsl:copy-of select="$tag"/> <xsl:text>></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(\'"\', ., \'"\')"/> </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><--</xsl:text> <xsl:value-of select="."/> <xsl:text>--></xsl:text> </span> </xsl:template> <xsl:template match="processing-instruction()"> <div class="pi"> <xsl:text><?</xsl:text> <xsl:value-of select="name(.)"/> <xsl:text> </xsl:text> <xsl:value-of select="."/> <xsl:text>?></xsl:text> </div> </xsl:template> </xsl:stylesheet>' } tree.create(); } }) ();