Как обрабатывается пробел в HTML, CSS и DOM
Наличие пробелов в DOM может вызывать проблемы с макетом и затруднять неожиданным образом манипулирование деревом содержимого, в зависимости от их расположения. В этой статье рассматриваются случаи, когда могут возникнуть трудности, и способы их минимизации.
Что такое пробел?
Пробел — это любая строка текста, состоящая только из пробелов, табуляций или символов новой строки (точнее, последовательности CRLF, возврата каретки или перевода строки). Эти символы позволяют форматировать код так, чтобы его было легко читать вам и другим людям. Фактически, большая часть нашего исходного кода содержит эти символы пробела, и мы обычно избавляемся от них на этапе создания производственного билда, чтобы уменьшить размер загружаемого кода.
HTML в значительной степени игнорирует пробелы?
В случае HTML пробелы в значительной степени игнорируются — пробелы между словами рассматриваются как один символ, а пробелы в начале и конце элементов и вне элементов игнорируются. Рассмотрим следующий минимальный пример:
<!doctype html> <h1> Hello World! </h1>
Этот исходный код содержит несколько символов новой строки после doctype и множество пробелов до, после и внутри элемента <h1>, но браузер, похоже, совершенно не обращает на них внимания и отображает слова "Hello World!" так, как будто этих символов вообще не существует:
Это сделано для того, чтобы символы пробела не влияли на макет страницы. Размещение пробелов вокруг и внутри элементов — задача CSS.
Что происходит с пробелами?
Однако они не просто исчезают.
Все символы пробела, которые находятся вне HTML-элементов в исходном документе, представлены в DOM. Это необходимо для того, чтобы редактор сохранял форматирование документов. Это означает, что:
- Будут некоторые текстовые узлы, содержащие только пробелы, и
- Некоторые текстовые узлы будут содержать пробелы в начале или конце.
Например, рассмотрим следующий документ:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>My Document</title>
</head>
<body>
<h1>Header</h1>
<p>Paragraph</p>
</body>
</html>
Дерево DOM для этого выглядит следующим образом:
Сохранение символов пробела в DOM полезно во многих отношениях, но в некоторых местах это затрудняет реализацию определенных макетов и создает проблемы для разработчиков, которые хотят перебирать узлы в DOM. Мы рассмотрим эти вопросы и некоторые решения позже.
Как CSS обрабатывает пробелы?
Большинство символов пробела игнорируются, но не все. В приведенном ранее примере один из пробелов между "Hello" и "World!" все еще существует при отображении страницы в браузере. В движке браузера существуют правила, определяющие, какие символы пробела полезны, а какие нет. Эти правила, по крайней мере частично, определены в CSS Text Module Level 3, а особенно в разделах о свойстве CSS white-space и подробностях обработки пробелов, но мы также предлагаем более простое объяснение ниже.
Пример
Рассмотрим еще один пример. Для большей наглядности мы добавили комментарии, показывающие все пробелы ◦, все табуляции ⇥ и все символы новой строки ⏎:
Этот пример:
<h1> Hello
<span> World!</span> </h1>
<!--
<h1>◦◦◦Hello◦⏎
⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>
-->
отображается в браузере следующим образом:
Объяснение
Элемент <h1> содержит только инлайновые элементы. Фактически он содержит:
- Текстовый узел (состоящий из некоторых пробелов, слова "Hello" и некоторых табуляций).
- Инлайновый элемент (элемент
<span>, который содержит пробел и слово "World!") - Другой текстовый узел (состоящий только из табуляций и пробелов).
Из-за этого устанавливается так называемый контекст форматирования inline. Это один из возможных контекстов рендеринга макета, с которыми работают движки браузеров.
Внутри этого контекста обработка символов пробела может быть сведена к следующему:
-
Сначала все пробелы и табуляции непосредственно перед и после символов новой строки игнорируются, поэтому, если мы возьмем наш пример разметки из предыдущего примера:
<h1>◦◦◦Hello◦⏎ ⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>
...и применим это первое правило, получим:
<h1>◦◦◦Hello⏎ <span>◦World!</span>⇥◦◦</h1>
-
Далее, все символы табуляции обрабатываются как пробелы, поэтому пример становится:
<h1>◦◦◦Hello⏎ <span>◦World!</span>◦◦◦</h1>
-
Затем, символы новой строки преобразуются в пробелы:
<h1>◦◦◦Hello◦<span>◦World!</span>◦◦◦</h1>
-
После этого, любые пробелы, непосредственно следующие за другими пробелами (даже через два отдельных инлайновых элемента), игнорируются, поэтому в итоге мы получаем:
<h1>◦Hello◦<span>World!</span>◦</h1>
-
И, наконец, последовательности пробелов в начале и конце элемента удаляются, поэтому в итоге мы получаем это:
<h1>Hello◦<span>World!</span></h1>
Вот почему посетители веб-страницы увидят фразу "Hello World!" красиво написанной в верхней части страницы, а не странно отступом "Hello", за которым следует еще более странно отступом "World!" в следующей строке.
Примечание: Firefox DevTools поддерживает выделение текстовых узлов с версии 52, что облегчает отображение того, в каких узлах содержатся символы пробела. Чистые узлы пробелов помечены меткой "whitespace".
Пробелы в контекстах форматирования блоков
Выше мы только что рассмотрели элементы, содержащие инлайновые элементы и контексты форматирования inline. Если элемент содержит по крайней мере один блочный элемент, то он вместо этого устанавливает так называемый контекст форматирования блоков.
В этом контексте пробелы обрабатываются совершенно по-другому.
Пример
Давайте рассмотрим пример, чтобы объяснить, как это работает. Мы отметили символы пробела так же, как и раньше.
У нас есть 3 текстовых узла, содержащих только пробелы: один перед первым <div>, один между двумя <div> и один после второго <div>.
<body> <div> Hello </div> <div> World! </div> </body> <!-- <body>⏎ ⇥<div>◦◦Hello◦◦</div>⏎ ⏎ ◦◦◦<div>◦◦World!◦◦</div>◦◦⏎ </body> -->
Это отображается следующим образом:
Объяснение
Мы можем сформулировать, как обрабатываются пробелы здесь (могут быть небольшие различия в точном поведении между браузерами, но в целом это работает так):
-
Поскольку мы находимся внутри контекста форматирования блоков, всё должно быть блоком, поэтому наши 3 текстовых узла также становятся блоками, как и 2
<div>. Блоки занимают всю доступную ширину и располагаются друг над другом, что означает, что, начиная с приведенного выше примера:<body>⏎ ⇥<div>◦◦Hello◦◦</div>⏎ ⏎ ◦◦◦<div>◦◦World!◦◦</div>◦◦⏎ </body>
...мы получаем макет, состоящий из этого списка блоков:
<block>⏎⇥</block> <block>◦◦Hello◦◦</block> <block>⏎⏎◦◦◦</block> <block>◦◦World!◦◦</block> <block>◦◦⏎</block>
-
Это затем упрощается дальше, применяя правила обработки пробелов в контекстах форматирования inline к этим блокам:
<block></block> <block>Hello</block> <block></block> <block>World!</block> <block></block>
-
3 пустых блока, которые у нас теперь есть, не будут занимать никакого места в окончательном макете, поскольку они не содержат ничего, поэтому в итоге у нас останутся только 2 блока, занимающих место на странице. Пользователи веб-страницы видят слова "Hello" и "World!" на 2 отдельных строках, как и ожидается от двух
<div>. Движок браузера по существу проигнорировал все пробелы, которые были добавлены в исходный код.
Пробелы между инлайновыми и инлайно-блочными элементами
Давайте перейдем к рассмотрению нескольких проблем, которые могут возникнуть из-за пробелов, и как их решить. Прежде всего, рассмотрим, что происходит с пробелами между инлайновыми и инлайно-блочными элементами. Фактически, мы уже видели это в нашем первом примере, когда описывали обработку пробелов внутри контекстов форматирования inline.
Мы сказали, что есть правила игнорирования большинства символов, но символы, разделяющие слова, сохраняются. Когда вы имеете дело только с блочными элементами, такими как <p>, которые содержат только инлайновые элементы, такие как <em>, <strong>, <span>, и т. д., вы обычно этим не беспокоитесь, потому что дополнительные пробелы, которые попадают в макет, помогают разделить слова в предложении.
Однако становится интереснее, когда вы начинаете использовать элементы inline-block. Эти элементы ведут себя как инлайновые элементы снаружи и как блочные элементы внутри и часто используются для отображения более сложных элементов интерфейса, например, элементов навигационного меню, рядом друг с другом в одной строке.
Поскольку они являются блочными элементами, многие люди ожидают, что они будут вести себя как таковые, но на самом деле это не так. Если между соседними инлайновыми элементами есть пробельные символы форматирования, это приведет к пробелу в макете, как и пробелы между словами в тексте.
Пример
Рассмотрим этот пример (ещё раз, мы добавили HTML-комментарий, который показывает символы пробелов в HTML):
.people-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.people-list li {
display: inline-block;
width: 2em;
height: 2em;
background: #f06;
border: 1px solid;
}
<ul class="people-list"> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <!-- <ul class="people-list">⏎ ◦◦<li></li>⏎ ⏎ ◦◦<li></li>⏎ ⏎ ◦◦<li></li>⏎ ⏎ ◦◦<li></li>⏎ ⏎ ◦◦<li></li>⏎ </ul> -->
Это отображается следующим образом:
Вероятно, вы не хотите этих пробелов между блоками — в зависимости от сценария использования (это список аватаров или горизонтальные кнопки навигации?), вы, вероятно, хотите, чтобы стороны элементов были прижаты друг к другу и могли самостоятельно управлять интервалами.
Инспектор HTML в инструментах разработчика Firefox выделит текстовые узлы и покажет, какую область занимают элементы — полезно, если вы хотите понять, что вызывает проблему и, возможно, думаете, что у вас есть дополнительные отступы или что-то в этом роде!
Способы решения
Существует несколько способов решения этой проблемы:
Используйте Flexbox, чтобы создать горизонтальный список элементов вместо попытки inline-block решения. Это обрабатывает все за вас и определенно является предпочтительным решением:
ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
}
Если вам необходимо полагаться на inline-block, вы можете установить font-size списка на 0. Это работает только в том случае, если ваши блоки не масштабируются с помощью ems (основанных на font-size, поэтому размер блока также будет 0). Хорошим выбором здесь были бы rems:
ul {
font-size: 0;
/* … */
}
li {
display: inline-block;
width: 2rem;
height: 2rem;
/* … */
}
Или вы можете установить отрицательный отступ для элементов списка:
li {
display: inline-block;
width: 2rem;
height: 2rem;
margin-right: -0.25rem;
}
Вы также можете решить эту проблему, разместив элементы списка в одном ряду в исходном коде, что приводит к тому, что узлы пробелов не создаются в первую очередь:
<li></li><li></li><li></li><li></li><li></li>
Обход DOM и пробелы
При попытке выполнить манипуляции с DOM в JavaScript вы также можете столкнуться с проблемами из-за узлов пробелов. Например, если у вас есть ссылка на родительский узел и вы хотите повлиять на его первого дочерний элемент с помощью Node.firstChild, если сразу после открывающего тега родительского узла есть нежелательный узел пробела, вы не получите ожидаемого результата. Вместо желаемого элемента будет выбран текстовый узел.
Ещё один пример: если у вас есть определённый поднабор элементов, для которых вы хотите что-то сделать, в зависимости от того, пустые они (не имеют дочерних узлов) или нет, вы можете проверить, пуст ли каждый элемент, используя что-то вроде Node.hasChildNodes(), но снова, если какие-либо целевые элементы содержат текстовые узлы, вы можете получить ложные результаты.
Вспомогательные функции для обработки пробелов
В приведенном ниже коде JavaScript определены несколько функций, которые упрощают работу с пробелами в DOM:
/**
* Throughout, whitespace is defined as one of the characters
* "\t" TAB \u0009
* "\n" LF \u000A
* "\r" CR \u000D
* " " SPC \u0020
*
* This does not use JavaScript's "\s" because that includes non-breaking
* spaces (and also some other characters).
*/
/**
* Determine whether a node's text content is entirely whitespace.
*
* @param nod A node implementing the `CharacterData` interface (i.e.,
* a `Text`, `Comment`, or `CDATASection` node)
* @return `true` if all of the text content of `nod` is whitespace,
* otherwise `false`.
*/
function isAllWs(nod) {
return !/[^\t\n\r ]/.test(nod.textContent);
}
/**
* Determine if a node should be ignored by the iterator functions.
*
* @param nod An object implementing the `Node` interface.
* @return `true` if the node is:
* 1) A `Text` node that is all whitespace
* 2) A `Comment` node
* and otherwise `false`.
*/
function isIgnorable(nod) {
return (
nod.nodeType === 8 || // a comment node
(nod.nodeType === 3 && isAllWs(nod))
); // a text node, all ws
}
/**
* Version of `previousSibling` that skips nodes that are entirely
* whitespace or comments. (Normally `previousSibling` is a property
* of all DOM nodes that gives the sibling node, the node that is
* a child of the same parent, that occurs immediately before the
* reference node.)
*
* @param sib The reference node.
* @return The closest previous sibling to `sib` that is not
* ignorable according to `isIgnorable`, or `null` if
* no such node exists.
*/
function nodeBefore(sib) {
while ((sib = sib.previousSibling)) {
if (!isIgnorable(sib)) {
return sib;
}
}
return null;
}
/**
* Version of `nextSibling` that skips nodes that are entirely
* whitespace or comments.
*
* @param sib The reference node.
* @return The closest next sibling to `sib` that is not
* ignorable according to `isIgnorable`, or `null`
* if no such node exists.
*/
function nodeAfter(sib) {
while ((sib = sib.nextSibling)) {
if (!isIgnorable(sib)) {
return sib;
}
}
return null;
}
/**
* Version of `lastChild` that skips nodes that are entirely
* whitespace or comments. (Normally `lastChild` is a property
* of all DOM nodes that gives the last of the nodes contained
* directly in the reference node.)
*
* @param sib The reference node.
* @return The last child of `sib` that is not ignorable
* according to `isIgnorable`, or `null` if no
* such node exists.
*/
function lastChild(par) {
let res = par.lastChild;
while (res) {
if (!isIgnorable(res)) {
return res;
}
res = res.previousSibling;
}
return null;
}
/**
* Version of `firstChild` that skips nodes that are entirely
* whitespace and comments.
*
* @param sib The reference node.
* @return The first child of `sib` that is not ignorable
* according to `isIgnorable`, or `null` if no
* such node exists.
*/
function firstChild(par) {
let res = par.firstChild;
while (res) {
if (!isIgnorable(res)) {
return res;
}
res = res.nextSibling;
}
return null;
}
/**
* Version of `data` that doesn't include whitespace at the beginning
* and end and normalizes all whitespace to a single space. (Normally
* `data` is a property of text nodes that gives the text of the node.)
*
* @param txt The text node whose data should be returned
* @return A string giving the contents of the text node with
* whitespace collapsed.
*/
function dataOf(txt) {
let data = txt.textContent;
data = data.replace(/[\t\n\r ]+/g, " ");
if (data[0] === " ") {
data = data.substring(1, data.length);
}
if (data[data.length - 1] === " ") {
data = data.substring(0, data.length - 1);
}
return data;
}
Пример
Следующий код демонстрирует использование вышеуказанных функций. Он итерирует по дочерним элементам элемента (все дочерние элементы — это элементы), чтобы найти тот, текст которого "This is the third paragraph", а затем изменяет атрибут класса и содержимое этого абзаца.
let cur = firstChild(document.getElementById("test"));
while (cur) {
if (dataOf(cur.firstChild) === "This is the third paragraph.") {
cur.className = "magic";
cur.firstChild.textContent = "This is the magic paragraph.";
}
cur = nodeAfter(cur);
}
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace