Чтобы одиночные переводы строк в исходном тексте отображались как пробелы, а пустые строки начинали абзацы, сначала создайте структуру абзацев, а затем примените обычный перенос текста. На Mac это можно проверить на локальной странице в браузере; одного CSS недостаточно, чтобы различать эти случаи.

Это руководство предназначено для тех, кто готовит английский текст для простой веб-страницы или предварительного просмотра Markdown. Задача — сохранить удобочитаемые строки исходного текста и при этом получить связные абзацы, которые переносятся заново при изменении ширины окна браузера.

Почему настройка пробелов в CSS не решает обе задачи

Рассмотрим этот синтетический исходный текст. Каждый абзац занимает две строки, а абзацы разделены пустой строкой:

Панель A знакомит с макетом
и описывает первый раздел.

Панель B знакомит с другим разделом
и описывает оставшееся пространство.

В результате должны получиться два абзаца. Внутри каждого абзаца перевод строки в исходном тексте заменяется пробелом. Затем браузер выбирает видимые переносы строк в зависимости от доступной ширины.

При 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.

Поэтому практическое решение состоит из двух этапов: определить абзацы в исходном тексте, а затем позволить браузеру расположить их текст. Стандарт HTML WHATWG определяет элемент 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 описывает создание обычного текстового файла и выбор расширения в статье Работа с HTML-документами в TextEdit на Mac. Чтобы снова открыть файл для редактирования, включите в TextEdit параметр Display HTML files as HTML code instead of formatted text в меню TextEdit > Settings > Open and Save.

<!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>

Сначала преобразователь нормализует окончания строк CR и CRLF до LF. Затем он разделяет текст в местах, где строка пуста или содержит только обычные пробелы и знаки табуляции. В каждом получившемся блоке оставшийся перевод строки вместе с соседними пробелами или знаками табуляции заменяется одним обычным пробелом.

Это явные правила для данного примера, а не универсальный парсер документов. Несколько пустых строк образуют одну границу абзаца. Начальные и конечные обычные пробелы удаляются. Пустой ввод не создает абзацев. Исходный текст остается видимым в поле ввода, поэтому преобразование можно с ним сравнить.

Текст каждого выходного абзаца записывается через textContent. MDN описывает, что присваивание этому свойству заменяет дочерние узлы элемента текстовым узлом. Здесь оно присваивается только что созданным пустым абзацам. См. Node: свойство textContent.

Функция преобразования прошла шесть детерминированных проверок в Node.js: обычный ввод с абзацами, ввод с CRLF, ввод с CR, ввод только из пустых строк, повторяющиеся пустые строки и сохранение неразрывного пробела. При подготовке страницы ее отображение в браузере не проверялось; воспользуйтесь приведенными ниже визуальными проверками на своем Mac.

Проверяйте границы абзацев и переносы отдельно

Начните с предоставленного примера. В предварительном просмотре фраза Панель A знакомит с макетом и описывает первый раздел. должна составлять один абзац, а за ней должен идти соответствующий абзац о панели B. При узком окне любой из абзацев может занимать несколько видимых строк.

Теперь внесите следующие изменения по одному:

  1. Замените одиночный перевод строки после layout пробелом. Структура абзацев в предварительном просмотре должна остаться прежней.
  2. Восстановите этот перевод строки и удалите пустую строку между двумя блоками. В предварительном просмотре должен получиться один абзац.
  3. Добавьте между блоками пустую строку, содержащую только пробелы или знаки табуляции. Снова должны получиться два абзаца.
  4. Добавьте в это место несколько пустых строк. Интервал должен остаться равным одному промежутку между абзацами.
  5. Измените размер окна браузера. Граница абзаца должна остаться на месте, а строки внутри каждого абзаца — перенестись заново.

Эта последовательность позволяет различить границы исходного текста и переносы, выполняемые браузером. В примере интервал между абзацами задается правилом о полях соседних абзацев. Изменение этого поля меняет видимый интервал, но не влияет на разбор исходного текста.

Если использовать white-space: normal без этого преобразователя, визуальное объединение текста не означает, что перевод строки удален из исходного текста. MDN объясняет, что пробелы в обычном текстовом содержимом остаются в DOM, даже когда CSS схлопывает их при отображении. См. Обработка пробелов.

Используйте парсер Markdown для документов Markdown

В Markdown уже есть нужное здесь соглашение для оформления абзацев. CommonMark определяет абзац как последовательность непустых строк, которые не относятся к другому типу блока; пустые строки разделяют абзацы. Обычный перевод строки внутри абзаца считается мягким переносом, который средство отображения может выводить как перевод строки или пробел. См. абзацы CommonMark и мягкие переводы строк.

Вставьте синтетический пример в режим редактирования исходного текста Markdown-редактора и изучите предварительный просмотр. Если каждый одиночный перевод строки отображается как перенос, проверьте документированные настройки отображения редактора. Не предполагайте, что во всех приложениях для Markdown есть одна и та же настройка.

Если в проекте используется библиотека markdown-it, то ее документированная настройка breaks по умолчанию имеет значение false. Значение true заставляет преобразовывать переводы строк в абзацах в элементы br. Значение false поддерживает отображение связных абзацев при условии, что CSS результата также схлопывает пробелы. См. MarkdownItOptions.

Явные жесткие переносы строк в Markdown все равно требуют внимания. Согласно CommonMark, два или более пробела в конце строки или завершающий обратный слеш могут создать жесткий перенос. Удаляйте эти маркеры только там, где они не нужны. Правила описаны в разделе жесткие переносы строк.

Не запускайте преобразователь обычного текста для всего документа Markdown. Он не распознает заголовки, списки, блоки кода с ограждениями и правила отступов. Позвольте парсеру Markdown сохранить эту структуру, а стили пробелов применяйте к получившимся абзацам прозы.

Сохраняйте границы браузерного набора текста

По возможности сохраняйте текст каждого абзаца связным. Пример создает один текстовый узел на абзац и оставляет браузеру переносы строк и размещение символов. Он не измеряет слова и не вставляет отдельный элемент вокруг каждого символа.

Эта граница важна для типографики. W3C указывает случаи, когда формирование текста должно продолжаться через границы встроенных элементов, и случаи, когда оно должно прерываться, в том числе при некоторых изменениях интервалов и выравнивания. Не добавляйте лишние встроенные блоки лишь для управления положением строк. См. Формирование текста на границах элементов.

Используйте преобразователь только тогда, когда каждый отдельный перевод строки в исходном тексте является редакторским переносом. Если перевод строки намеренно разделяет подписи или находится внутри слова, замена его пробелом изменит содержимое. Кроме того, в примере строка с неразрывным пробелом считается содержимым, а не пустым разделителем; MDN отличает неразрывные пробелы от обычных схлопываемых пробелов документа в статье Обработка пробелов.

Для английского текста сначала определите границы абзацев, а затем разрешите браузеру выполнять обычный перенос строк внутри них. Локальный пример позволяет проверить это правило: исходный текст сохраняет редактируемые строки, предварительный просмотр содержит настоящие абзацы, а изменение размера окна влияет только на видимые переносы строк.

Источники