2文字目にクラスを付ける


0   名前: かぼちゃ : 2010/02/23(火) 07:39  ID:IHdaIrjU sub-Gi
h2:first-letter のようにcssで1文字目だけスタイルを変える方法はありますよね。
これと同じように2文字目だけスタイルを変えたいのですが、たぶんcssでは出来ないのでjavascriptを使おうと思ってます。

とりあえず2文字目を取得してクラスを付けることが出来れば、あとはcssでスタイルを指定できると思ったのですが、肝心のjavascript部分が分かりません。
書き方を教えて頂ければと思います。
ちなみにjQueryを使ってるので、スマートに書けるのであればjQueryを使用したいです。

宜しく御願いします。


javascript 2文字目を取得、などで検索しましたがヒントになりそうなサイトはヒットしませんでした。

1   名前: ! : 2010/02/23(火) 07:39  ID:x18I3gbE sub-T.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<title>TEST</title>

<style type="text/css">

.fifth-letter {
  background: #aaf;
}

</style>

<p>1234<em>567</em>89</p>


<script type="text/javascript">

function getNthLetterPoint(root, nth) {
    var node = root;
    var count = nth - 1;
    var tmp;
    
    if (count < 0) {
        return null;
    }
    while (node) {
        switch (node.nodeType) {
        case 3 :
        case 4 :
            if (node.length > count) {
                return [node, count, nth];
            }
            count -= node.length;
        }
        if ((tmp = node.firstChild)) {
            node = tmp;
            continue;
        }
        do {
            if (node === root) {
                node = null;
                break;
            }
            if ((tmp = node.nextSibling)) {
                node = tmp;
                break;
            }
        } while ((node = node.parentNode));
    }
    return null;
};

function surroundLetter(point) {
    if (point) {
        var text   = point[0];
        var offset = point[1];
        var nth    = point[2];
        var doc    = text.ownerDocument;
        var span   = doc.createElement('span');
        
        try {
            var range = text.ownerDocument.createRange();
            range.setStart(text, offset);
            range.setEnd(text, offset + 1);
            range.surroundContents(span);
            range.detach();
        } catch (err) {
            var newText = text.splitText(offset);
            span.appendChild(doc.createTextNode(newText.substringData(0, 1)));
            newText.deleteData(0, 1);
            newText.parentNode.insertBefore(span, newText);
        }
        return span;
    }
    return null;
}


// p 要素の 5 番目の文字
var span = surroundLetter(getNthLetterPoint(document.getElementsByTagName('p')[0], 5));

if (span) {
  span.className = 'fifth-letter';
}

</script>

jQuery がスマートだとは全く思いませんが、組み込みたければ好きにして下さい。上記は jQuery で代替できる部分ではない(と思う)ので、jQuery 用に書き換える部分はほとんどないはず。そのまま組み込めるはずです。

2   名前: ! : 2010/02/23(火) 07:39  ID:x18I3gbE sub-T.
言うまでもありませんが一応。jQuery で独自の疑似要素
jQuery('p::-tagindex-nth-letter(5)')

を使えるようにするには、
jQuery.expr.filters['-tagindex-nth-letter'] = function(node, position, args, nodeset) {
  // args => ["::-tagindex-nth-letter(2n+1)", "-tagindex-nth-letter", "", "5"]
};

を定義すれば良い。この関数には、タイプセレクタ p でマッチした p 要素の集合を nodeset として、各 p 要素と番号が node と position として順番に送られてくるので、その p 要素が必要なら true を、不要なら false を返す。

……と言うのはこの際どうでも良くて(どうせ jQuery は疑似要素を返せない)、ここで大事なのは、3 番目の引数である配列 args に、疑似要素の名前 "-tagindex-nth-letter(5)" と引数の "5" が入っていること。

主語である p 要素と、-tagindex-nth-letter() の引数の 5。何をすれば良いか、もうお分かりですね。



セレクタを拡張するなら接頭辞を付けて下さい。CSS 2.1 がせっかく独自拡張と相互運用性のためにベンダ接頭辞の仕組みを導入したのに、それをガン無視して独自疑似クラスを追加する jQuery 本家のようなやり方は避けるべきでしょう。

現在の Selectors 仕様では、疑似要素はセレクタに 1 つだけ、かつ主語(最後)にのみ出現が許されています。ならば、おそらくは
querySelector('p')
  .realizePseudoElement('-tagindex-nth-letter(5)')
  .asElement('span')
  .asClass('fifth-letter')

のようにでもして疑似要素を切り離した方が(いろんな意味で)無難なのでしょうが。

3   名前: かぼちゃ : 2010/02/23(火) 07:39  ID:IHdaIrjU sub-Gi
さっそくの解答ありがとうございます。
わざわざjQueryのコードも書いて下さり本当に助かりました。たぶん分かっていると思うので、これで試してみようと思います。

予想ではもっと短く簡単なスクリプトで出来ると思っていたんですが、結構長くて複雑ですよね。まだまだ勉強不足だなと痛感しました。
今回はこれで解決しました。!さん、ありがとうございました。

4   名前: ! : 2010/02/23(火) 07:39  ID:x18I3gbE sub-T.
一応言っておくと、>>1 には不具合があります。

・空白類を数えます。従って、DOM 木構築時に空白類を除去する IE と、除去しない XHTML ブラウザとの間で差異が出ます。

CSS 2.1: 16.1.1 を参考に、white-space の計算値から適切に空白を扱えるよう修正して下さい。最低でも、空白類だけのテキストノードを除外する処理は入れるべきでしょう。>>1 はそれもサボりましたがね。

・文書に動的な変更が加えられてもアップデートしません。つまり、後から文字を追加・削除したときに文字数がずれる可能性があります。

MutationEvent 等を駆使してうまくやって下さい。

真面目にやるなら、ちょっとした CSS レンダリングエンジン程度にはなりますよ。

5   名前: ! : 2010/02/23(火) 07:39  ID:x18I3gbE sub-T.
やはり空白類を数えないよう >>1 を修正しておく。
var AsCSS_makeOffsetMapping = (function () {
    var S = '\\u0000-\\u0020\\u0085\\u034F\\u2000-\\u200F\\u2060-\\u2063';
    var W = new RegExp ('^[' + S + ']+');
    var C = new RegExp ('^[^' + S + ']+');
    
    return function (data, offset) {
        var sourceCount  = 0;
        var mappedOffset = 0;
        var s;
        var r;
        var o;
        
        for (s = data; s.length > 0; s = s.slice (r)) {
            if ((r = W.exec (s))) {
                r = r[0].length;
                sourceCount += r;
                continue;
            }
            if ((r = C.exec (s))) {
                r = r[0].length;
                
                if (mappedOffset + r > offset) {
                    break;
                }
                sourceCount += r;
                mappedOffset += r;
                continue;
            }
            throw new Error ('something was wrong.');
        }
        o = offset - mappedOffset;
        return [ o, o + sourceCount ];
    };
})();


var DOMCSSNode_getNthLetterCaretPosition = function (root, count, mapper) {
    var node   = root;
    var offset = count - 1;
    var n;
    var o;
    
    if (offset < 0) {
        return null;
    }
    while (node) {
        switch (node.nodeType) {
        case 3 :
        case 4 :
            o = mapper (node.data, offset);
            
            if (o[1] < node.length) {
                return [ node, o[1] ];
            }
            offset = o[0];
        }
        if ((n = node.firstChild)) {
            node = n;
            continue;
        }
        do {
            if (node === root) {
                node = null;
                break;
            }
            if ((n = node.nextSibling)) {
                node = n;
                break;
            }
        } while ((node = node.parentNode));
    }
    return null;
};


var DOMCSSCaretPosition_surroundLetter = function (point, newParent) {
    var text   = point[0];
    var offset = point[1];
    var doc    = text.ownerDocument;
    
    if (doc.implementation.hasFeature ('Range', '2.0')) {
        var range = text.ownerDocument.createRange ();
        range.setStart (text, offset);
        range.setEnd (text, offset + 1);
        range.surroundContents (newParent);
        range.detach ();
    } else {
        var newText = text.splitText (offset);
        
        if (newParent.hasChildNodes ()) {
            newParent.removeChild (newParent.firstChild);
        }
        newParent.appendChild (doc.createTextNode (newText.substringData (0, 1)));
        newText.deleteData (0, 1);
        newText.parentNode.insertBefore (newParent, newText);
    }
    return newParent;
};


var DOMCSSHTMLNode_surroundNthLetter = function (node, count, tagName) {
    var point = DOMCSSNode_getNthLetterCaretPosition (node, count, AsCSS_makeOffsetMapping);
    var newElt;
    
    if (point) {
        newElt = node.ownerDocument.createElement (tagName || 'span');
        return DOMCSSCaretPosition_surroundLetter (point, newElt);
    }
    return null;
};


//______________________________________________________________________


var node = DOMCSSHTMLNode_surroundNthLetter (
  document.getElementsByTagName ('p')[5],
  5
);

if (node) {
    node.className = 'fifth-letter';
}

スキップするのは空白類のみ。



余談。::first-letter の仕様を見てみると(CSS2.1: 5.12.2, Selector3: 7.2)、

・約物は次の文字と一緒に数える。例えば:
<p>「警部、事件です!」</p>

p::first-letter => 「警

・結合文字は 1 文字として。例えば、オランダ語表記の「ij」は 1 文字とカウント。漢字にもサロゲートペアとか正規化とか云々。

・:first-letter の定義が「最初の行の最初の文字」なので、次の例では ::first-letter が存在しない。
<p>
  <br>
  ところで、......
</p>

何かのエディタがこういうのを頻繁に吐いてた気がする。ご愁傷様。



まあとにかく、記号類を 1 文字として数えるのかどうか。

一覧へ戻る