パスワードを忘れた? アカウント作成
433542 journal
政治

airheadの日記: bookmarklet: Wrap! ― 長い1byte文字列を折り返す(Mozilla) 1

日記 by airhead

レンダリングエンジンにGeckoを用いるMozilla系のブラウザは、空白文字以外の1byte文字が連続する文字列の途中を行折り返しの対象にしない。そのため利用者はしばしば、長いURLが直書きされたページなどで横スクロールバーが出てしまい前後の文章が読みにくくなる、という問題に直面する。そうなったときに「%」など特定文字の前後にWBR要素を挿入し、折り返しをブラウザに促すbookmarklet。とりあえずMozilla系専用。複行リストはこのエントリのコメントを参照。

javascript: threshold = new RegExp(/[\x21-\xff]{50}/); wrapChr = new RegExp(/([\/\?])|([&%])/g); avoidElm = new RegExp(/SCRIPT|INPUT|TEXTAREA|OPTION/); var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, true); while (cNode = walker.nextNode()) { s1 = cNode.nodeValue; pNode = cNode.parentNode; pNodeName = pNode.nodeName; if (!pNodeName.match(avoidElm) && s1.match(threshold)) { s1 = s1.replace(wrapChr, '$1<dummyWBR>$2'); s2 = s1.split('<dummyWBR>'); cNode.nodeValue = s2.pop(); for (i=0; i<s2.length; i++) { nNode = document.createTextNode(s2[i]); pNode.insertBefore(nNode, cNode); nNode = document.createElement('WBR'); pNode.insertBefore(nNode, cNode); } } } document.body.style.width = '100%'; focus();

補足

表示中のページについて、HTMLタグやコメントなどに挟まれたそれら以外の部分、つまり通常はそのままページに表示されるテキスト部分を取得し、対象となる文字があればその前か後にWBR要素を挿入している。WBR要素というのはNN~IEの独自要素で、折り返しを抑制するNOBR要素内においてブラウザに折り返し候補位置を指示する要素である(ただし現在のGeckoでのWBR要素は、NOBR要素内においては機能せず、単独で用いた場合にのみ折り返し候補位置を指示するものになっている)。

上記リストでは「/」「?」の直後、「&」「%」の直前を折り返し候補とするように書かれている。URL以外の文字列について「%」の直前を折り返し候補とするのは違和感があるかもしれないが、そういった文字列ではその前後に既にある折り返し候補(空白や日本語文字)で折り返されることも多いだろう。URLエンコードされた文字列では「%」の直前が自然ではないかと考えた。

対象となるテキスト部分は、DOMツリー上ではTEXTノードとして表される。TEXTノードのみを跳び歩いてDOMツリーを縦断するのにDOM Level 2 Traversal参考:@ITの記事)のTreeWalkerオブジェクトを使い、それぞれについて挿入候補位置があれば分割してWBR要素と分割済みテキストをノードとして挿入している。

1byte文字の連続が短めであれば、折り返しにほとんど影響しない。それらまで対象としていては処理時間が無駄に大きくなるため、空白以外の1byte文字が連続して50文字登場するノードのみを対象に処理を行っている。最後に、折り返し候補位置すべてにWBR要素を挿入してもそれだけではTABLE要素サイズの再計算は行われないので、BODY要素の幅を再設定してドキュメント領域の幅にあわせた折り返しを促している。

当bookmarkletを実行して完了までに要する時間は、ページの内容および1byte文字列の文字数条件に左右されるが、/.Jの過去の記事(ログアウトした状態でアクセスするとHTMLのみで約600KB)をローカルに保存し計測したところ当方の環境では6秒前後、ページのリロード・再表示完了に要する時間との比では 110%~130% 程度だった。文字数条件を50→100とするとそれだけ挿入対象ノードが減るが、処理時間は120%→110%程度までしか減少しない。あまりに大きい値を指定するとそれに満たない文字数の1byte文字列が折り返されず、当bookmarkletと使う意味がなくなってしまう。逆に条件を10としても処理時間は130%までしか増加しない。条件を1にするとさすがに150%程度に増加し、条件を撤廃すると200%程度まで増加した。

言いわけ

WBR要素はウェブ標準に取り入れられていないNN系~IEの独自要素なので、ページ閲覧者の使うブラウザがサポートしていると期待することはできない。したがってページ作成者がページ記述に用いるのは適切ではないが、ページ閲覧者が自身のブラウザの独自機能を活用してブラウザの欠点(あるいはページの不備)を補うくらいは許される範囲ではないかと思う。

IEは最新バージョンのIE6においてもDOM Level 2のサポートが充分でなく、当bookmarkletは動作しない。しかしIEではもとより前後が折り返し候補となる文字が多く、同種の問題は少ない。そもそもIEユーザは多く、IEで問題を起こす記述は回避される傾向にあるので、当bookmarkletの出番はないだろう。

Operaはいくつかの文字の前後を折り返し候補とするものの、IEほどではない。そのため同じ問題が発生する可能性はあるのだが、OperaはWBR要素をサポートしておらず当bookmarkletそのままでは機能しない。WBR要素という邪道のさらに上を行く邪道になるが、とりあえずはSRC属性なしでサイズ0のIMG要素( <img width=0 height=0> )がWBR要素の代用として改行候補を作ってくれる(はずだ。OperaのJavaScript実装で「%」を含む文字列を扱うと化けてしまう問題があり、その回避のしかたがわからないので当bookmarkletでは対象外とした)。

当初は別の手法で実現できないかと模索していた。具体的には、 document.body.innerHTML でBODY要素内全体を取得し、タグの終端を示す文字「>」で分割した後にタグの始端を示す文字「<」で分割することで個々のタグ間の内容を取得し、それぞれにおいて置換を行い特定文字の前後にWBR要素を挿入し、逆のプロセスで書き戻す(「<」「>」で結合したものを document.body.innerHTML に設定する)、というものだった。

その手法では、BODY要素内に書かれたSCRIPT要素の内容が書き換えられることの回避は難しそうだ。 split('>') で分割し配列にしていたのだが、分割点がタグ終端の「>」だったのかスクリプト中の「>」だったのか、配列を前にさかのぼっていって判断しなければならない。TEXTAREA要素なども回避したいが、innerHTMLを使う手法でそれら要素の開始・終了を検出するような記述をするのは面倒だし、処理も重くなりそうだ。DOMツリーをたどる手法であれば、親ノードを調べることだけで回避できる。

簡単に検証したところ一旦表示されたページ内のスクリプトはそれ以降ソースを参照しないようで、書き換えたところでたいした実害はないかもしれないが、気持ち悪い。スクリプト内の「<」「>」の登場順によってはbookmarkletの方が思わぬ動作をするかもしれない。結局現在の手法で書き直した。

ページ内スクリプトの記述法としては、SCRIPT要素を認識しないブラウザのために <script><!-- (スクリプト) // --></script> とすることが勧められているが、そのようなページについてもGeckoのDOMはSCRIPT要素内すべてをTEXTノードとして扱うようだ。そこで当bookmarkletでは、親ノードがSCRIPT要素の場合は対象外としている。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • javascript:
    threshold = new RegExp(/[\x21-\xff]{50}/);
    wrapChr = new RegExp(/([\/\?])|([&%])/g);
    avoidElm = new RegExp(/SCRIPT|INPUT|TEXTAREA|OPTION/);
    var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, true);

    while (cNode = walker.nextNode()) {
        s1 = cNode.nodeValue;
        pNode = cNode.parentNode;
        pNodeName = pNode.nodeName;

        if (!pNodeName.match(avoidElm) && s1.match(threshold)) {
            s1 = s1.replace(wrapChr, '$1<dummyWBR>$2');
            s2 = s1.split('<dummyWBR>');
            cNode.nodeValue = s2.pop();

            for (i=0; i<s2.length; i++) {
                nNode = document.createTextNode(s2[i]);
                pNode.insertBefore(nNode, cNode);
                nNode = document.createElement('WBR');
                pNode.insertBefore(nNode, cNode);
            }
        }
    }

    document.body.style.width = '100%';
    focus();
typodupeerror

192.168.0.1は、私が使っている IPアドレスですので勝手に使わないでください --- ある通りすがり

読み込み中...