若要讓來源文字中的單一換行視為空格、空白行則開始新段落,請先建立段落結構,再套用一般文字換行。在 Mac 上,你可以在本機瀏覽器頁面中測試;單靠 CSS 無法區分這兩者。

本指南適用於準備英文內容,以供簡易網頁或 Markdown 預覽使用的人。目標是在保留易讀的原始碼行的同時,產生能隨瀏覽器視窗寬度變化而重新換行的流動段落。

為什麼 CSS 空白設定無法同時滿足兩項需求

請看以下合成的來源文字。每個段落占兩個來源行,段落之間以空白行分隔:

Panel A introduces the layout
and describes the first section.

Panel B introduces another section
and describes the remaining space.

預期結果是兩個段落。每個段落內的來源換行會轉成空格。接著,瀏覽器會依可用寬度決定實際顯示的換行位置。

使用 white-space: normal 時,來源換行會和其他可折疊的空白一樣折疊。連續換行也包含在內:來源中的空白行不會建立段落。使用 white-space: pre-line 時,換行字元會保留為換行,包括你想合併的單一換行。MDN 在其 white-space 屬性參考中說明了這些行為。

套用至單一文字區塊的設定 單一來源換行 空白來源行 適合這項需求嗎?
white-space: normal 會與空白一起折疊 也會折疊 只有在建立段落之後才適合
white-space: pre-line 保留為換行 保留換行 不適合
white-space: pre-wrap 保留為換行 保留換行 不適合
使用 white-space: normal 的獨立段落元素 在各段落內流動 以段落邊界表示 適合

W3C 將空白處理描述為一種呈現作業,不會變更底層文件資料。其分段符號規則也取決於周圍文字;將換行替換成空格適用於此處使用的英文內容,並非適用於所有文字的通用轉換。請參閱 CSS Text Module Level 3。

因此,實際解法分成兩個步驟:先辨識來源文字中的段落,再讓瀏覽器排版。WHATWG HTML 標準將 p 元素定義為代表段落。段落之間的可見間隔可透過樣式獨立控制。請參閱 p 元素。

選擇符合來源格式的做法

變更任何內容之前,請先參考以下判斷指南。它將來源的解讀方式與外觀分開,能避免修錯層級。

你目前的內容 做法
只有文章內容的純文字 依空白行分割,在每個段落內合併單一換行,並建立段落元素
含有標題、清單或程式碼區塊的 Markdown 使用 Markdown 剖析器,並檢查軟換行的行為
已使用個別段落元素的現有 HTML 檢查段落的空白與間距樣式
含有明確換行元素的頁面 若換行並非預期,請修正來源或呈現設定
只能檢視的頁面 使用可用的來源或匯出工作流程;此處的範例需要能控制文件

在辨識段落之前,不要將每個換行都替換成空格:這會移除區分段落所需的分隔符。同樣地,也不要以重複的 br 元素取代段落結構。WHATWG 將 br 定義為換行,並指出不得用它來分隔段落內的主題群組。請參閱 br 元素。

使用 TextEdit 建立本機工作範例

以下完整頁面需要 TextEdit,以及已啟用 JavaScript 的瀏覽器。頁面包含可編輯的來源欄位和獨立預覽,且不使用外部程式庫。

  1. 開啟 TextEdit,選擇 File > New,然後選擇 Format > Make Plain Text。
  2. 貼上以下完整範例。
  3. 選擇 File > Save,並將檔案命名為 paragraph-demo.html。
  4. 如果系統詢問副檔名,請選擇 Use .html。
  5. 使用 Safari 或其他桌面瀏覽器開啟儲存的檔案。

Apple 在 在 Mac 上使用 TextEdit 處理 HTML 文件中說明了建立純文字文件與設定副檔名的步驟。重新開啟檔案編輯時,請在 TextEdit > Settings > Open and Save 下使用 TextEdit 的 Display HTML files as HTML code instead of formatted text 設定。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Paragraph flow demo</title>
  <style>
    body {
      max-width: 46rem;
      margin: 2rem auto;
      padding: 0 1rem;
      font: 1rem/1.6 system-ui, sans-serif;
    }
    label { display: block; }
    textarea {
      box-sizing: border-box;
      width: 100%;
      min-height: 12rem;
      font: 1rem/1.5 monospace;
    }
    #preview p {
      white-space: normal;
      margin: 0;
    }
    #preview p + p { margin-top: 1em; }
  </style>
</head>
<body>
  <h1>Paragraph flow demo</h1>
  <label for="source">Source text</label>
  <textarea id="source">Panel A introduces the layout
and describes the first section.

Panel B introduces another section
and describes the remaining space.</textarea>
  <h2>Preview</h2>
  <div id="preview"></div>
  <script>
    function toParagraphs(source) {
      return source.replace(/\r\n?/g, '\n')
        .split(/\n[ \t]*\n(?:[ \t]*\n)*/)
        .map(block => block
          .replace(/^[ \t\n]+|[ \t\n]+$/g, '')
          .replace(/[ \t]*\n[ \t]*/g, ' '))
        .filter(text => text.length > 0);
    }

    const source = document.getElementById('source');
    const preview = document.getElementById('preview');

    function render() {
      const paragraphs = toParagraphs(source.value).map(text => {
        const paragraph = document.createElement('p');
        paragraph.textContent = text;
        return paragraph;
      });
      preview.replaceChildren(...paragraphs);
    }

    source.addEventListener('input', render);
    render();
  </script>
</body>
</html>

轉換器會先將歸位字元與 CRLF 換行正規化為 LF。接著,在空行或只含一般空格與定位字元的行處分割文字。在各個區塊中,剩餘的換行符號及其相鄰的空格或定位字元會替換成一個一般空格。

這些是此範例明確採用的規則,不是通用文件剖析器。多個空白行只會產生一個段落邊界。開頭與結尾的一般空白會移除。空白輸入不會產生段落。原始來源仍顯示在輸入欄位中,因此可以與轉換結果比較。

每個輸出段落都會透過 textContent 設定文字。MDN 說明,設定此屬性會以文字節點取代節點的子節點。此處只會將它用於新建立的空段落。請參閱 Node:textContent 屬性。

轉換函式已在 Node.js 中通過六項確定性檢查:一般段落輸入、CRLF 輸入、歸位字元輸入、只有空白的輸入、重複空行,以及保留不換行空格。製作期間未執行瀏覽器呈現;請在 Mac 上使用下方的視覺檢查確認結果。

分別檢查段落邊界與換行

先使用提供的範例。預覽中應有一個段落顯示 Panel A introduces the layout and describes the first section.,接著是相應的 Panel B 段落。視窗較窄時,任一段落都可能顯示為數行。

接著進行以下受控編輯:

  1. 將 layout 後的單一換行替換成空格。預覽的段落結構應保持不變。
  2. 還原該換行,並移除兩個區塊之間的空白行。預覽應變成一個段落。
  3. 在兩個區塊之間加入只含空格或定位字元的空白行。預覽應恢復為兩個段落。
  4. 在此處加入數個空白行。間隔仍應只有一個段落間距。
  5. 調整瀏覽器視窗大小。段落邊界應保持固定,而每個段落內的行則會重新換行。

這個流程能區分來源邊界與瀏覽器換行。在此範例中,段落間距來自相鄰段落的邊界間距規則。變更該間距會改變可見空隙,但不會改變來源的剖析方式。

若未使用此轉換器而只採用 white-space: normal,視覺上合併並不代表來源換行已被刪除。MDN 說明,即使 CSS 在呈現時折疊空白,DOM 中仍保留一般文字內容的空白。請參閱 處理空白。

使用 Markdown 剖析處理 Markdown 文件

Markdown 已提供此處所需的段落慣例。CommonMark 將段落定義為連續的非空白行,且這些行不屬於其他區塊類型;空白行會分隔段落。段落內的一般換行屬於軟換行,呈現器可將其輸出為換行或空格。請參閱 CommonMark 段落和軟換行。

將合成範例貼到 Markdown 編輯器的來源檢視中,並檢查預覽。如果每個單一換行都產生可見的換行,請檢查編輯器已說明的呈現選項。不要假設所有 Markdown 應用程式都有相同設定。

若專案使用名為 markdown-it 的程式庫,其文件所述的 breaks 選項預設為 false。將它設為 true 會把段落換行轉換為 br 元素。保持為 false 則可支援流動段落的結果,前提是輸出 CSS 也會折疊空白。請參閱 MarkdownItOptions。

Markdown 中明確的硬換行仍需留意。根據 CommonMark,行尾兩個以上的空格或尾端反斜線都會建立硬換行。只在這些標記並非預期時移除它們。規則記載於硬換行。

不要對整份 Markdown 文件執行純文字轉換器。它無法辨識標題、清單、圍欄式程式碼或縮排規則。請讓 Markdown 剖析器保留這些結構,並對產生的文章段落套用空白樣式。

保留瀏覽器與文字塑形的邊界

在可行的情況下,請讓每個文章段落保持為連續文字。此範例為每個段落建立一個文字節點,並讓瀏覽器處理換行與字形排版。它不會測量文字長度,也不會在每個字元周圍插入獨立元素。

這項界線對排版很重要。W3C 規定了字形塑形必須跨越行內元素邊界延續,以及必須中斷的情況,其中包括特定的間距與對齊變更。避免只為控制行的位置而加入不必要的行內方塊。請參閱 跨元素邊界的塑形。

只有在每個獨立的來源換行都是編輯時產生的折行時,才使用此轉換器。如果換行刻意用來分隔標籤,或出現在單字中間,將它與空格合併就會改變內容。此範例也會將含有不換行空格的行視為內容,而非空白分隔符;MDN 在處理空白中區分了不換行空格與一般可折疊的文件空白。

對英文內容而言,請先建立段落邊界,再讓瀏覽器在段落內正常換行。本機範例讓這項規則一目了然:來源保留可編輯的行,預覽包含真正的段落,而調整視窗大小只會改變可見的換行方式。

來源