Навигация по дереву токенов

Markdown-it — это мощная библиотека для парсинга Markdown в JavaScript, которая преобразует исходный текст в структуру токенов. Токены представляют собой единицы синтаксического дерева, каждую из которых можно рассматривать как отдельный элемент Markdown-документа: заголовок, абзац, список, ссылку или inline-контент. Для эффективной работы с Markdown на низком уровне необходимо понимать структуру токенов и уметь навигировать по дереву.


Структура токена

Каждый токен в Markdown-it имеет несколько ключевых свойств:

  • type — тип токена, например paragraph_open, inline, heading_close. Тип определяет синтаксическую роль токена.
  • tag — HTML-тег, соответствующий токену, например p, h1, ul.
  • attrs — массив атрибутов [ключ, значение], применяемых к тегу. Может быть null, если атрибутов нет.
  • map — массив [startLine, endLine], указывающий на строки исходного Markdown, которые соответствуют токену.
  • nesting — значение 1 для открывающего тега, -1 для закрывающего, 0 для самозакрывающегося.
  • level — уровень вложенности токена, помогает понимать иерархию.
  • children — массив дочерних токенов для inline-элементов, таких как текст, ссылки или эмодзи.

Пример типичного токена:

{
  type: 'paragraph_open',
  tag: 'p',
  attrs: null,
  map: [0, 1],
  nesting: 1,
  level: 0,
  children: null
}

Получение токенов

Токены формируются при парсинге Markdown с помощью метода parse объекта Markdown-it:

const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();

const tokens = md.parse('# Заголовок\n\nТекст абзаца', {});
console.log(tokens);

Метод parse возвращает массив токенов верхнего уровня. Каждый элемент массива — отдельный токен с возможными дочерними токенами в свойстве children.


Уровни вложенности и навигация

Уровень вложенности (level) используется для построения дерева токенов и навигации по нему.

  • level 0 — токены верхнего уровня, обычно блоки документа.
  • level 1 и выше — вложенные токены, например inline-текст внутри абзаца или ссылки внутри текста.

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

function traverseTokens(tokens, callback) {
  tokens.forEach(token => {
    callback(token);
    if (token.children) {
      traverseTokens(token.children, callback);
    }
  });
}

// Использование
traverseTokens(tokens, token => {
  console.log('Тип токена:', token.type, 'Уровень:', token.level);
});

Связь открывающих и закрывающих токенов

Markdown-it использует парные токены для блоков с началом и концом: *_open и *_close.

Пример:

# Заголовок

При разборе формируются два токена:

  1. heading_open с nesting: 1
  2. inline с текстом заголовка
  3. heading_close с nesting: -1

Соответствие открывающих и закрывающих токенов удобно отслеживать по уровню вложенности и порядку в массиве токенов.


Атрибуты и HTML-параметры

Токены могут содержать атрибуты, определяющие HTML-свойства:

{
  type: 'link_open',
  tag: 'a',
  attrs: [['href', 'https://example.com']],
  nesting: 1
}

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

function getAttr(token, name) {
  if (!token.attrs) return null;
  const attr = token.attrs.find(([key]) => key === name);
  return attr ? attr[1] : null;
}

const href = getAttr(tokens[1], 'href');

Inline-токены

Inline-токены (inline) содержат текст и вложенные элементы, такие как эмодзи, ссылки или выделение. Их структура:

{
  type: 'inline',
  tag: '',
  children: [
    { type: 'text', content: 'Пример текста', level: 1 },
    { type: 'strong_open', level: 1 },
    { type: 'text', content: 'жирный текст', level: 2 },
    { type: 'strong_close', level: 1 }
  ]
}

Обход children рекурсивно позволяет детально анализировать содержимое блоков.


Примеры навигации по дереву токенов

  1. Поиск всех заголовков документа:
const headings = [];
traverseTokens(tokens, token => {
  if (token.type.endsWith('_open') && token.tag.startsWith('h')) {
    headings.push(token);
  }
});
  1. Сбор всех ссылок:
const links = [];
traverseTokens(tokens, token => {
  if (token.type === 'link_open') {
    links.push(getAttr(token, 'href'));
  }
});
  1. Извлечение текста абзацев:
function extractText(tokens) {
  let result = '';
  traverseTokens(tokens, token => {
    if (token.type === 'text') {
      result += token.content;
    }
  });
  return result;
}

Практические советы

  • Всегда проверять наличие children перед рекурсией.
  • Использовать nesting и level для идентификации парных токенов.
  • Inline-токены требуют рекурсивного обхода для анализа текста и форматирования.
  • Токены верхнего уровня (level 0) обычно соответствуют блочным элементам документа.

Эта структура позволяет строить собственные плагины, расширять функционал Markdown-it и создавать инструменты для анализа и трансформации Markdown-документов на низком уровне.