#062
posted on 2021.12.12

テキストを1文字ずつ任意のHTMLのタグで囲うコーディングの一括処理。

見出しなどにCSSでアニメーションを付けるためにテキストを1文字ずつ任意のHTMLタグで囲いたかったので、一括でまとめて1文字ずつをコーディングする方法のメモ。

 

WordPressなどCMSで出力される不特定のテキストには手動では対処できないので、PHPかJavaScriptで自動的に一括処理する。

※ 対象テキストにHTMLタグが含まれている場合、HTMLタグの文字列は処理せずに中のテキストノードの文字列だけを1文字ずつコーディングする。

//元のHTML
<h1><span class="foo">任意</span>のテキスト</h1>

//テキストだけを1文字ずつ任意のHTMLタグで囲う一括処理
<h1><span class="foo"><span class="span-chara">任</span><span class="span-chara">意</span></span><span class="span-chara">の</span><span class="span-chara">テ</span><span class="span-chara">キ</span><span class="span-chara">ス</span><span class="span-chara">ト</span></h1>

 

  1. 対象のテキストから1文字ずつ正規表現で拾い上げる。
  2. 取得した1文字をコーディングをした状態に置換する。
  3. コーディング置換したあとの文字を繋げ直す。
  4. 置換して繋げ直したテキストと元の対象のテキストを入れ替える。

 

 

PHPで処理する場合

PHPで処理する場合、自動コーディングする関数を作成して、テキストを出力したい場所でその関数を実行する。

 

1. 自動処理をする関数の作成

第1引数に「処理したいテキスト」、第2引数に「付加したいクラス名」、第3引数に「文字を囲うHTMLタグの種類」を受け取れるように関数を作成する。

(下記では、第2引数のデフォルト値は「クラス名無し」、第3引数のデフォルト値は「span」。)

 

「preg_split()」関数でテキストを1文字ずつに分割・配列化、「foreach」文で1文字ずつコーディング処理する。

  1. 1文字をHTMLダグで囲った状態に置換する関数(ag2chara_replace)を定義。
  2. テキストを渡して置換を実行する関数(ag2chara_wrap)を定義。
  3. 関数内で使う変数の初期値を定義。
  4. 「preg_split()」関数を使って指定されたテキストを1文字ずつ分割して配列化。
  5. 配列化した文字を「foreach」文で1つずつ取り出してコーディング処理。
  6. 処理した文字を変数(ここでは「$new_str」)に代入し、繋ぎ合わせる。
  7. 対象テキスト内にHTMLタグがある場合、タグの文字列はコーディング処理しないので、文字列が「<」なら、次に「>」が出るまで置換処理せずにそのまま変数に代入。(swich文で文字列を確認して処理を分ける。)
  8. すべての文字の処理が終わったら関数の返り値として変数(「$new_str」)を返す。

※ 正規表現のパターン装飾子に「u」を指定して、日本語やサロゲートペア(絵文字や異体文字)の文字コードにも対応できるようにする。(英数字のみのテキストにしか使用しないなら不要。)

※ 分割の境界の指定が「//」だと配列の最初と最後に空文字が格納されてしまうので、「PREG_SPLIT_NO_EMPTY」を指定して空文字は返さないようにする。

※ 対象文字列に半角スペースが含まれる場合、Wordpressのfunctions.phpに関数を記述して使ったときに半角スペースが勝手に削除されてしまったので、半角スペースが含まれている場合は「&nbsp」(HTMLの文字実体参照)に変換処理して返す。(functions.phpから呼び出した関数の返り値は自動的にサニタイズされるからなのか、原因は不明。)

※ PHPの正規表現の公式ドキュメント

//1文字をHTMLタグで置換する関数を定義
function ag2chara_replace($v,$c,$h){
  return '<'.$h.$c.'>'.$v.'</'.$h.'>';
}
//テキストを渡して置換を実行する関数
function ag2chara_wrap($str,$cls='',$html='span'){
  //変数を定義
  if($cls) $cls = ' class="'.$cls.'"';
  $new_str = '';
  $tag_flg = false;

  //テキストを1文字ずつ分割して配列化
  $chara_arr = preg_split("//u", $str, 0, PREG_SPLIT_NO_EMPTY);

  //配列から1文字ずつ処理
  foreach($chara_arr as $value){
  	switch($value){
  		case '<':
  			$new_str .= $value;
  			$tag_flg = true;
  			break;
  		case '>':
  			$new_str .= $value;
  			$tag_flg = false;
  			break;
  		case ' ':
  			if($tag_flg){
  				$new_str .= $value;
  			}else{
  				$value = '&nbsp;';
  				$new_str .= ag2chara_replace($value,$cls,$html);
  			}
  			break;
  		default:
  			if($tag_flg){
  				$new_str .= $value;
  			}else{
  				$new_str .= ag2chara_replace($value,$cls,$html);
  			}
  	}
  }
  return $new_str;
}

preg_split(‘正規表現パターン’, ‘文字列’, ‘リミット数’, ‘フラグ’) : 正規表現でマッチした箇所で指定した文字列を分割し、分割されたそれぞれの文字列を配列にして返す。失敗した場合はfalseを返す。リミット数(「-1」か「0」の指定はリミット数の制限無し)を指定した場合は、分割した文字列をn個まで返し、残りはすべて最後の文字列に含めて返す。フラグ「PREG_SPLIT_NO_EMPTY」で、分割された文字列が空文字の場合は配列に返さない。

/正規表現パターン/パターン装飾子 : PHPの正規表現。正規表現パターンを囲うデリミタには、英数字、バックスラッシュ、空白文字以外の任意の文字が使用でき、一般的には「/」(スラッシュ)が使用される。パターン装飾子(JavaScriptの正規表現ではフラグと呼ぶ)で、パターンのマッチに関するオプションを指定できる。パターン装飾子「u」で、パターンと対象文字列をUTF-8として処理する。

 

2. 自動処理の関数の実行

引数に、処理したい「対象のテキスト」と「付加したいクラス名」を指定して、作成した関数を任意の場所で実行する。

$mytxt = '<span class="foo">任意</span>のテキスト';//処理するテキスト
$myclass = 'span-chara';//付加するクラス名

echo '<h1>';
echo ag2chara_wrap($mytxt, $myclass);
echo '</h1>';

 

 

JavaScript(ネイティブJavaScript)で処理する場合

JavaScriptで処理する場合、自動コーディングする関数を作成しておいて、DOM構築後のタイミングで対象となる要素をDOMから取得して処理を実行する。

 

1. HTMLのマークアップ

処理対象となるテキストを内包するHTML要素に予め任意のクラス名を付与しておく。(ここでは「h1」に「target-txt」を付与。)

<h1 class="target-txt"><span class="foo">任意</span>のテキスト</h1>

 

2. 自動処理をする関数の作成

「文字のコーディング処理を実行する関数」と「指定した要素の子ノードのノードタイプをチェックする関数」を作成。

 

[ 文字のコーディング処理を実行する関数 ]

テキストノードが保持している文字列をコーディング処理する関数。

  1. テキストノードが保持している文字列を取得。
  2. 正規表現で文字コードを指定して1文字ずつを拾い上げる。
  3. 1文字を任意のHTMLタグで囲ったコーディングの置換処理。
  4. 置換後の文字列を任意のHTML要素に代入してHTML文字列(htmlString)に変換してノード化。(オブジェクトとして処理できるようにする。)
  5. ノード化したHTML文字列のノードリストを取得して配列化。
  6. 配列にしたHTML文字列の1つめのノードで元々のテキストノードを置き換える。
  7. HTML文字列のノードが2つ以上あれば、次の位置に順番に挿入。

※ 日本語とサロゲートペアに対応するために、コードユニットのエスケープシーケンスを使って正規表現パターンを指定する。(サロゲートペアに関しては前の記事を参照。)

※ 正規表現のMozillaの公式ドキュメント

 

[ 指定した要素の子ノードのノードタイプをチェックする関数 ]

置換処理の対象となる指定のHTML要素が、テキストだけでなくHTMLタグを含めている場合の対処。

  1. 指定した要素が内包する子ノードのノードリストを取得。
  2. すべての子ノードのノードタイプを確認。
  3. テキストノードの場合は上記のコーディング処理の関数を実行。
  4. エレメントノード(HTMLタグ)の場合は、更にその子ノードを取得して同じチェックを実行。

※ 対象テキストにHTMLタグが含まれている場合には、HTMLタグ自体はそのままにして中の文字のみコーディング処理する。

const ag2chara = {
  wrap: function(t,c,h,n,i){
    if(c) c = ' class="'+c+'"';

    //テキストノードから文字列を取得して置換処理
    let chara = t.textContent;
    chara = chara.replace(/([\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF])/g, '<'+h+c+'>$1</'+h+'>');

    //置換後の文字列を適当なHTMLタグに挿入してノード化(オブジェクト化)
    let tmpEle = document.createElement('div');
    tmpEle.innerHTML = chara;
    let tmpEleChildren = Array.from(tmpEle.childNodes);
    let tmpEleChildrenNum = tmpEleChildren.length;

    //ノード化した文字列で元のテキストを書き換え
    n.item(i).parentNode.replaceChild(tmpEleChildren[0], n.item(i));

    //置換後の文字列のノードが複数なら続けて挿入
    if(tmpEleChildrenNum > 1){
      for(let j = 1, k = i; j < tmpEleChildrenNum; j++, k++){
        n.item(k).parentNode.insertBefore(tmpEleChildren[j], n.item(k).nextElementSibling);
      }
    }
    return tmpEleChildrenNum;
  },
  nodeCheck: function(t,c='',h='span'){
    //指定した要素の子ノードを取得
    let children = t.childNodes,
        childrenNum = children.length;

    //取得した子ノードのノードタイプをチェックして処理
    for(let i = 0; i < childrenNum; i++){
      thisItem = children.item(i);
      if(thisItem.nodeType === 3){
        //処理した文字数分だけ子ノードが増えるので調整
        i = i + ag2chara.wrap(thisItem,c,h,children,i) - 1;
        //処理後のリアルタイムの子ノードの数を再取得
        childrenNum = children.length;
      }else if(thisItem.nodeType === 1){
        ag2chara.nodeCheck(thisItem,c,h);
      }
    }
  }
};

ノード.textContent : Nodeのプロパティー。ノードおよびその子孫のテキストの内容を表す。テキストノードの場合、そのノードの内側のテキスト(Node.nodeValue)を返す。(innerHTMLとは違い、値がHTMLとして解析されない。)

対象文字列.replace(‘正規表現パターン’, ‘置換後の文字列’) : 指定した正規表現パターンが対象文字列内でマッチしたら、置換後の文字列に置き換える。「正規表現パターン」ではなく具体的な「文字列」を指定した場合、最初に表れたその文字列を「置換後の文字列」に置き換える。置き換え処理後の新しい文字列を返す。

/正規表現パターン/フラグ : JavaScriptでリテラルに正規表現を宣言する場合には、「/」をデリミタに使う。

() : キャプチャグループ。正規表現で丸括弧内の指定パターンにマッチした文字列は記憶され、第2引数内で後方参照できる。(結果の配列要素のインデックス「[1], …, [n]」や、予め定義されているRegExpオブジェクトのプロパティー「$1, …, $9」でアクセスできる。)

[] : 角括弧に含まれるいずれか1文字にマッチ。角括弧内で使用できるメタ文字(「-」「^」「\」「]」)はエスケープが必要。

: 角括弧内でのみ使用できる範囲を指定するメタ文字。

| : 左右辺の文字列のいずれかにマッチ。

[^] : 角括弧内で^の後ろに指定している文字以外にマッチ。

g : (global)フラグ。2番目、3番目…以降にマッチする部分も検索して配列として返す。

document.createElement(‘タグ名’) : 指定したタグ名のHTML要素を生成する。

要素.innerHTML : 指定した要素内のHTMLまたはXMLのマークアップを取得する。値を代入して設定した場合は、要素のすべての子孫を削除して、htmlStringの文字列で与えられたHTMLを解析して構築されたノードに置き換える。

Array.from(‘オブジェクト’) : 指定した配列風オブジェクトまたは反復可能オブジェクトからArrayインスタンスを生成。

配列.length : Array型のインスタンスであるオブジェクトのプロパティー。配列の要素の数を設定または取得する。

ノード.childNodes : 読み取り専用プロパティー。指定したノードの子ノードのノードリストを返す。(処理によって内容が変更されていれば、リアルタイムでその時点でのノードリストを返す。)

ノードリスト.item(‘インデックス番号’) : 指定したノードリストの中から指定したインデックス番号の位置にあるノードを取得する。

ノード.parentNode : 指定したノードのDOMツリー内の親ノードを返す。

ノード.replaceChild(‘置換後のノード’, ‘子ノード’) : 指定したノードが持つ指定した子ノードを置換後のノードで置き換える。

親ノード.insertBefore(‘ノード’,’参照ノード’) : 指定したノードを参照ノード(指定した親ノードの子である必要がある)の前に、指定した親ノードの子として挿入する。(参照ノードがnullの場合は、親ノードの子ノードの末尾に挿入される。)

要素.nextElementSibling : 読み取り専用プロパティー。指定した要素の親要素から見た、指定した要素の次の子要素を返す。次の子要素が無ければnullを返す。

ノード.nodeType : 指定したノードの種類を表す整数のコードを返す。「1」はエレメントノード、「3」はテキストノード。

 

3. 自動処理の関数の実行

すべてのDOM構築が完了したタイミングで上記の関数を実行する。(対象要素の読み込みが完了している必要があるので。)

//DOM構築が完了したら関数を実行
document.addEventListener('DOMContentLoaded', function(){
 //処理の対象とする要素のクラス名を指定して取得
 const targetEle = document.getElementsByClassName('target-txt'),
  targetEleNum = targetEle.length;
 //コーディング処理時に付与するクラス名
 const charaClass = 'span-chara';

 //指定したクラス名を持つすべての要素で処理
 for(let i = 0; i < targetEleNum; i++){
  ag2chara.nodeCheck(targetEle[i], charaClass);
 }
});

document.getElementsByClassName(‘クラス名’) : 指定されたクラス名を持つすべての要素の配列風オブジェクト(HTMLCollection)を返す。(HTML要素の集合を表すオブジェクト。)

HTMLCollection.length : HTMLCollectionに含まれるitemの個数を返す。

対象要素.addEventListener(‘イベントのタイプ’, ‘関数’, ‘イベント伝播順’) : 対象要素に指定のイベントでコールする関数を指定して登録。第3引数(初期値 : false)でイベントの伝播する方向を指定できる。falseでDOM階層の下位から上位に伝播。

 

 

jQueryで処理する場合

jQueryで上記と同じ処理をする場合。

 

const ag2chara = {
  wrap: function(t,c,h){
    if(c) c = ' class="'+c+'"';
    $(t).replaceWith($(t).text().replace(/([\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF])/g, '>'+h+c+'>$1</'+h+'<'));
  },
  nodeCheck: function(t,c='',h='span'){
    $(t).contents().each(function(i){
      if(this.nodeType === 3){
        ag2chara.wrap(this,c,h);
      }else if(this.nodeType === 1){
        ag2chara.nodeCheck(this,c,h);
      }
    });
  }
};

$(document).ready(function(){
  const $target = $('.target-txt');
  const charaClass = 'span-chara';
  ag2chara.nodeCheck($target,charaClass);
});

 

 

この記事をシェア
この記事のURL

https://memo.ag2works.tokyo/post-3641/

コピー
この記事のタイトル

テキストを1文字ずつ任意のHTMLのタグで囲うコーディングの一括処理。 | memo メモ [AG2WORKS]

コピー
この記事のリンクタグ

<a href="https://memo.ag2works.tokyo/post-3641/" target="_blank" rel="noopener">テキストを1文字ずつ任意のHTMLのタグで囲うコーディングの一括処理。 | memo メモ [AG2WORKS]</a>

コピー
※ フィールドをクリックでコピーするテキストの編集ができます。

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)