なんなんだこのタイトル。
さて、以前「見出しとかの日本語をいい感じの塊で自動改行するCSS」という記事を投稿しました。
しかし、塊を明記するために結構めんどくさいHTMLを書く必要があり、微妙に使いにくいものでした。
See the Pen 見出しとかの日本語をいい感じの塊で自動改行するCSSのサンプル2 by 松田美文 (@mifumi323) on CodePen.
なんなんだこの例文。
とにかく、ちゃんとやろうとするとHTMLがこのように長く入り組んだものになってしまい、とても手書きでは対応できません(できても書きたくありません)。
もっと簡単な記述でこれ相当のHTMLを出せるプログラムを作りましょう。
目標
塊を記述すること自体はどうしても必要なので、簡単な記述からHTMLを生成できるようにしましょう。
ウルトラ|||マン||コスモスが|個性的な||お菓子||||作りを|||する|パチンコ||発見
上記のように、パイプ「|」で塊を区切り、以下のようなHTMLを得ることを目標とします。
<span class="line-chunk"><span class="line-chunk"><span class="line-chunk">ウルトラ</span><span class="line-chunk">マン</span></span><span class="line-chunk">コスモスが</span></span><span class="line-chunk"><span class="line-chunk">個性的な</span><span class="line-chunk"><span class="line-chunk"><span class="line-chunk">お菓子</span><span class="line-chunk">作りを</span></span><span class="line-chunk">する</span></span></span><span class="line-chunk"><span class="line-chunk">パチンコ</span><span class="line-chunk">発見</span></span>
連続した|の数を多くするほど細かい塊の区切りとすることにします。
| レベル1
|| レベル2
||| レベル3
できたPHP
なんでPHPかって?いつも使ってるからだよ。
<?php
class InsertLineChunkTags
{
/**
* 改行可能なポイントを設定します。
* 半角パイプ「|」の位置が改行可能な位置となり、連続する「|」の数が多いほど細かい分割単位となります。
* 「1234」→改行ポイント設定なし。
* 「12|34」→「1234」が表示しきれない場合は「12」と「34」に分割。
* 「1||2|3||4」→上記に加え、「12」が表示しきれない場合は「1」と「2」、「34」についても同様。
*/
public static function convert(string $html): string
{
return self::impl($html, 1);
}
private static function impl(string $html, int $level): string
{
$separator = str_repeat('|', $level);
if (!str_contains($html, $separator)) {
return $html;
}
$chunks = preg_split('/(?<!\|)\|{'.$level.'}(?!\|)/', $html);
$next_level = $level + 1;
$result = '';
foreach ($chunks as $chunk) {
$chunk_head = '';
$chunk_body = $chunk;
$chunk_tail = '';
if (preg_match('/\S/u', $chunk)) {
if (preg_match('/^(\s*)(.*\S)(\s*)$/u', $chunk, $matches)) {
$chunk_head = $matches[1];
$chunk_body = $matches[2];
$chunk_tail = $matches[3];
}
}
$result .= $chunk_head;
$result .= '<span class="line-chunk">';
$result .= self::impl($chunk_body, $next_level);
$result .= '</span>';
$result .= $chunk_tail;
}
return $result;
}
}
解説いるかな?技術記事だし一応しとくか。
最初の正規表現は、前後に余分な|がついてない$level個連続の|で文字列を分割しています。
(?<!\|)と(?!\|)は否定の言明で、文字数を消費せず場所にマッチします。
2つ目の正規表現は、スペースの扱いのケアです。
今回の例文にはスペース含まれないんですけどね。
変数名$htmlって書いてますが、ただspanタグを入れるだけで、ちゃんとHTMLを考慮したものにはなっていません。
XSS対策などはしてないので、あらかじめHTMLエスケープされている想定です。
実用上実害はないと思いますが、「ある||でる」みたいに|のレベルが飛んでいると余分なspanが付きます。
JavaScriptも
ここから先、AI生成物が含まれます。
さて、今回は自分で使っていた都合でPHPを使いましたが、他の言語でやるにしてもだいたいは同じです。
ブラウザ上でリアルタイムに処理できるように、JavaScriptでやってみましょうか。
既にPHPでの正解のコードはあるので、AIに頼っちゃいましょう。
class InsertLineChunkTags {
static convert(html) {
return this.impl(html, 1);
}
static impl(html, level) {
const separator = '|'.repeat(level);
if (!html.includes(separator)) {
return html;
}
const chunks = this.splitBySeparator(html, level);
const nextLevel = level + 1;
let result = '';
for (const chunk of chunks) {
let chunkHead = '';
let chunkBody = chunk;
let chunkTail = '';
if (/\S/u.test(chunk)) {
const matches = chunk.match(/^(\s*)(.*\S)(\s*)$/u);
if (matches) {
chunkHead = matches[1];
chunkBody = matches[2];
chunkTail = matches[3];
}
}
result += chunkHead;
result += '<span class="line-chunk">';
result += this.impl(chunkBody, nextLevel);
result += '</span>';
result += chunkTail;
}
return result;
}
static splitBySeparator(html, level) {
const separator = '|'.repeat(level);
const parts = [];
let start = 0;
for (let i = 0; i <= html.length - level; ) {
if (html.slice(i, i + level) === separator) {
const before = i === 0 ? null : html[i - 1];
const after = i + level >= html.length ? null : html[i + level];
if (before !== '|' && after !== '|') {
parts.push(html.slice(start, i));
i += level;
start = i;
continue;
}
}
i += 1;
}
parts.push(html.slice(start));
return parts;
}
}
GitHub Copilotの無料プランで「このInsertLineChunkTagsのJavaScript版作って。」とだけ指示して作らせました。
最初の正規表現の代わりにsplitBySeparator関数が生えてるとか、impl関数が普通に公開されてるとか、実装に多少違いはありますが、いい感じですね。
Gitリポジトリ
動作確認環境
PHP 8.3.13
Google Chrome 149.0.7827.201
おわりに
一度コードを書いたらAIで他言語版作るんもありですね。
もちろん確認は必要でしょうが。