Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

HTML-дерево PHP: Деревья

Древовидные структуры встречаются в разных областях: генеалогическое древо, файловая система и т.д. В этом уроке мы познакомимся с деревом разметки HTML, которое постоянно встречается в веб-разработке.

<html>
  <body>
    <h1>Сообщество</h1>
    <p>Общение между пользователями Хекслета</p>
    <hr>
    <input>
    <div class='hexlet-community'>
      <div class='text-xs-center'></div>
      <div class='fa fa-spinner'></div>
    </div>
  </body>
</html>

Корнем является тег html. Важно отметить, что некоторые теги не могут иметь вложенные теги внутри себя, например, hr и input.

Попробуем описать это дерево в такой структуре, с которой было бы удобно работать. Первым шагом необходимо описать для каждого тега свойства, которыми он обладает. Как минимум можно выделить такие свойства: имя, тип, класс, дети. В реальности таких свойств бывает гораздо больше, но сейчас нам достаточно этих. Теперь опишем дерево html в этой структуре:

$htmlTree = [
  'name' => 'html',
  'type' => 'tag-internal',
  'children' => [
    [
      'name' => 'body',
      'type' => 'tag-internal',
      'children' => [
        [
          'name' => 'h1',
          'type' => 'tag-internal',
          'children' => [
            [
              'type' => 'text',
              'content' => 'Сообщество',
            ],
          ],
        ],
        [
          'name' => 'p',
          'type' => 'tag-internal',
          'children' => [
            [
              'type' => 'text',
              'content' => 'Общение между пользователями Хекслета',
            ],
          ],
        ],
        [
          'name' => 'hr',
          'type' => 'tag-leaf',
        ],
        [
          'name' => 'input',
          'type' => 'tag-leaf',
        ],
        [
          'name' => 'div',
          'type' => 'tag-internal',
          'className' => 'hexlet-community',
          'children' => [
            [
              'name' => 'div',
              'type' => 'tag-internal',
              'className' => 'text-xs-center',
              'children' => [],
            ],
            [
              'name' => 'div',
              'type' => 'tag-internal',
              'className' => 'fa fa-spinner',
              'children' => [],
            ],
          ],
        ],
      ],
    ],
  ],
];

Главным свойством в каждом узле является тип узла. В нашем дереве есть теги и текст. Текст может быть вложен в тег, то есть может быть потомком. Поэтому текст является листовым узлом. Также у нас есть некоторые теги, которые являются листовыми узлами, поэтому для тегов выделено два типа: tag-internal - внутренние узлы, это теги, которые могу иметь детей; tag-leaf - листовые узлы, это теги, которые не могут иметь детей. Итак, для описания нашего дерева html достаточно определить три типа узлов:

  • tag-internal - теги, которые могут иметь детей, внутренний узел
  • tag-leaf - теги, которые не могут иметь детей, листовой узел
  • text - простой текст, листовой узел

Теперь мы можем работать с нашим деревом. Например, отфильтруем все пустые теги. Для этого прежде всего надо определить, как фильтровать каждый тип. Каждый тип фильтруется по-своему:

  • tag-internal - если нет детей или все дети пустые, значит и родитель пустой
  • tag-leaf - не может иметь детей, такой тег всегда выводится
  • text - текстовый узел не может содержать детей, вместо этого он может содержать текстовый контент, поэтому фильтруем по пустому контенту

Функция фильтрации будет выглядеть следующим образом:

function filterEmpty($tree) {
  // Перед фильтрацией отфильтровываем всех потомков
  $internalFiltered = array_map(function($node) {
    if ($node['type'] === 'tag-internal') {
      // Тут самый важный момент. Рекурсивно вызываем функцию фильтрации.
      // Дальнейшая работа не завершится, пока функция фильтрации не отфильтрует вложенные узлы.
      return filterEmpty($node);
    }
    return $node;
  }, $tree['children']);

  $result = array_filter($internalFiltered, function($node) {
    // Каждый тип фильтруется по-своему, удобно для этого использовать switch
    switch ($node['type']) {
      case 'tag-internal':
        // К этому моменту мы уже отфильтровали пустых потомков
        // Если остались не пустые потомки, значит текущий родитель не пустой
        return count($node['children']) > 0;
      case 'tag-leaf':
        // Листовые узлы всегда выводятся
        return true;
      case 'text':
        // Для текстовых узлов просто проверяем существование контента
        return !!$node['content']; // Для однозначности приводим значение к булевому типу
    }
  });

  $tree['children'] = $result;
  return $tree;
};

Фильтр в качестве параметра принимает узел с типом tag-internal и обрабатывает вложенные в него элементы. Вначале проходимся по всем потомкам и у всех, с типом tag-internal, фильтруем также вложенные элементы с помощью нашей же функции (рекурсия). Далее вызывается метод filter(), в нем каждый тип уже фильтруется по той логике, которую мы определили.

После фильтрации получим такое дерево:

[
  'name' => 'html',
  'type' => 'tag-internal',
  'children' => [
    [
      'name' => 'body',
      'type' => 'tag-internal',
      'children' => [
        [
          'name' => 'h1',
          'type' => 'tag-internal',
          'children' => [
            [
              'name' => '',
              'type' => 'text',
              'content' => 'Сообщество',
            ],
          ],
        ],
        [
          'name' => 'p',
          'type' => 'tag-internal',
          'children' => [
            [
              'type' => 'text',
              'content' => 'Общение между пользователями Хекслета',
            ],
          ],
        ],
        [
          'name' => 'hr',
          'type' => 'tag-leaf',
        ],
        [
          'name' => 'input',
          'type' => 'tag-leaf',
        ],
      ],
    ],
  ],
];

Это дерево не содержит элемент div с классом hexlet-community, даже несмотря на то, что оно содержало другие элементы. Это произошло потому, что перед фильтрацией родителя были отфильтрованы его пустые потомки. Теперь можно собрать дерево в строку:

// Для удобства определим отдельную функцию для формирования вывода класса
function buildClass($node) {
  return array_key_exists('className', $node) ? " class={$node['className']}" : '';
}

// Основная функция для сборки страницы
function buildHtml($node) {
  // Каждый тип формируется по-своему, как и в фильтрации используем switch
  switch ($node['type']) {
    case 'tag-internal':
      // Этот тип может иметь детей, формируем вывод детей
      $childrenFormatted = array_map(function($item) {
        return buildHtml($item);
      }, $node['children']);
      $childrenView = implode('', $childrenFormatted);
      // Собираем все, вместе с родительским узлом
      $classView = buildClass($node);
      return "<{$node['name']}{$classView}>{$childrenView}</{$node['name']}>";
    case 'tag-leaf':
      // Листовые узлы формируются просто
      $classView = buildClass($node);
      return "<{$node['name']}{$classView}>";
    case 'text':
      // В текстовых узлах выводится сам контент
      return $node['content'];
  }
};

// Получаем отфильтрованное дерево
$filteredTree = filterEmpty($htmlTree);

// Формируем результат
$html = buildHtml($filteredTree);
print_r($html); // => <html><body><h1>Сообщество</h1><p>Общение между пользователями Хекслета</p><hr><input></body></html>

Если расставить отступы и каждый тег на новой строке, то на выходе получаем html без пустых тегов:

<html>
   <body>
      <h1>Сообщество</h1>
      <p>Общение между пользователями Хекслета</p>
      <hr>
      <input>
   </body>
</html>

https://replit.com/@hexlet/php-trees-html

Код для обработки деревьев выглядит довольно лаконичным. Это - следствие удобной структуры для представления дерева html. Выделив в этом дереве несколько типов узлов, остается только описать логику для каждого типа. Любой внутренний узел является таким же деревом, поэтому обрабатывается рекурсивно этой же функцией. Правильное представление структуры значительно упрощает обработку дерева.


Дополнительные материалы

  1. Знакомство с HTML

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 25 апреля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»