HTMLDocument の動的な作成 ― 2009年10月29日 06時11分
ブラウザ上で、JavaScript を使って HTML のソースから HTML 文書を生成するのに、どんな方法があるのか調べました。なお、以下のスクリプトは HTML 文書上で実行することが前提です。
- 表の見方
- XSLT の HTML 出力
createHTMLDocument
メソッドcreateDocument
メソッドcreateDocument
メソッドと名前空間の指定createDocument
メソッドと文書型宣言の指定createDocument
メソッドと文書型宣言及び名前空間の指定cloneNode
メソッド- iframe 要素
ActiveXObject
- CID からの作成
- まとめ
表の見方
以下の表において、各項目の意味は次の通りです。
doc
- HTML 文書を作成できれば○、XML 文書を作成できれば△、それ以外なら×です。HTML 文書かどうかは、
createElement
メソッドが HTML 要素を作成するかどうかで判断しています。 doc.title
- 文書ノードの
title
プロパティから title 要素の内容を取得できれば○、できなければ×です。 doc.body
- 文書ノードの
body
プロパティから body 要素を取得できれば○、できなければ×です。 - 名前空間なし XPath
//p
のような名前空間接頭辞の付かない XPath 式で HTML 要素を取得できれば○、できなければ×です。- 名前空間付き XPath
//h:p
のような名前空間接頭辞の付いた XPath 式で HTML 要素を取得できれば○、できなければ×です。ここで、接頭辞h
は名前空間 URIhttp://www.w3.org/1999/xhtml
に関連付けられているものとします。innerHTML
innerHTML
プロパティに<p>hello world
のような、整形式 XML ではなく、かつ HTML の実体参照を含んだ文字列を設定して、エラーが出なければ○、エラーになれば×です。- スクリプト
- script 要素で指定されたスクリプトが実行されれば○、されなければ×です。
また、使用したブラウザは次の通りです。いずれも Windows 版です。
- Firefox 3.0.11
- Firefox 3.5.3
- Minefield (Firefox の開発版) 3.7a1pre、HTML5 パーサ無効
- Minefield 3.7a1pre、HTML5 パーサ有効 (
html5.enable
をtrue
に設定) - Opera 9.64
- Opera 10.00
- Safari 3.2.3
- Safari 4.0.3
- Internet Explorer 6 SP3
- Internet Explorer 7
- Internet Explorer 8
調査には次のファイルを使いました。
HTMLDocument
の作成 (Firefox、Safari、Opera 用)HTMLDocument
の作成 (Firefox、Safari、Opera、Chrome 用)HTMLDocument
の作成 (IE 用)
XSLT の HTML 出力
function createHTMLDocument_XSLT(source) {
var processor = new XSLTProcessor();
var sheet = new DOMParser().parseFromString(
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">' +
'<xsl:output method="html"/>' +
'<xsl:template match="/">' +
'<html><head><title></title></head><body></body></html>' +
'</xsl:template>' +
'</xsl:stylesheet>',
'application/xml'
);
processor.importStylesheet(sheet);
var doc = processor.transformToDocument(sheet);
var range = doc.createRange();
range.selectNodeContents(doc.documentElement);
range.deleteContents();
doc.documentElement.appendChild(range.createContextualFragment(source));
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
doc.title |
× | ○ | ○ | ○ | × | × | × | ○ |
doc.body |
× | × | × | ○ | × | × | ○ | ○ |
名前空間なし XPath | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
名前空間付き XPath | × | × | ○ | ○ | ○ | ○ | × | × |
innerHTML |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
スクリプト | × | × | × | × | × | × | × | × |
比較的以前から使われている方法です。XSLT を使ってある文書を HTML 文書に変換し、後は innerHTML
なり Range#createContextualFragment
なりでソースを流し込みます。Firefox 2 (Gecko 1.8.1) 以前では、フラグメント識別子付きの URL 上でエラーが出るそうです。
XSLTProcessor
、DOMParser
、Range#createContextualFragment
は Mozilla による独自拡張ですが、Safari (WebKit) と Opera もサポートしています。ブラウザによっては createContextualFragment
を使ったパースで、head 要素や body 要素が消えてしまうことがあります。
createHTMLDocument
メソッド
function createHTMLDocument_createHTMLDocument(source) {
var doc = document.implementation.createHTMLDocument('');
var range = doc.createRange();
range.selectNodeContents(doc.documentElement);
range.deleteContents();
doc.documentElement.appendChild(range.createContextualFragment(source));
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
× | × | × | × | ○ | ○ | ○ | ○ |
doc.title |
― | ― | ― | ― | × | × | × | ○ |
doc.body |
― | ― | ― | ― | × | × | ○ | ○ |
名前空間なし XPath | ― | ― | ― | ― | ○ | ○ | ○ | ○ |
名前空間付き XPath | ― | ― | ― | ― | ○ | ○ | × | × |
innerHTML |
― | ― | ― | ― | ○ | ○ | ○ | ○ |
スクリプト | ― | ― | ― | ― | × | × | × | × |
Safari と Opera は、HTML 文書を直接生成するその名も createHTMLDocument
メソッドを実装しています。このメソッドは、DOM 2 HTML 2002 年 6 月 5 日付勧告候補までは存在したのですが、以降は削除され勧告には含まれていません。しかし、HTML5 草案で復活したので、将来的には他のブラウザでも実装されるかもしれません。
createDocument
メソッド
function createHTMLDocument_createDocument(source) {
var doc = document.implementation.createDocument(null, 'html', null);
var range = document.createRange();
range.selectNodeContents(document.documentElement);
var content = doc.adoptNode(range.createContextualFragment(source));
doc.documentElement.appendChild(content);
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
△ | △ | △ | △ | △ | △ | △ | △ |
doc.title |
× | ○ | ○ | ○ | × | × | × | × |
doc.body |
× | × | × | × | × | × | × | × |
名前空間なし XPath | ○ | ○ | × | × | × | × | ○ | ○ |
名前空間付き XPath | × | × | ○ | ○ | ○ | ○ | × | × |
innerHTML |
× | × | × | × | × | × | ○ | ○ |
スクリプト | × | × | × | × | × | × | × | × |
XML 文書を生成するための createDocument
メソッドを使います。これにより生成される文書はあくまでも XML 文書なので、HTML ソースのパースには呼び出し元の HTML 文書を使っています。異なる文書で生成されたノードを文書ツリーに追加するため adoptNode
を使っていますが、Firefox 2 以下では対応していないので代わりに importNode
を使う必要があります。
createDocument
メソッドと名前空間の指定
function createHTMLDocument_createDocument_NS(source) {
var XHTML_NS = 'http://www.w3.org/1999/xhtml';
var doc = document.implementation.createDocument(XHTML_NS, 'html', null);
var range = document.createRange();
range.selectNodeContents(document.documentElement);
var content = doc.adoptNode(range.createContextualFragment(source));
doc.documentElement.appendChild(content);
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
△ | △ | △ | △ | ○ | ○ | ○ | ○ |
doc.title |
× | ○ | ○ | ○ | × | × | × | ○ |
doc.body |
× | × | × | × | × | × | ○ | ○ |
名前空間なし XPath | ○ | ○ | × | × | × | × | ○ | ○ |
名前空間付き XPath | × | × | ○ | ○ | ○ | ○ | × | × |
innerHTML |
× | × | × | × | × | × | ○ | ○ |
スクリプト | × | × | × | × | × | × | × | × |
Safari と Opera では、createDocument
で文書要素 (ルート要素) に XHTML の名前空間を指定すると、XHTML 文書が生成されます。このとき、Safari では innerHTML
に整形式 XML の断片しか設定できません。
createDocument
メソッドと文書型宣言の指定
function createHTMLDocument_createDocument_DTD(source) {
var doctype = document.implementation.createDocumentType('html',
'-//W3C//DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd');
var doc = document.implementation.createDocument(null, 'html', doctype);
var range = document.createRange();
range.selectNodeContents(document.documentElement);
var content = doc.adoptNode(range.createContextualFragment(source));
doc.documentElement.appendChild(content);
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
△ | ○ | ○ | ○ | △ | △ | △ | △ |
doc.title |
× | ○ | ○ | ○ | × | × | × | × |
doc.body |
× | × | × | × | × | × | × | × |
名前空間なし XPath | ○ | ○ | ○ | ○ | × | × | ○ | ○ |
名前空間付き XPath | × | × | ○ | ○ | ○ | ○ | × | × |
innerHTML |
× | ○ | ○ | ○ | × | × | ○ | ○ |
スクリプト | × | × | × | × | × | × | × | × |
DOM 3 Core では、createDocument
で文書型宣言を指定すると、その文書型宣言に見合った文書を作成してもよい (may) ことになっています。Firefox 3.5 (Gecko 1.9.1) 以降ではこれに従って、HTML 4 の文書型宣言に対しては HTML 文書が、XHTML 1.0 の文書型宣言に対しては XHTML 文書が生成されます。
createDocument
メソッドと文書型宣言及び名前空間の指定
function createHTMLDocument_createDocument_DTD_NS(source) {
var XHTML_NS = 'http://www.w3.org/1999/xhtml';
var doctype = document.implementation.createDocumentType('html',
'-//W3C//DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd');
var doc = document.implementation.createDocument(XHTML_NS, 'html', doctype);
var range = document.createRange();
range.selectNodeContents(document.documentElement);
var content = doc.adoptNode(range.createContextualFragment(source));
doc.documentElement.appendChild(content);
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
△ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
doc.title |
× | ○ | ○ | ○ | × | × | × | ○ |
doc.body |
× | × | × | ○ | × | × | ○ | ○ |
名前空間なし XPath | ○ | ○ | ○ | ○ | × | × | ○ | ○ |
名前空間付き XPath | × | × | ○ | ○ | ○ | ○ | × | × |
innerHTML |
× | ○ | ○ | ○ | × | × | ○ | ○ |
スクリプト | × | × | × | × | × | × | × | × |
HTML 4 の文書型宣言と XHTML の名前空間を両方指定した場合です。
cloneNode
メソッド
function createHTMLDocument_cloneNode(source) {
var doc = document.cloneNode(false);
doc.appendChild(doc.importNode(document.documentElement, false));
var range = document.createRange();
range.selectNodeContents(document.documentElement);
var content = doc.adoptNode(range.createContextualFragment(source));
doc.documentElement.appendChild(content);
return doc;
}
Firefox | Safari | Opera | ||||||
---|---|---|---|---|---|---|---|---|
3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
× | ○ | ○ | ○ | × | × | × | × |
doc.title |
― | ○ | ○ | ○ | ― | ― | ― | ― |
doc.body |
― | × | × | ○ | ― | ― | ― | ― |
名前空間なし XPath | ― | ○ | ○ | ○ | ― | ― | ― | ― |
名前空間付き XPath | ― | × | ○ | ○ | ― | ― | ― | ― |
innerHTML |
― | ○ | ○ | ○ | ― | ― | ― | ― |
スクリプト | ― | × | × | × | ― | ― | ― | ― |
DOM 3 Core では、文書ノードに対する cloneNode
の動作は実装依存となっていますが、Firefox 3.5 以降では文書ノードを複製できます。なお、cloneNode
は例外を投げないことになっていますが、Opera では文書ノードを複製しようとすると例外が発生します。
iframe 要素
function createHTMLDocument_iframe(source) {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var doc = iframe.contentWindow.document;
doc.open();
doc.write(source);
doc.close();
document.body.removeChild(iframe);
return doc;
}
Internet Explorer | Firefox | Safari | Opera | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
6 | 7 | 8 | 3 | 3.5 | 3.7a | 3.7a HTML5 | 3.2 | 4 | 9.6 | 10 | |
doc |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | × |
doc.title |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ― |
doc.body |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ― |
名前空間なし XPath | ― | ― | ― | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ― |
名前空間付き XPath | ― | ― | ― | × | × | ○ | ○ | ○ | ○ | × | ― |
innerHTML |
○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ― |
スクリプト | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ | △ | ― |
iframe 要素を使って HTML 文書を生成します。IE でも動きます。iframe 要素を文書ツリーに追加しないと、doc
が null
になってしまうことがあります。Opera 9.6 では、スクリプトは実行されるものの、特定の操作 (document.write
など) を行うと HTML 文書をうまく取得できなくなってしまうようです。また、Opera 10 では、iframe 要素の load
イベントなどを見て非同期にアクセスしないと、うまく HTML 文書が取れないようです。
ActiveXObject
function createHTMLDocument_ActiveXObject(source) {
var doc = new ActiveXObject('htmlfile');
doc.open();
doc.write(source);
doc.close();
return doc;
}
Internet Explorer | |||
---|---|---|---|
6 | 7 | 8 | |
doc |
○ | ○ | ○ |
doc.title |
○ | ○ | ○ |
doc.body |
○ | ○ | ○ |
innerHTML |
○ | ○ | ○ |
スクリプト | ○ | ○ | ○ |
IE でのみ使用可能です。フラグメント識別子付きの URL を参照する iframe 要素を含むソースを指定すると、挙動がおかしくなるそうです。
CID からの作成
const NS_HTMLDOCUMENT_CID = '{5d0fcdd0-4daa-11d2-b328-00805f8a3859}';
var doc = Components.classesByID[NS_HTMLDOCUMENT_CID].createInstance();
Firefox で、さらには拡張機能や userChrome.js スクリプトといった chrome 権限を持ったスクリプトでのみ可能な方法です。xpcshell のような document
オブジェクトが存在しない環境でも HTML 文書を作成できます。この CID は、少なくとも Firefox 1.0 から 3.7a1pre まで変更されていません。これにより作られた文書は空なので、自分で要素を作って挿入する必要があります。
ちなみに、この方法を知ったのは、xpcshellでHTMLDocumentって無理なのかなぁ - The Other Road Ahead をきっかけに HTML 文書の作り方を調べていたときで、次のような経緯によります。
- Twitter: なんかコントラクトIDから直接 Documentを生成する手段があった気が → "@mozilla.org/dom"でMXR検索 → 見つからない → @mozilla\.org/.*document でGoogleソースコード検索 →
- Twitter: "@mozilla.org/xul/xul-document;1"を発見 → その付近のソースコードを見る → NS_HTMLDOCUMENT_CID を発見 → MXR検索でCIDの実際の値を確認 → とりあえずcreateInstance() → なんかできた ← 今ここ。
まとめ
createHTMLDocument
が使えるなら、それを使うのが楽です。- Firefox では、作った HTML 文書をどう扱いたいのかによって、作成手段が異なってきます。XPath で要素を取りたいだけなら
createDocument
でもいいですし、Firefox 3 以下でもHTMLDocument
固有の機能を使いたいというのなら XSLT を使ったほうがいいかもしれません。 - head 要素や body 要素がなくなるのが気になるなら、自分で作る必要があります。
コメント
_ os0x ― 2009年10月29日 11時09分
_ nanto_vi ― 2009年10月29日 11時45分
コメントをどうぞ
※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。
※投稿には管理者が設定した質問に答える必要があります。
トラックバック
このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2009/10/29/4660197/tb
new DOMParser().parseFromString(source, 'application/xhtml+xml');
もありですね。nanto_viさんがAutoPagerizeに送ったパッチからですが。
あと、iframeはobject要素に置き換えできますね。
ついでに、XPathで要素を取りたいだけなら、Firefox3.5ではDocumentを作らなくても動くみたいですね。こんな感じで動きました。
var src = '<html><head></head><body><p id="header">1</p></body></html>';
var html = document.createElement('html');
var body = html.appendChild(document.createElement('body'));
var range = document.createRange();
range.selectNodeContents(html);
body.appendChild(range.createContextualFragment(src));
console.log(html, html.querySelector('#header'), $X('id("header")',html)[0], $X('//p',html)[0]);