HTMLDocument の動的な作成2009年10月29日 06時11分

ブラウザ上で、JavaScript を使って HTML のソースから HTML 文書を生成するのに、どんな方法があるのか調べました。なお、以下のスクリプトは HTML 文書上で実行することが前提です。

  1. 表の見方
  2. XSLT の HTML 出力
  3. createHTMLDocument メソッド
  4. createDocument メソッド
  5. createDocument メソッドと名前空間の指定
  6. createDocument メソッドと文書型宣言の指定
  7. createDocument メソッドと文書型宣言及び名前空間の指定
  8. cloneNode メソッド
  9. iframe 要素
  10. ActiveXObject
  11. CID からの作成
  12. まとめ

表の見方

以下の表において、各項目の意味は次の通りです。

doc
HTML 文書を作成できれば○、XML 文書を作成できれば△、それ以外なら×です。HTML 文書かどうかは、createElement メソッドが HTML 要素を作成するかどうかで判断しています。
doc.title
文書ノードの title プロパティから title 要素の内容を取得できれば○、できなければ×です。
doc.body
文書ノードの body プロパティから body 要素を取得できれば○、できなければ×です。
名前空間なし XPath
//p のような名前空間接頭辞の付かない XPath 式で HTML 要素を取得できれば○、できなければ×です。
名前空間付き XPath
//h:p のような名前空間接頭辞の付いた XPath 式で HTML 要素を取得できれば○、できなければ×です。ここで、接頭辞 h は名前空間 URI http://www.w3.org/1999/xhtml に関連付けられているものとします。
innerHTML
innerHTML プロパティに <p>hello&nbsp;world のような、整形式 XML ではなく、かつ HTML の実体参照を含んだ文字列を設定して、エラーが出なければ○、エラーになれば×です。
スクリプト
script 要素で指定されたスクリプトが実行されれば○、されなければ×です。

また、使用したブラウザは次の通りです。いずれも Windows 版です。

  • Firefox 3.0.11
  • Firefox 3.5.3
  • Minefield (Firefox の開発版) 3.7a1pre、HTML5 パーサ無効
  • Minefield 3.7a1pre、HTML5 パーサ有効 (html5.enabletrue に設定)
  • Opera 9.64
  • Opera 10.00
  • Safari 3.2.3
  • Safari 4.0.3
  • Internet Explorer 6 SP3
  • Internet Explorer 7
  • Internet Explorer 8

調査には次のファイルを使いました。

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 上でエラーが出るそうです。

XSLTProcessorDOMParserRange#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 要素を文書ツリーに追加しないと、docnull になってしまうことがあります。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 文書の作り方を調べていたときで、次のような経緯によります。

  1. Twitter: なんかコントラクトIDから直接 Documentを生成する手段があった気が → "@mozilla.org/dom"でMXR検索 → 見つからない → @mozilla\.org/.*document でGoogleソースコード検索 →
  2. Twitter: "@mozilla.org/xul/xul-document;1"を発見 → その付近のソースコードを見る → NS_HTMLDOCUMENT_CID を発見 → MXR検索でCIDの実際の値を確認 → とりあえずcreateInstance() → なんかできた ← 今ここ。

まとめ

  • createHTMLDocument が使えるなら、それを使うのが楽です。
  • Firefox では、作った HTML 文書をどう扱いたいのかによって、作成手段が異なってきます。XPath で要素を取りたいだけなら createDocument でもいいですし、Firefox 3 以下でも HTMLDocument 固有の機能を使いたいというのなら XSLT を使ったほうがいいかもしれません。
  • head 要素や body 要素がなくなるのが気になるなら、自分で作る必要があります。