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

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

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

const 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 - текстовый узел не может содержать детей, вместо этого он может содержать текстовый контент, поэтому фильтруем по пустому контенту

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

const filterEmpty = (tree) => {
  const filtered = tree.children
    .map((node) => {
      // Перед фильтрацией отфильтровываем всех потомков
      if (node.type === 'tag-internal') {
        // Тут самый важный момент. Рекурсивно вызываем функцию фильтрации.
        // Дальнейшая работа не завершится, пока функция фильтрации не отфильтрует вложенные узлы.
        return filterEmpty(node);
      }
      return node;
    })
    .filter((node) => {
      const { type } = node;
      // Каждый тип фильтруется по-своему, удобно для этого использовать switch
      switch (type) {
        case 'tag-internal': {
          // К этому моменту мы уже отфильтровали пустых потомков
          const { children } = node;
          // Если остались не пустые потомки, значит текущий родитель не пустой
          return children.length > 0;
        }
        case 'tag-leaf':
          // Листовые узлы всегда выводятся
          return true;
        case 'text': {
          const { content } = node;
          // Для текстовых узлов просто проверяем существование контента,
          return !!content; // Для однозначности приводим значение к булевому типу
        }
      }
    });
  return { ...tree, children: filtered };
};

Фильтр в качестве параметра принимает узел с типом 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: [
            {
              name: '',
              type: 'text',
              content: 'Общение между пользователями Хекслета',
            },
          ],
        },
        {
          name: 'hr',
          type: 'tag-leaf',
        },
        {
          name: 'input',
          type: 'tag-leaf',
        },
      ],
    },
  ],
};

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

// Для удобства определим отдельную функцию для формирования вывода класса
const buildClass = (node) => node.className ? ` class=${node.className}` : '';

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

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

// Формируем результат
const html = buildHtml(filteredTree);
console.log(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/js-trees-html

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


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

  1. Знакомство с HTML
  2. Для чего два восклицательных знака перед выражением?

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

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

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
7 июля 10 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
7 июля 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
Новый
Разработка фронтенд и бэкенд компонентов веб-приложений
7 июля 16 месяцев

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

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

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

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»