Удаление узлов из дерева

В работе с Markdown и HTML через Remark и Rehype часто возникает необходимость модифицировать дерево синтаксического разбора (AST), удаляя определённые узлы. Операции удаления узлов требуют понимания структуры AST и методов обхода, предоставляемых этими библиотеками.


Структура AST в Remark и Rehype

Remark работает с деревом Markdown, где каждый узел имеет тип type и может содержать массив children. Типичные узлы:

  • root — корневой узел документа.
  • paragraph — абзац.
  • text — текстовое содержимое.
  • heading — заголовок, с уровнем depth.

Rehype работает с HTML-деревом. Узлы имеют типы:

  • root — корень документа.
  • element — HTML-элемент, с полями tagName, properties, children.
  • text — текстовый узел.

Общее для обеих библиотек: для обхода дерева используются функции unist-util-visit или unist-util-visit-parents, а для изменения дерева — unist-util-modify-children и прямое изменение массива children.


Методы удаления узлов

Удаление узлов может быть выполнено несколькими способами:

  1. Прямое фильтрование массива children Наиболее простой способ. Для каждого узла с дочерними элементами:
import { visit } from 'unist-util-visit';

visit(tree, 'paragraph', (node, index, parent) => {
  if (node.children.some(child => child.type === 'text' && child.value.includes('удалить'))) {
    parent.children.splice(index, 1);
  }
});

Объяснение:

  • visit(tree, 'paragraph', callback) обходит все узлы типа paragraph.
  • index — позиция текущего узла в parent.children.
  • splice(index, 1) удаляет узел из дерева.
  1. Использование unist-util-visit-parents для удаления с доступом к родителю
import { visitParents } from 'unist-util-visit-parents';

visitParents(tree, 'heading', (node, ancestors) => {
  if (node.depth === 3) {
    const parent = ancestors[ancestors.length - 1];
    const index = parent.children.indexOf(node);
    if (index !== -1) parent.children.splice(index, 1);
  }
});

Пояснение:

  • visitParents предоставляет полный путь от корня до текущего узла.
  • Это позволяет безопасно удалять узлы даже при вложенных структурах.
  1. Удаление с помощью фильтрации children рекурсивно

Для удаления узлов по сложным условиям удобно написать рекурсивную функцию:

function removeNodes(node, predicate) {
  if (!node.children) return;
  node.children = node.children.filter(child => !predicate(child));
  node.children.forEach(child => removeNodes(child, predicate));
}

removeNodes(tree, node => node.type === 'text' && node.value.match(/удалить/));

Преимущества:

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

Удаление элементов в Rehype

В HTML-документах удаление работает аналогично:

import { visit } from 'unist-util-visit';

visit(tree, 'element', (node, index, parent) => {
  if (node.tagName === 'script' || node.tagName === 'style') {
    parent.children.splice(index, 1);
  }
});

Особенности:

  • tagName указывает на тип HTML-элемента.
  • Можно комбинировать условия: класс, атрибут id, наличие дочерних текстов.

Важные рекомендации

  • Всегда проверять наличие parent перед удалением узла. Корневой узел root не имеет родителя.
  • Использовать обратный обход (от конца массива к началу), если удаляются несколько узлов одновременно, чтобы избежать смещения индексов.
  • Для больших деревьев эффективнее использовать visit с фильтром по типу, а не обходить все узлы без разбора.

Примеры условного удаления

  1. Удаление всех заголовков h2 и h3:
visit(tree, 'heading', (node, index, parent) => {
  if (node.depth === 2 || node.depth === 3) {
    parent.children.splice(index, 1);
  }
});
  1. Удаление всех пустых параграфов:
visit(tree, 'paragraph', (node, index, parent) => {
  if (!node.children || node.children.length === 0) {
    parent.children.splice(index, 1);
  }
});
  1. Удаление всех элементов с определённым классом в Rehype:
visit(tree, 'element', (node, index, parent) => {
  if (node.properties && node.properties.className?.includes('remove-me')) {
    parent.children.splice(index, 1);
  }
});

Итоговая схема работы

  1. Выбирается тип узлов для удаления (visit или visitParents).
  2. Определяется условие фильтрации.
  3. Используется доступ к родителю для безопасного удаления.
  4. В случае сложных деревьев рекомендуется рекурсивная фильтрация.

Удаление узлов из AST Remark и Rehype является мощным инструментом для трансформации Markdown и HTML. Грамотное использование обхода и фильтрации позволяет модифицировать структуру документа без нарушения целостности дерева.