LoginSignup
23
12

More than 3 years have passed since last update.

【CSS】文字列を真に折り返したいのなら

Last updated at Posted at 2020-04-14

※これは元々、overflow-wrap: break-word; や word-break: break-all; が万能の改行処理だったなら、こんなに苦労していないに対するコメントとして2020年1月に書かれたものだが、line-break: anywhere;white-space: break-spaces;の知名度と支持率を上げるため、記事化して人目に触れやすくした。

問題提起

word-break: break-all;はすごい。長い英単語も、何でもかんでもほぼ完ぺきにズタズタに分解して折り返してくれる、何があっても強制改行してくれる、そんなCSSだ。ほとんどの人が、最初はそんな印象を持つだろう。もうword-break: break-all;信者になろう。とりあえず折り返しが防ぎたい時にはこれ!なんて楽だ!
この夢は永遠に続くと思われた。そう、あの文字列に遭遇するまでは。

元記事でのサンプル文字列を拝借:

サンプル
現実はかくも無情!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

おや?情!!!!!!!!!!!!の部分が2行目にまわり、あとは折り返されず突き抜けている。
ここであなたはすかさず、折り返しの定番word-break: break-all;をかける。
はい、効かない。

そう、!!!!!!!!!!!word-break: break-all;で折り返せないのである。

信者の夢は打ち砕かれた。長い英単語だって律義に折り返されてるのに、!!!!!!!!!!!だけが残る。まるで小麦粉のダマだ。裏切られたなんてヌルいもんじゃない、筆舌に尽くしがたい心情に突き落とされる。元記事主様はこれを、「何があっても強制改行と言ったな?あれは嘘だ」と表現している。

これがtableのセルをレイアウト崩壊させたり、そもそもtable以外の本文でも厄介な存在となる。スマホサイトで横スクロールを生み出すのもこいつだ。ユーザー入力コメント欄で「面白かったです!!!!!!!!!!!!!!!!」とか書き込まれてみろ。はい死亡ー

ここで、「CSS 折り返し」等でさんざん調べものをするだろう。だが、ググっても出てくるのは

  • word-break: break-all;overflow-wrap: break-word;といった王道たちを、それらの違いを分かりにくくそれっぽく説明した(つもりになってる)もの
  • white-space: nowrap;がかかってませんか」とかいう筋違いアドバイス
  • 「Unicodeのゼロ幅スペース&#8203;<wbr>を使え」とかいう愚行(←絶対にしちゃ駄目だよ!さもなくばこんなトラブルの元になる。そもそも、見栄えのためにデータに手を加えるなんてあり得ない)

こんなのばかりだ。は????折り返し問題はコレで解決?何をおっしゃい。そんなクソハウツーサイトや有耶無耶ブログばかりだ。英語で検索しても、なかなか真の解決方法を見つけるのは難しい。

万策尽きた…………… この記事は、そんな壁にぶち当たったあなたが対象だ。

さっさと結論

これらはまさしく、過去の私もぶち当たった問題だ。
私はかつてこの問題と徹底的に闘い、Chrome開発者と直接情報交換をしながら、その全貌を
https://ss1.xrea.com/ango.g3.xrea.com/jkkn/chrome_bug5.html
https://ss1.xrea.com/ango.g3.xrea.com/jkkn/chrome_bug14.html
に纏めてある。

結論から書くと、今の所究極の、(しかし、悲しいかな、正確には「将来究極になる予定の」)解決方法は、

word-break: break-all;        /* 元記事と同じ */
overflow-wrap: break-word;    /* 〃 */
word-wrap: break-word;        /* 〃 */
line-break: anywhere;         /* 見慣れない。 */
white-space: pre-wrap;        /* 任意。詳細は後述 */
white-space: break-spaces;    /* 見慣れない。但し、これも任意 */

である。
私が得たこの究極の結論には、見覚えのないCSSが含まれていると思う。
下の方にある、line-break: anywhere;white-space: break-spaces;の2つだ。

また、この中には、元記事中で推奨されていたword-break: break-word;が含まれていない。なぜか? かつては私もword-break: break-word;を発見し、歓喜したものだ。しかし、後に解決方法から除外した。その理由は後述。

この究極の結論、特にline-break: anywhere;について、まだ日本ではまだ殆ど知られていないと思われる為、情報共有する。

問題の整理

まず、問題を整理しよう。実は、元記事主様が実現したいことは、4つに分割できる。以下、その4つの問題を見ていこう。

  1. 長い英単語PneumonoultramicroscopicsilicovolcanoconiosisやURL等)を折り返したい。
  2. 禁則処理はなるべく守りたい。例えば、閉じ括弧や句読点類(.,!?、。等)が行頭に来ないようにしたい(行頭禁則)。開き括弧は行末に来ないようにしたい(行末禁則)。
  3. 連続記号=連続句読点類=禁則処理対象記号の連続」」」」」」」!!!!!!!!等)は、折り返したい。
  4. テーブルセル内でも通用する方法であってほしい。

しかし、これら4つを一挙に解決する方法は、なかなか無いものだ。

一長一短すぎる各方法(元記事の要約)

  • overflow-wrap(word-wrap): break-word;
    1. 長い英単語:  ○
    2. 禁則処理:   ○
    3. !!!!!!!!!: ○
    4. テーブルセル: ×
  • word-break: break-all;
    1. 長い英単語:  ○
    2. 禁則処理:   ○
    3. !!!!!!!!!: ×
    4. テーブルセル: ○

この○×一覧を見て、「あーー!overflow-wrap(word-wrap)さん、いい所までいったのに、なぜ、なぜお前は、肝心のテーブルセルで無力なのか…セルさえ…セルさえ打倒できれば…」と思うのももっともだ。
前者は折り返し問題については満点で、本当に惜しい所まで行っている。一方、word-break: break-all;で×の付いた問題3は、厄介な「何があっても強制改行と言ったな?あれは嘘だ」問題をはらんでいる。その為、後者でなく前者をベースに解決法を練りたくなるのも無理はない。

そこで、前者で未解決の問題4を解決する為に、

(1) (元記事曰く)天真爛漫純粋無垢かつ無情なword-break: break-word;
(2) divタグ

の2方法を試した。

(1)のword-break: break-word;は、非常に惜しい存在だ。問題1~4が全て○のパーフェクトクリアなのだ。最強では!?!?!?そう思った。しかし… IE・Edge非対応の為、暫定解決方法としては除外せざるを得なかった。

(2)は、divタグによってdisplay: table-cell;の呪縛を逃れ、セル内に無理やりdisplay: block;な空間を作る事で、半ば無理やり問題4を解決している。これはうまい事機能するが、方法としては気持ち悪い為、「暫定」の域を出ない…

と、ここまでが元記事の内容だが、

実は天真爛漫な(1)には更なる欠点がある。
実はこのword-break: break-word;、非推奨なのだ。
https://jigsaw.w3.org/css-validator/ 等で確認すると、The value break-word is deprecatedと警告が出る。実はこのvalue、昔のものらしく、今後廃止される運命にあるようだ。IEやEdgeがこのvalueに対応してないのは別におかしな話ではなく、寧ろChromeが未だにこれをサポートし続けているのがおかしい、という事らしい。先程、「このvalueを発見した時、私も歓喜したものだ。しかし、後に解決方法から除外した」と言ったが、こういう事情なのだ。

つまり、(1)も(2)も、究極の解決法としては採用できず、道を断たれる。
※テーブルの場合は(3)としてtable-layout: fixed;も使えるが、テーブル以外では通用しない。

じゃあどうすれば?

新たな天使、line-break: anywhere;様のお出ましだ

先程の経緯により、overflow-wrap(word-wrap): break-word;の欠点を埋める方向での解決策は行き詰まった。

ならば原点に立ち返って、word-break: break-all;の欠点を埋める方向、つまり、「何があっても強制改行と言ったな?あれは嘘だ」問題に立ち向かえばいい。
その中核にあるのが、問題3( !!!!!!!!!問題)だ。

これは一見、無謀に思えたが、私はこの問題をChromeの開発者へ持ち込んでみた。
それがこちら(このコメントの読了前にリンク先を読むと混乱する為、今は読まなくていい):
https://ss1.xrea.com/ango.g3.xrea.com/jkkn/chrome_bug5.html
https://bugs.chromium.org/p/chromium/issues/detail?id=852313

すると、なんてこった。Chrome開発者側も(、実はCSSの仕様策定側も)この問題を承知であり、Chromeとしてはこの問題を
https://bugs.chromium.org/p/chromium/issues/detail?id=720205
として長年扱ってきたという。(その為、私のIssue 852313は、このIssue 720205にマージされた。)

そして、Chrome開発者側から問題3の解決方法として教えて頂いたのが、
line-break: anywhere;だ。ここでやっと、主役の登場である。

「何があっても強制改行と言ったな?あれは嘘だ」問題の黒幕

ここで、件の「4つの問題」を振り返ってみよう。

よく考えると実は、問題2と問題3には、相反する要望が含まれている事が分かるはずだ。
2は禁則処理を守りたがり、3は禁則処理を破ってまで強制的に折り返したがっている。
つまり、2と3は互いに矛盾し合う要望をはらんでおり、両立が難しい。

この矛盾、もとい"仕様や要望の競合"こそが、問題3、つまり「何があっても強制改行と言ったな?あれは嘘だ」問題の解決を難しくしている根本原因であり、黒幕なのだ。
CSSやブラウザを作っているWHATWGやGoogleの開発者も、実は長年、この矛盾問題や仕様策定に頭をひねってきた。

何故Chromeでは、word-break: break-all;!!!!!!!!!!に対して効かないのか。
つまり、何故Chromeでは、問題2と問題3が両立できないのか。
それは、以下のようなメカニズムで起こる。

禁則処理は本来、例えば行頭に,!等が来ないようにする為の処理だ。
そして、この処理を実装する為にはまず、この処理の対象になる記号類をブラウザが選定せねばならない。それが、件の,!等だ。

次にChromeは、この禁則処理を実装する際、
各対象記号から見て、それぞれ(行頭禁則なら)直前/(行末禁則なら)直後にある文字と強く強く結合させるように実装した。

例えば、「あいうえおかきくけこ。」という文字列があり、セルやボックスの幅がそれより1文字分狭い場合。
Chromeは、行頭禁則処理対象である「」の直前の文字、「」を見る。そして、Chromeはこの2文字、「こ。」を強く強く結合させる。そう、word-break: break-all;の優先順位を上回るほどの、強い結合だ。こうなると最早、Chrome上では「こ。」の2文字があたかも分割できない1文字であるかのような振る舞いを見せる。
結果として、1行目には「あいうえおかきくけ」だけが入り、「」が入る隙間を右端に残したまま、「こ。」が2行目に回る、という具合だ。こうして、2行目の行頭が「」になる事は防がれ、「」が行頭となる。

Chromeはこうして、禁則処理を実現している。これが、問題2の裏側だ。

一見これは、禁則処理の実装方法としては妥当のように思えるし、実際これはCSSの定める仕様書にも則った実装だ。その為、この実装方法はバグではなくChromeの仕様であり、将来変更される事はないだろう。
だがこのChromeの実装方法には、結合が連鎖し、結合部分全体が肥大化する可能性を考慮してない点に問題がある。

次の文はどうか。「あい。。。。。。。。。
上記のような実装をしているChromeは、この文字列に対し、以下のような処理をするだろう:

  1. 最後の「」は行頭禁則処理対象の為、その直前にある「」と強く結合させる。これで、最後の2文字「。。」が強く結合される。
  2. しかし、その直前文字自体もまた、禁則処理対象であるから、更にその1文字前の「」とも強く結合させる。この時点で、最後の3文字「。。。」が強く結合された。
  3. 同じようにして、「」までもが、結合対象に組み込まれる。
  4. 結果としてChromeは、「い。。。。。。。。。」をまるごと強く結合させ、あたかも1文字であるかのように扱う。
  5. セルやボックスの幅が、文字列全体よりも1文字分狭かった場合、"2文字目"である「い。。。。。。。。。」は入りきらない。その為、1行目には「」だけが残され、残りの「い。。。。。。。。。」は全て、2行目に回されてしまう。
  6. もしも、全体の幅が「い。。。。。。。。。」自体よりも狭かったとしても、Chromeは「い。。。。。。。。。」を1文字かのように扱うので、レイアウト崩れを起こしてでも無理やり「い。。。。。。。。。」を死守する。

例では「」を取り上げたが、「!」に対しても全く同じだ。上記例では、禁則対象以外の文字で2行目に回された文字は、「」のみ。これは、元記事サンプルの「現実はかくも無情!!!!!!!!!!!」の「」までが1行目に残り、「」以降が2行目へ折り返される挙動と一致する。

こうして、問題3、つまり!!!!!!!!が折り返せない問題が発生してしまうのだ。

問題2を実装する為に、Chromeは結合システムを作った。しかし、その結合ベースな実装方法が、問題3と競合矛盾し合う。これが、さっき「問題2と3が相反する」と言った真意だ。

この原因は何か? 勿論、このChromeの実装方法、つまり、強すぎる禁則結合の多重連鎖に原因がある。

しかし、さっきも言った通り、この実装はCSSの仕様に反してはいない。どういう事か。
CSSの仕様では、禁則処理対象の記号が連続した場合、つまり「。。。。。。。。。」や「!!!!!!!!!!!」が現れた場合の挙動について、明記していない。つまり、CSSの仕様自体が曖昧なのだ。そのせいで、その曖昧な部分の実装方法は、実際のブラウザ開発者に委ねられたのだ。

Chromeと違う実装方法をとっているのが、Firefoxだ。Firefoxはこの件に関してはとても賢い実装をしていて、行頭に対象記号が来るのを防ぐと同時に、うまいこと良しなに連続記号をも折り返してくれる(恐らく、対象記号の結合相手を対象記号以外に限定してくれている為、結合連鎖が起こらない。賢い)。
その為、Firefoxではword-break: break-all;単体で、問題2と問題3の解決を両立できる。

つまり、問題2と問題3が矛盾し合う問題が起こるのはChromeだけ、という事になるが…

改めてこの問題の原因を問おう。原因は何か?
直接の原因は勿論Chromeのくだんの実装方法なのだが、
間接的な原因として、CSSの仕様の曖昧さがある、という事なのだ。

そもそも、line-breakプロパティ is 何

禁則処理に関して仕様に曖昧さが残っていると発覚したCSSには、新たにline-breakというプロパティが追加された。
禁則処理の挙動を明確に決める為、line-breakは誕生したのだ。

これは元々IE等に独自実装として昔からあったが、Chromeがline-breakに対応したのはChrome 58、Firefoxに至ってはFirefox 69である。つまり、標準仕様としてはかなり後発組のプロパティである事が分かる。

つまり、line-breakは禁則処理の挙動を指定する専用のプロパティだ。
このline-breakこそが、「。。。。。。。。。」や「!!!!!!!!!!!」をぶっ壊すかどうかを指定できる権限を持つ、唯一にして待望のプロパティなのだ。

となると、CSSではword-breakoverflow-wrap/word-wrapとは別に、
禁則処理を扱うプロパティとしてline-breakも用意している、という事になる。

仕様がどんどん複雑化してやがる。しかし、おかげで問題3の解決の糸口が見えたというもの。

このline-breakがとりうる値は、autoloosenormalstrictanywhere
詳しい挙動の説明は端折る(というか私も把握しきれていない)が、大体こうだ:
strictは一番禁則処理が強く、normalは普通、looseが緩め、anywhereは禁則処理の完全解除だ。

ここで私が知りたいのは、「!!!!!!!!!!!」を壊せるかどうか。
anywhereこそが、私たちの求める最強のものだろう。禁則処理の完全解除なのだから、「!!!!!!!!!!!」は確実に折り返せる。
少なくともFirefoxは、この全ての値を実装してくれており、Firefoxのline-break: anywhere;は理想通りに動いてくれる。

しかし、Chromeは長年、autoloosenormalstrictしか実装してくれていない。肝心のanywhere様が、Chromeには長年無いのだ。
そこで、次に緩いlooseに希望を託すわけだが、なんと…禁則処理緩めのはずのlooseでさえ、「!!!!!!!!!!!」を壊す事はできなかったのだ。「!!!!!!!!!!!」を壊せるのはanywhereだけらしい。
これが、先程の
https://bugs.chromium.org/p/chromium/issues/detail?id=720205
の内容だ。このIssueのタイトルは「Implement 'line-break: anywhere'」、つまり「(現状のChromeでは実装できていない)line-break: anywhereを実装する」事を目標としたIssueだ。

私がこのIssueに働きかけた結果、長年保留状態だったこのIssueは動き出し、現在はなんとFixedになっている。という事は、2020年現在のChromeでは、バッチリanywhereできるはず。

しかし…あれ?未だ機能しない。これはどういう事か。

実は2020年現在、Chromeのline-break: anywhere;は、chrome://flagsからExperimental Web Platform featuresフラグをEnabledにしないと使えるようにならない。つまり、実装されたはされたが、未だChromeとしてはline-break: anywhere;を実験段階としているのだ。

一応フラグを有効にして試すと、なんと理想通りに動く。やったじゃん。これで、!!!!!!!!!は折り返される。しかもこいつは最強だ。セル内でも有効なのだ。問題4まで一挙に解決だ。
word-break: break-all;line-break: anywhere;の合わせ技で、なんと問題1~4まで全解決だ(厳密には問題2を犠牲にしているが、折り返し問題完全解決という意味で有意義だ。そもそも、前述の仕様矛盾・実装競合の話を考えれば、問題3解決の為には問題2を犠牲にせねばならない事はもう分かるだろう)。

しかし、どうやらChrome開発者側は、anywhereの実装についてまだ突き詰められていない問題があるらしく、まだanywhereを正式リリースしたくないらしい。
冒頭で

悲しいかな、正確には「将来究極になる予定の」

と言ったのは、こういう事情だ。

とはいえ、いずれanywhereが正式リリースされるのは期待できる。
また、念の為overflow-wrap/word-wrapの方も併用して、

word-break: break-all;        /* 当記事と同じ */
overflow-wrap: break-word;    /* 〃 */
word-wrap: break-word;        /* 〃 */
line-break: anywhere;         /* 見慣れない。 */

とする事で、なんと完全理想の文字折り返しが実現するのだ。
これこそが、先程の「私のたどり着いた究極の解決方法」のうち、最初の4つだ。

ただ、実はこの話、更に続きがある。
残る2つ、5・6番目のwhite-space達についてだ。

これで、折り返しの敵を全て倒せたのか?いいえ、まだ空白類があります

※以降は、連続する空白類をそのまま表示したい場合、つまり、
連続空白を表示する為にwhite-space: pre-wrap;を指定した時の話だ。
デフォルトのまま、空白類を切り詰めた表示のままでいいのなら、以降は読まなくてもいい。

貴方は知っていただろうか…

連続する空白類は、連続する句読点類(!!!!!!!!!!!!!!!!等)よりも更に屈強なのだ。
連続した半角空白、つまり、width指定を超えるほどの長い連続空白を含む「a                     bcde」のような文字列に対しては、line-break: anywhere;ですら無力なのだ。
※このコメント欄には連続半角空白を打ち込めないため便宜上全角空白で書いてあるが、実際には半角空白。

どのように表示崩れが起こるのかというと、
a                     」の部分が折り返さずに右に突き抜け、連続空白の終わった直後の「bcde」が次の行頭に来てしまう。

つまり、括弧や句読点類だけでなく、半角空白も行頭禁則処理の対象となっており(半角空白も行頭に来ないよう処理されており)、しかもその制御にline-break: anywhere;が効かないのだ。空白類への禁則処理は、別個に行われているようだ。
言われてみれば、HTMLで行頭に半角空白を見た事がない。
そして、どうやらline-break: anywhere;は空白類に関しては管轄外らしい。

「これでは、幅を超えるほどの大量の連続半角空白を入力された場合のレイアウト崩れを防げないではないか!line-break: anywhere;は、半角空白に対しても効くべきでは?それともこれは仕様か?仕様ならば、回避策は用意されてないのか?」と思い、これもChrome開発者へ報告した。それが以下だ:
Sample: https://ss1.xrea.com/ango.g3.xrea.com/jkkn/chrome_bug14.html
Issue: https://bugs.chromium.org/p/chromium/issues/detail?id=966773

すると今度はChrome開発者側から、
更なる新たなCSS、white-space: break-spaces;を教えて頂いたのだ。驚愕だ。

従来からあるwhite-space: pre-wrap;は、空白類を詰めずにそのまま表示する代わりに、行末の空白類については次の行頭に折り返さずに無理やりはみ出させる挙動をとる。wrapとか言うくせに、空白類についてはそうじゃなかったのだ。
break-spacesは、このはみ出しを防いでくれる。それ以外の挙動はpre-wrapと一緒だ。

これが、冒頭の「究極の解決方法」の6行目white-space: break-spaces;の意味だ。

しかし、break-spacesline-break以上に新しいもので、2020年1月時点では非対応ブラウザの方が多いくらいだ。Chromeでも、Experimental Web Platform featuresフラグを有効にしないと使えない。その為、非対応環境の為にwhite-space: pre-wrap;を併記した方がいいだろう。それが、冒頭の「究極の解決方法」の5行目の真意だ。

纏めると、

CSSでは、word-breakoverflow-wrap/word-wrapとは別に、
括弧や句読点等(但し空白類は含まない)の禁則処理を扱うプロパティとしてline-breakが用意され、
更にそれとは別に、
空白類の禁則処理を扱う値としてwhite-space: break-spaces;も用意されている

という事になる。複雑怪奇。


以上が、私が折り返しとCSSと闘った全貌だ。長々と済まぬが、情報共有したい。

23
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12