wbr 的這些那些

在 responsive design 成為主流之後,有個問題也隨之被突顯出來,就是文字的換行,尤其是標題文字的換行位置,現在的瀏覽器的換行方式簡單來說就是超出區塊範圍的東西都往下放到下一行,所以在某些情況下,就會有第一行很長第二行很短的狀況,視覺上非常不平衡,以下圖為例,網頁的副標題沒有作特殊處理,所以會有可能會變成兩行長度差距很大的樣子:

line breaking

這個問題有蠻多解決方法的,目前我知道的就有:

  1. 微調 responsvie 的樣式來避免出現不平衡的狀態
  2. 在特定地方插入 避免換行
  3. 用 flex layout 來控制換行位置

基本上就是只要你不擇手段,問題還是可以解決的,不過我一直以來都是會偏好用標準的方法來解決問題,所以整理了一下我所知道可以拿來用的東西:

  1. <wbr>HTML element
  2. white-spaceCSS property
  3. <nobr>HTML element
  4. text-wrapCSS property

首先的想法是<wbr>配上white-space: nowrap;或是<nobr>,不過意外的是大部分瀏覽器都不支援這個組合,也就是說,包在<nobr>內的<wbr>的地方在現在大部分主流瀏覽器內是不會換行的:

<nobr>ChatGPT: Optimizing<wbr>Language Models<wbr>for Dialogue</nobr>

這就激起了我的好奇心了,於是我開始仔細的找資料,看看<nobr><wbr>到底是怎樣運作的。首先,就來看看<wbr>吧,雖然他第一次出現在 W3C 的文件內就是在 HTML5,但是其實它已經出現了 20 年以上,最早是作為 Netscape 的 HTML 2 extension 的一員:

The WBR element stands for Word BReak. This is for the very rare case when you have a NOBR section and you know exactly where you want it to break. Also, any time you want to give the Netscape Navigator help by telling it where a word is allowed to be broken. The WBR element does not force a line break (BR does that) it simply lets the Netscape Navigator know where a line break is allowed to be inserted if needed.

在 Internet Archive 上找到的備份,最早的定義其實明確的說著<wbr>處應該是要可以優先於<nobr>的,我甚至還在 bugzilla 上找到一個 24 年前的 bug report 在講這件事,根據這張票最後的關掉前的討論,其實可以用</nobr><nobr>來達成一樣的效果,然後 Firefox 不打算支援<wbr>,看到這邊,我只能說這解法怎麼這麼天才(稱讚的意味)。

查到這邊,我還是很好奇為什麼現在的主流瀏覽器依然<wbr>優先度比<nobr>還低,所以繼續找資料,這次看的就是最新的文件了,首先是<wbr>定義,在 HTML5 中變成:

Thewbrelement represents a line break opportunity.

從 "word break" 變成 "line break opportunity" 了,<nobr>則和<wbr>一樣一開始是 Netscape extension,不過他倒是沒有進到 HTML5,事實上,雖然<nobr>不是 HTML5 的一員,但是文件中定義的 default style 還是有它

br { display-outside: newline; } /* this also has bidi implications */
nobr { white-space: nowrap; }
wbr { display-outside: break-opportunity; } /* this also has bidi implications */
nobr wbr { white-space: normal; }

其實就是等價於white-space: nowrap,而white-space屬性在 CSS2 時是用來定義空白的處理方式:

This property declares how white space inside the element is handled.

到 CSS Text Level 3 時定義說的更明確,是用來決定遇到 "line break opportunity" 時的處理方式和是不是要合併 space 字元的屬性:

其實在 Level 4 更是可以分開設定兩種屬性,white-space變成一個 shorthand,而看到這些定義的演進,其實也讓人發現最早命名時其實只有考慮到西方語言的特色。

看到這邊,會發現有一個新的名詞:"line break opportunity",有些地方是稱為 "wrap opportunity",這個名詞其實是出現在 CSS Module Text 文件中的,顧名思義,就是可以換行的位置,而這份文件也是定義換行邏輯的文件,不過這邊其實沒把換行演算法(line breaking algorithm)明確定義下來,而是闡明各種相關的 CSS 屬性和它們會怎樣影響換行的結果,例如換行有分強制(<br>)和非強制(<wbr>),然後不同 CSS 屬性會影響這些換行點的出現與否,至於文本之中,哪些地方可以換行,就是換行演算法的部分了,這部份在 W3C 文件沒有定義死,所以是允許瀏覽器自己決定的,不過有提供一些參考文件,像是 Unicode 的附件 14:"Unicode Line Breaking Algorithm",或是稱為 UAX14,這份文件要搭配 Unicode Database(UCD) 的 Line_Break Property 資料,文件中的第五章有仔細的說明各種不同的 line breaking class,然後資料庫則是定義了所有 Unicode 字元的 Line_Break property,接著的第六章就是最重要的換行演算法了,這邊列出了 31 條規則,基本上是反向列舉,說哪些地方不能換行,例如 WJ(word joiner)前後都不能換行,數字中間的符號前後也不能換行之類的,不過不確定哪些瀏覽器是實作 UAX14 的,Chrome 似乎有用到 UCD,Firefox 則是以 JIS X 4051 為基礎做的換行演算法,其實 JIS X 4051 是我所知道,二戰後世界,最早的正式的文字編排的標準,查到的紀錄是 1989 有一版,而 UAX14 第一個非草稿的版本則是要到 1999 年才出來。

回到現在主流瀏覽器不支援<wbr>放在white-space: nowrap裡面無法換行的問題,其實要回到這個 HTML 標籤在繪製時,是不是有 magic 的,什麼是 magic 呢?簡單說就是,你能不能用 CSS 來定義該標籤的樣子,以及你能不能用 CSS 來改掉這些預設的樣式,而<br><wbr>正好是有 magic 的標籤,可以想想看,要怎樣用 CSS 讓文字內產生一個換行點,可能有人會說剛剛上面才看到的,不過,其實那幾個display-outside的值只存在於以前的草稿中,CSS-WG 決議不為了這個問題新增多的 display 狀態,所以問題就回到 HTML-WG 這邊了,CSS-WG 的 fantasai 其實有給了一組 default style:

br { all: unset !important; display: contents !important; content: "\a" !important; white-space: pre !important; }
wbr { all: unset !important; display: contents !important; content: "\200B" !important; }

我把!important拿掉整理一下:

br {
  all: unset;
  display: contents;
  content: "\a";
  white-space: pre;
}
wbr {
  all: unset;
  display: contents;
  content: "\200B";
}

其中,\a就是換行字元\200B則是 ZWSP,zero width space,因為是 zero width,所以看不到,然後又因為是 space,所以可以用來把字切開,也就表示可以在該處斷行。不過這個版本有些問題,因為有很多瀏覽器還不支援在::before, ::after以外的物件上套用content屬性,所以 fantasai 又提供一版用::before的版本:

br, wbr { all: unset !important; display: contents !important; white-space: pre !important; }
br::before { all: unset !important; content: "\a" !important; }
wbr::before { all: unset !important; content: "\200B" !important; }

但是,實際上直接拿這組定義來用,還是一樣有問題,就是有些瀏覽器已經讓<wbr>有 magic 了,結果wbr::before是沒有用的,目前 HTML 標準的修改也就還卡在這邊(issue 則是另外一個),HTML-WG 的 Domenic 開了這個 PR 要讓<br><wbr>就用 magic 來實現效果,不過這討論已經停很久了,所以最後會是怎樣的方案還不知道。

總之目前的結論就是,現在如果想要讓<wbr>照其定義的一樣,可以在<nobr>或是white-space: nowrap內產生換行,是辦不到的,不過可以用其他的標籤來辦到,像是:

.wbr::before {
  display: inline;
  content: "\00200B";
  white-space: normal;
}

配上

<nobr>ChatGPT: Optimizing<span class="wbr">Language Models<span class="wbr">for Dialogue</nobr>

這個寫法也有出現在一份由 Leif Halvard Silli 在提交 bug 給 WebKit 所做的 test 內

到這邊,大概已經把自己控制換行位置的部分講的差不多了,不過其實,還有一個方法可以處理一開始所提到的換行結果不理想的問題,就是在 CSS Text Module Level 4 中有一個新的屬性叫做text-wrap,其中有一個屬性值是balance,合起來就是

text-wrap: balance;

這樣設定,預期的結果就是會換行換在每一行的寬度最接近的位置,不過當然還沒有瀏覽器支援,連 caniuse 都還查不到text-wrap屬性,只是還是有 JS 的解決方案:

最後的最後補上一些參考資料,一篇是古老的 IE 時代的東西,一篇則是現在的相關 CSS 屬性,一篇則是 balance wrap 的介紹: