ブラウザにおける空白文字に関する考察

趣旨

ブラウザによる空白文字の取り扱いは、なかなか一筋縄ではいかないので、これを整理してみる。

情報ソース

ブラウザの問題:半角スペース、全角スペース、改行コード、整形処理
よくまとまっている。おすすめ。

ブラウザ上の空白文字の表示

一般的なブラウザ(Firefox 2 や IE6 など)で次のような HTML 文書がどのように表示されるだろうか?
(\n; は改行(LF, 0x0A), \tはタブ(0x09), \s は半角スペース(0x20)をそれぞれ表すとする)

A\nB\tC\sD

答えは、"A B C D"。つまり A, B, C, D のそれぞれに半角スペースが1つずつ存在する。これは簡単だ。

では、半角文字にかえて、全角文字を使ってみたらどうであろうか?

あ\nい\tう\sえ

答えは、"あい う え" になる。ここで、「あ」と「い」の間に半角スペースが入っていないことに注意してほしい。つまり改行文字に関してはその前後に全角文字が来るか半角文字が来るかで、ブラウザの表示が変わってくるのである。これは、全角文字を主に使い、分かち書きをしない言語(例:日本語・中国語)では、文中の任意の場所に改行文字が置かれることがあるので、この改行文字を表示すると、表示が不自然になってしまうためだろう。(それに対して分かち書きをするヨーロッパ系言語では、単語の途中に改行が置かれることは原則としてない)*1

半角文字と全角文字の間に改行文字が置かれていたら、どうであろうか?

あ\na\nい

この結果は "あ a い" となる。つまり、半角文字と全角文字の間には半角スペースが1個置かれる。

ちなみに上のすべてのケースにおいて、文字と文字の間に改行文字が2個以上置かれた場合は、表示上は、1個の半角スペースに置き換えられる。この事情は、半角でも全角でも同じである。なお、上では改行文字は LF(0x0A) を使っているが、実験してみたところ Firefox 2 / IE 6 の両方で 、改行文字としては、LF + CR(0x0D, 0x0A) でも CR(0x0D) でもまったく同じ振る舞いであった。

インラインレベル要素のタグをめぐる問題

原則は上のとおりなのだが、<A> <SPAN> <IMG> のようなインラインレベル要素タグが全角文字間に挿入されると事情が少し変わる。

あ\n<a href="#">い</a>
う<span>\nえ</span>

上の例はそれぞれ "あ い", "う え" と半角スペースが挿入される形で表示される。つまり、半角文字間の改行文字と同じ扱いになる。

Firefox 2 における空白文字の扱い(TextNode)

DOM 上は空白文字はどう表現されているだろうか? FirefoxIE で別々に見てみよう。まずは Firefox 2.0 (Windows XP SP2) から。

次のような、サンプルコードを考える

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>whitespace test</title>
<script type="text/javascript">

function leftDollar(s, n) {
	var len = s.length;
	return s.substring(len - n, len);
}

function getTextInHex(s) {
	var res = [];
	for(var i = 0; i < s.length; i++) {
		var c = s.charCodeAt(i);
		var item = leftDollar("000" + c.toString(16), 4).toUpperCase();
		res.push(item);
	}
	return res.join(" ");
}

var Event = {};
Event.observe = function(element, name, observer) {
	if(element.addEventListener) {
		element.addEventListener(name, observer, false);
	} else if (element.attachEvent) {
    element.attachEvent('on' + name, observer);
  }
};

function eachTextNode(element, callback, nodeNum) {
	nodeNum = isNaN(nodeNum) ? 1 : nodeNum;
	var nodes = element.childNodes;
	for(var i = 0, len = nodes.length; i < len; i++) {
		var node = nodes.item(i);
		switch(node.nodeType) {
			case 1: // Element
				nodeNum = eachTextNode(node, callback, nodeNum);
			break;
			case 3: // Text
				callback(node, nodeNum++);
			break;
		}
	}
	return nodeNum;
}

function debug(msg) {
	if(typeof console != "undefined" && console.debug) {
		// Firefox console
		console.debug(msg)
	} else {
		alert(msg);
	}
}
		
function init() {
	eachTextNode(document.body, function(textNode, i) {
		debug("Text node " + i + ": " + getTextInHex(textNode.data));
	});
}

Event.observe(window, "load", init);

</script>
</head>
<body>
a
b	c d
</body>
</html>

これは要するに、document.body の下にある DOM の TextNode を再帰的にすべて取得して、その中身のテキストを 16 進数で表示するプログラムである。まずは、body の中身が

\nA\nB\tC\sD\n

の場合。(BODY の開きタグおよび閉じタグの内側には通常改行が置かれると考えられるので \n を文の前後に配置した)

結果は以下の通り。

Text node 1: 000A 0061 000A 0062 0009 0063 0020 0064 000A
Text node 2: 000A

Text node 1 は上の BODY 要素の中身そのものである。0x61, 0x62 ... は "A", "B" ... を表している。面白いのは、Firefox の改行の内部表現はかならず 0x0A であるらしい。HTML ファイル自体の改行文字を CR (0x0D) にしても、CR + LF(0x0D + 0x0A) にしても、これは変化なかった。

Text node 2 はなんだろう?実は、これは必ず存在するものらしい。BODY 要素の中身を完全に空にしても、0x0A を1つだけ含むテキストノードが1つだけ存在することを、実験で確かめた。

次に全角文字についてだ。

\nあ\nい\tう\sえ\na\nお\n

はどうなるか。

Text node 1: 000A 3042 000A 3044 0009 3046 0020 3048 000A 0061 000A 304A 000A
Text node 2: 000A

Javascript では、文字はすべて Unicode で内部的に表現されている。0x3042, 0x3044, 0x3046, 0x3048, 0x.304A はそれぞれ Unicode で「あ」「い」「う」「え」「お」に当たる。(Unicode 一覧)

Text node 1 を見ると、ソースコードそのものであることがわかる。画面上は半角スペースとして表示される部分も、内部的にはやはり改行であったりタブであったりする。

では、A 要素や SPAN 要素の内側に改行がある場合はどうだろうか?

あ\n<a href="#">い</a>う<span>\nえ</span>

結果は、

Text node 1: 3042 000A
Text node 2: 3044
Text node 3: 3046
Text node 4: 000A 3048
Text node 5: 000A

となった。これもソースコードそのままである。しかし A 要素や SPAN 要素が Text Node を細分化しているのがわかる。

以上の実験結果をまとめると、FireFox ではソースコードの内容が比較的忠実にそのまま TextNode に反映されていることがわかる。

IE6 における空白文字の扱い(TextNode)

上と同じサンプルコードを IE6 で走らせてみる。まずは半角文字から。document.body の中身が次の場合。

\nA\nB\tC\sD\n

結果は、

Text node 1: 0061 0020 0062 0020 0063 0020 0064 0020

となる。驚いたことに Firefox 2.0 の出力結果とまったく異なるのだ。先頭の改行文字は無視され、残りの改行文字やタブは、半角スペース(0x20)にすべて変換されてしまっている。これはブラウザ上の表示に一致する。つまり IE の TextNode に対する考え方は、それをなるべくブラウザの表示結果と一致させようということらしい。

同様に、全角文字については、

\nあ\nい\tう\sえ\na\nお\n

の結果は、

Text node 1: 3042 3044 0020 3046 0020 3048 0020

となる。「あ」(0x3042) と「い」(0x3044) の間の改行文字が消えてしまっていることに注意してほしい。これはなるほどブラウザ上の表示に一致している。

最後に、A 要素や SPAN 要素の内側に改行がある場合について。

あ\n<a href="#">い</a>う<span>\nえ</span>

結果は、

Text node 1: 3042 0020
Text node 2: 3044
Text node 3: 3046
Text node 4: 0020 3048
Text node 5: 0020

となった。やはり改行やタブが半角スペースに置き換えられている。不思議なのは Text node 5 の 0x0020 だ。これはなんでくっついて来るのかよくわからない。ちなみに IE6 の場合 BODY 要素を完全に空にした場合は、Text Node は1個も存在しないという結果だった。

IE6 における空白文字の扱い(TextRange)

実は IE6 の場合、テキスト扱う別のインターフェイスが存在する。それは選択文字列などの取得に使用する TextRange オブジェクトである。

上のサンプルコードを次のように修正してみる。

function documentMouseUp(event) {
	var textRange = document.selection.createRange();
	debug("selection: " + getTextInHex(textRange.text));
}

function init() {
	Event.observe(document, "mouseup", documentMouseUp);
}

そして以下のサンプルにおいて

  1. Ctrl+A ですべての文字を選択。
  2. マウスクリックで、選択文字列の内容を取得。

という操作を行う。

半角文字:

\nA\nB\tC\sD\n

の結果は、

selection: 0061 0020 0062 0020 0063 0020 0064 0020

全角文字:

\nあ\nい\tう\sえ\na\nお\n

の結果は、

selection: 3042 3044 0020 3046 0020 3048 0020

インラインレベル要素:

あ\n<a href="#">い</a>う<span>\nえ</span>

結果は、

selection: 3042 0020 3044 3046 0020 3048 0020

となった。インラインレベル要素の例で、テキストノードが1つになったことを除くと、TextNode の例とまったく同様に動作している。(だた、TextNode と違う動きを見せたことを見たこともあるような・・・。これだけの実験では TextNode と TextRange がテキストの取得に関してまったく同様に動くとまでは断言できない)

まとめ

  1. ブラウザの表示では、基本的にソース上の空白文字は1個にまとめられる。
  2. 全角文字と半角文字では、その間に来る改行文字の表示方法が異なることがある。
  3. Firefox でテキストを DOM インターフェイスで取得すると、ソースの空白文字(改行・タブ・半角スペース)がそのまま取得される。(ソース重視)
  4. IE でテキストを DOM インターフェイスで取得すると、ソースの空白文字(改行・タブ・半角スペース)は、省略されるか、または半角スペースに変換されている。(表示重視)

*1:古いブラウザでは、全角文字間の改行を半角スペースとして表示していたような微かな記憶がある