ソース内の単独の改行をスペースとして表示し、空行を段落の開始位置にするには、通常のテキスト折り返しを適用する前に段落構造を作成します。Macでは、ローカルのブラウザページでこれを試せます。CSSだけではこの違いを表現できません。

このガイドは、シンプルなウェブページやMarkdownプレビュー用に英語の文章を準備する人を対象としています。目的は、読みやすいソースの行を保ちながら、ブラウザウィンドウの幅が変わると再折り返しされる、流れるような段落を作ることです。

CSSの空白設定だけでは両方の要件を満たせない理由

次の例のソーステキストを見てください。各段落はソース内の2行で構成され、段落の間には空行があります。

Panel A introduces the layout
and describes the first section.

Panel B introduces another section
and describes the remaining space.

意図する結果は2つの段落です。各段落内では、ソースの改行をスペースにします。ブラウザは、利用可能な幅に応じて表示上の改行位置を決めます。

white-space: normalでは、ソースの改行はほかの折り畳み可能な空白とともに折り畳まれます。連続する改行も同様です。空のソース行は段落を作りません。white-space: pre-lineでは、改行文字が行区切りとして保持されるため、つなげたかった単独の改行もそのまま残ります。MDNは、こうした動作をwhite-spaceプロパティのリファレンスで説明しています。

1つのテキストブロックに適用する設定 ソース内の単独の改行 空のソース行 この用途に適しているか
white-space: normal 空白とともに折り畳まれる こちらも折り畳まれる 段落を作成した後なら適している
white-space: pre-line 改行を保持する 改行を保持する いいえ
white-space: pre-wrap 改行を保持する 改行を保持する いいえ
white-space: normalを指定した個別の段落要素 段落内で流れる 段落の境界で表現される はい

W3Cは、空白処理を、基になる文書データを変更しないレンダリング操作として説明しています。また、W3Cのセグメント区切りの規則は前後のテキストにも左右されます。ここで使う英語の文章では、改行をスペースに置き換えるのが適切ですが、あらゆるテキストに適用できる一律の変換ではありません。CSS Text Module Level 3を参照してください。

したがって、実用的な解決策は2段階です。まずソース内の段落を認識し、次にブラウザにテキストをレイアウトさせます。WHATWG HTML Standardでは、p要素を段落を表すものと定義しています。段落の見た目上の間隔は、スタイルで個別に調整できます。p要素を参照してください。

ソースに合った方法を選ぶ

変更を加える前に、次の判断材料を参考にしてください。ソースの解釈と見た目を切り分けることで、誤った層を修正せずに済みます。

手元にあるもの 対応方法
文章だけを含むプレーンテキスト 空行で分割し、各段落内の単独の改行をつなぎ、段落要素を作成する
見出し、リスト、コードブロックを含むMarkdown Markdownパーサーを使い、ソフト改行の扱いを確認する
個別の段落要素がすでにあるHTML 段落の空白設定と間隔のスタイルを確認する
明示的な改行要素を含むページ その改行が意図しないものなら、ソースまたはレンダリング設定を修正する
表示することしかできないページ 利用可能なソースやエクスポートの手順を使う。この例では文書を編集できる必要がある

段落を識別する前にすべての改行をスペースに置き換えないでください。段落を区別するための区切りが失われます。同様に、段落構造の代わりにbr要素を繰り返し使わないでください。WHATWGはbrを改行として定義し、段落内のテーマ上のまとまりを分けるために使ってはならないとしています。br要素を参照してください。

TextEditでローカルの作業例を作成する

次の完全なページを使うには、TextEditとJavaScriptが有効なブラウザが必要です。編集可能なソース欄と独立したプレビューが含まれています。外部ライブラリは使いません。

  1. TextEditを開いてファイル > 新規を選び、次にフォーマット > プレーンテキストにするを選びます。
  2. 下の完全な例を貼り付けます。
  3. ファイル > 保存を選び、ファイル名をparagraph-demo.htmlにします。
  4. 拡張子について確認されたら、 .htmlを使用を選びます。
  5. 保存したファイルをSafariまたはほかのデスクトップブラウザで開きます。

Appleは、MacのTextEditでHTML書類を操作するでプレーンテキストの作成方法と拡張子の手順を説明しています。ファイルを再び編集する際は、TextEdit > 設定 > 開く/保存にある「HTMLファイルをフォーマット済みテキストではなくHTMLコードとして表示」設定を使ってください。

<!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に正規化します。次に、空の行、または通常のスペースとタブだけを含む行でテキストを分割します。各ブロック内では、残った改行と隣接するスペースまたはタブを、通常のスペース1つに置き換えます。

これらはこの例に限った明示的なルールであり、汎用の文書パーサーではありません。空行が複数あっても、段落の境界は1つになります。先頭と末尾の通常の空白は取り除かれます。空白だけの入力からは段落が作られません。変換前のソースは入力欄に表示されたままなので、変換結果と比較できます。

出力される各段落には、textContentを使ってテキストを設定します。このプロパティに値を割り当てると、ノードの子要素がテキストノードに置き換わることがMDNに記載されています。ここでは、新たに作成した空の段落にのみ値を設定します。Node: textContentプロパティを参照してください。

変換関数はNode.jsで6つの決定的なチェックを通過しました。通常の段落入力、CRLF入力、キャリッジリターン入力、空白のみの入力、連続する空行、改行なしスペースの保持です。準備時にはブラウザでのレンダリングを実行していません。Macで下記の目視確認を行ってください。

段落の境界と折り返しを個別に確認する

付属のサンプルから始めてください。プレビューでは、Panel A introduces the layout and describes the first section.が1つの段落として表示され、その後に対応するPanel Bの段落が続きます。ウィンドウ幅が狭い場合、どちらの段落も表示上は複数行になることがあります。

次の変更を1つずつ試してください。

  1. layoutの後の単独の改行をスペースに置き換えます。プレビューの段落構造は変わらないはずです。
  2. その改行を戻し、2つのブロックの間の空行を削除します。プレビューは1つの段落になるはずです。
  3. ブロック間にスペースまたはタブだけを含む空行を追加します。プレビューは2つの段落に戻るはずです。
  4. その位置に空行を複数追加します。段落間の間隔は1つ分のままです。
  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では、行末の2つ以上のスペース、または行末のバックスラッシュによって強制改行が生じる場合があります。意図しない箇所にあるものだけを削除してください。規則は強制改行に記載されています。

プレーンテキスト用の変換処理をMarkdown文書全体に適用しないでください。見出し、リスト、フェンス付きコード、インデントの規則を認識できません。こうした構造はMarkdownパーサーに保持させ、解析後の文章段落に空白のスタイルを適用してください。

ブラウザとテキスト整形の境界を保つ

可能な限り、文章の段落は連続したテキストのままにしてください。この例では段落ごとに1つのテキストノードを作成し、行の折り返しとグリフのレイアウトをブラウザに任せています。単語の長さを測ったり、文字ごとに別の要素を挿入したりはしません。

この境界はタイポグラフィ上重要です。W3Cは、特定の間隔や配置の変更を含め、インライン要素の境界をまたいで字形の整形を継続する場合と、そこで中断する場合を規定しています。行の位置を制御するだけの目的で、不要なインラインボックスを追加しないでください。要素境界をまたぐ字形整形を参照してください。

変換処理は、ソース内で独立した改行がすべて編集上の折り返しである場合にのみ使ってください。改行がラベルを意図的に分けている場合や、単語の途中にある場合は、スペースに置き換えると内容が変わります。また、この例では改行なしスペースを含む行は空の区切りではなく、内容として扱います。MDNは、空白の扱いで、改行なしスペースと通常の折り畳み可能な文書内の空白を区別しています。

英語の文章では、まず段落の境界を設定し、その内部の折り返しを通常のブラウザ動作に任せてください。ローカルの例では、このルールを実際に確認できます。ソースの行は編集可能なまま保たれ、プレビューには実際の段落が表示され、ウィンドウのサイズ変更で変わるのは表示上の行折り返しだけです。

参考資料