勝手に添削: Selection内のHTML Textをいい感じに取得する2010年02月05日 09時15分

というわけでやってまいりましたこのコーナー! 本日のお題は「Selection内のHTML Textをいい感じに取得する - 枕を欹てて聴く」でございます。選択範囲のHTMLソースを抜き出すというやつですね。では早速いってみましょう!

  if(src.focusNode){
    // selection

まずは HTML5 テキスト選択 APISelection オブジェクトが登場! 以後これに対する操作が続きます。しかしこの Slection オブジェクト、getRangeAt メソッドを使うとなんと選択範囲に対応する DOM 2 Traversal and RangeRange オブジェクトが取れちゃうんです!

    // common parent node search
    (以下 21 行省略)

それ Range#commonAncestorContainer で取れるよ!

      // common配下のindexを見て, focus と anchorがどちらが前方かを調べる
      (以下 9 行省略)

それ Range#startContainerRange#endContainer でわかるよ!

      // focusに沿って後方をremove
      (以下15行省略)
      // anchorに沿って前方をremove
      (以下11行省略)

それ Range#extractContents()Range#cloneContents() でできるよ!

というわけで不要な要素を除去する部分は置いといて今までの経過をまとめてみると、

function convertToHTMLString(source, safe) {
  if (!source || (source.getRangeAt && source.isCollapsed)) return '';
  var range = source.getRangeAt ? source.getRangeAt(0) : null;
  var node = range ? range.cloneContents() : source.cloneNode(true);
  if (safe) { ... }
  return new XMLSerializer().serializeToString(node);
}

なんということでしょう! コメント空行抜きでも 86 行あった部分が実質たったの 2 行に!

これに残った部分を付け足していくわけですが、出来上がった品はこちらになります。(お試しはこちら!)

まとめ

以上、もろもろの要因で無駄にハイテンションの nanto_vi (なんと) がお届けしました!

Kanasan.JS Jetpack ワークショップ2009年12月30日 18時08分

Kanasan.JS Jetpack ワークショップに行ってきました。Jetpack は Firefox 用の簡易拡張プラットフォーム。私としては通常の拡張機能のほうが高い自由度を持てて好きなのですが、Firefox を広く一般に使ってもらうには、簡単なものを簡単に作れるようにするという点が重要なのでしょう。

最初に mollifier さんによる Jetpack 入門があった後、二人組になり各グループで Jetpack フィーチャーを作っていくという流れ。私は satyr さんとペアになったのですが、さすが satyr さん、ゴルファーだけあって見慣れぬ記法を次々と使ってきます。

var { href } = location;

上のコードは分割代入の省略形式で、

var { href: href } = location;

の略、さらには、

var href = location.href;

の略なのですが、JavaScript 1.8 以降 (Firefox 3 以降) での機能とあって、私自身も久しく頭から抜けていました。

そんなこんなで完成した (といっても実際のコーディングはほとんど satyr さんでしたが) のが、現在閲覧中のページの W3C Markup Validation Service での検証結果をステータスバーに表示する Jetpack フィーチャー、HTMLValidator for Jetpack です。

ちなみに開発中に引っかかったのが、特定の名前空間に属する要素の jQuery での扱い。http://validator.w3.org/check?uri=http%3A%2F%2Fnanto.asablo.jp%2Fblog%2F&output=soap12 のように output パラメータに値 soap12 を指定すると、検証結果が、

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<m:markupvalidationresponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding" xmlns:m="http://www.w3.org/2005/10/markup-validator">
    <m:uri>http://nanto.asablo.jp/blog/</m:uri>
    <m:checkedby>http://validator.w3.org/</m:checkedby>
    <m:doctype>-//W3C//DTD HTML 4.01 Transitional//EN</m:doctype>
    <m:charset>utf-8</m:charset>
    <m:validity>true</m:validity>
    ...
</m:markupvalidationresponse>
</env:Body>
</env:Envelope>

と XML 形式で返ってくるのですが、ここから jQuery で validity 要素を選択するには、res を返ってきた XML 文書ノードオブジェクトとして、

$(res).find("m\\:validity")

のように指定する必要があるのでした。

そもそも DOM の getElementsByTagName メソッドからして、引数に指定できるのは要素名 (非修飾名) のみと勘違いしていたのですが、実際は、名前空間に属する要素を選択するためには接頭辞も含めた修飾名を指定する必要があるとのこと。名前空間の指定に、語彙に対して常に一意な名前空間 URI ではなく、文書片ごとに変わる可能性のある名前空間接頭辞を用いるのは、大変気持ち悪いです。

初めての JavaScript 第 2 版2009年12月30日 17時07分

O'Reilly Japan から書籍『初めての JavaScript 第 2 版』(Shelley Powers 著、武舎広幸・武舎るみ訳) (Amazon) が出版されています。出版にあたって、私が技術レビューを務めさせていただきました。販促効果があるのかわかりませんが、帯に推薦文も寄せています。

単なる JavaScript の書き方にとどまらず、デバッガの使い方など基礎になる事柄を幅広く取り扱っていますので、JavaScript を始めたいという方は一度手にとってみることをお勧めします。また、周囲にそのような方がいる場合もぜひ勧めてあげてください。

読み終えた方は、『JavaScript 第 5 版』(Amazon) や『JavaScript: The Good Parts』(Amazon) に進むのもいいですし、Prototype.js などのコードリーディングをしてみるのもいいと思います。なお、ここで Prototype.js の名前を挙げたのは、次の二つの理由によります。

  1. ソースコードが比較的読みやすい。
  2. 既にコードリーディングを行った人が大勢いて、Web 上に多数のコード解説がある。

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 要素がなくなるのが気になるなら、自分で作る必要があります。

Kanasan.JS JavaScript 第 5 版読書会 #82009年09月30日 03時41分

Kanasan.JS JavaScript 第 5 版読書会 #8 に行ってきました。今回は初めての京都での開催、それも町家でという風情あふれる読書会。真夏真冬はちょっとつらいかもしれませんが、春秋はこういった場所での勉強会というのも乙なものですね。名古屋の DeLLa.JS では茶室での開催もあったそうでうらやましいことです。

ちなみに会場「お結び庵」の運営は home's vi ("h" を抜くと「おむすび」) という法人によるものだそうで。なんとなく名前に親近感を感じてしまうのは気のせいでしょうか。

読書範囲は「17.5 キーイベント」から「18章 フォームとフォーム要素」まで。ブラウザごとに変な癖があるということが少なく (完全にばらばらか結構統一されているか)、さくさくと読めました。

キーボードショートカット

サイ本では、Web アプリケーションはキーボードショートカットをサポートすべきといっており、WCAG (ウェブコンテンツ・アクセシビリティ・ガイドライン) 2.0 (日本語訳) にもすべての機能をキーボードから利用できるようにするという指針があります。

しかし、製作者側でのショートカットキーの指定は、(特にアルファベットを含む場合) ブラウザやユーザー側で設定されたショートカットキーとの衝突や、サイト間での一貫性の欠如といった問題をはらんでいます。

個人的に、製作者側は、キーボードアクセス可能にするにあたって、

Tab
ウィジェット間の移動
矢印
ウィジェット内の項目の移動
Enter
確定、実行
スペース
状態の切り替え
Delete
削除

といったある程度意味の定まったキーに対しては動作を指定すべきだが、アルファベットを含むショートカットキーは使用を避けるか、少なくとも無効にできるようにすべきだと思っています。

関連リンク

自前でキーボードショートカットを実装するウェブサイトは、キーボードでブラウジングする人のことをもっと考えてあげてください - by edvakf in hatena
Web サイトが提供するキーボードショートカットを使うのは、普段からキーボードでブラウジングしている人たちであるという指摘と、キーボードショートカットを提供するスクリプトの書き方について。
accesskeyにはアクセシビリティが無い - Archiva
HTML の accesskey 属性の問題点に関して。
accesskeyの使い方を比較(Google,Yahoo!,MySpace,mixi,モバゲー,ニコ動,etc) | 携帯サイトをつくろう。
携帯サイトではショートカットキーがある程度統一されているという話。

delete 演算子の使用

「17.6 onload イベント」のサンプルコードで、不要になったプロパティを delete 演算子で削除していますが、個人的には null を代入するほうが好みです。

最近の JavaScript エンジンでは、同じ構造を持つオブジェクト同士は同一の「クラス」に属するものとして扱います。あるオブジェクトのプロパティを delete 演算子を使って削除すると、そのオブジェクトの構造と属する「クラス」が変化することになり、速度的に不利になる場合があります。とはいってもスクリプト全体の実行時間からすれば気にするほどではなく、どちらを選ぶかは趣味の問題でしょう。

テキスト入力に対するイベント

テキスト入力欄 (input type="text"、textarea) の入力値の変化を通知するものとして change イベントがありますが、これは入力欄からフォーカスが外れるタイミングでしか発生しません。キー入力に逐次対応したいときは keydown、keypress イベントを、それ以外の手段による変更にも対応したいときはさらにタイマー (setTimeout、setInterval) を組み合わせるのが一般的なようです。

手段を問わず (少なくとも一定時間内の) 入力値の変化を監視するために、HTML5 では input イベントが定義されています。Firefox 1.0、Safari 3、Opera 9 以上が対応していますが、Opera では (10.00の時点でも) 切り取りやドラッグ & ドロップによる値の変更に反応しない (その場合フォーカスが外れた時点で input イベントが発生する)、WebKit (Safari、Chrome) では IME で未確定の入力にも反応するなど、実装にばらつきがあります。

ちなみに、切り取り、貼り付けに反応するものとして、IE の cutpaste イベントがあり、Firefox 3 以上、Safari もこれを実装しています (element.onpaste - MDC)。このイベントはバブルしますが、IE では文書要素 (html 要素) までしかバブルしないようです。

name 属性の名前空間

サイ本 18.3.1 節 (日本語版 p. 466) には

<form name="everything">

document.everything

で参照する例が出てきますが、この方法では document のプロパティ名とフォーム名が競合したときに困るので、name 属性ではなく id 属性を使い、document.getElementById などでアクセスしたほうがいいです。

勘違いされがちですが、form、img、a 要素の name 属性が要素を一意に識別するためのものなのに対し、input、textarea、select 要素の name 属性はサーバーに送信するコントロール名を指定するためのものと、同名の属性でも役割がまったく異なります。form 要素に限らず、前者に関しては id 属性を使ったほうが、他の要素との一貫性も取れて楽でしょう。

ただし、IE 7 以下では name 属性も id 属性と同じ名前空間に入ってくる (document.getElementById("foo") で name="foo" な要素が返ることがある) ので、フォームコントロールの name 属性と、ある要素の id 属性とが同じ値を取らないようにしたほうがいいかもしれません。

フォームコントロールの値中での改行

フォームコントロールの値が改行を含むとき、その改行コードはブラウザによってさばらばらです。IE の場合、value プロパティに \r\n を含む文字列を設定しても、取得時にはすべて \r\n になっています。このような状況に対応するため、私は次のように改行コードを統一することがあります。

var value = control.value.replace(/\r\n?/g, "\n");

なお、この改行コード変換用正規表現は、Perl では正しくないそうですが、JavaScript では問題ないと思います。

IE での属性セレクタ

IE 7 以上は CSS の属性セレクタに対応していますが、スクリプトから属性に変更を加えても表示が更新されないというバグがあります (IE 8 でも発生)。その場合でも class 属性などをいじると、それまでの変更が表示に反映されるようです。

<style type="text/css">
[title="foo"] { color: #c63; }
</style>
element.setAttribute('title', 'foo');
// IE では、ここで終了すると element の色が変化しない

element.className += '';
// この時点で element の色が変化する