Таблицы: парсинг и рендеринг

Структура таблиц в Markdown

Markdown поддерживает создание таблиц в упрощённом виде с использованием символов | и -. Стандартная структура таблицы включает заголовочную строку, разделитель и строки данных:

| Заголовок 1 | Заголовок 2 | Заголовок 3 |
|------------|------------|------------|
| Данные 1   | Данные 2   | Данные 3   |
| Данные 4   | Данные 5   | Данные 6   |
  • Заголовок определяется первой строкой таблицы.
  • Разделитель |---|---|---| указывает конец заголовка и начало тела таблицы.
  • Тело таблицы может содержать любое количество строк.

Markdown позволяет выравнивание столбцов с помощью двоеточий:

| Левый | Центр | Правый |
|:-----|:----:|------:|
| A    | B    | C    |
  • :--- — выравнивание по левому краю
  • :---: — центрирование
  • ---: — выравнивание по правому краю

Парсинг таблиц с помощью Remark

Remark использует систему плагинов для анализа Markdown. Для работы с таблицами важны узлы типа table, tableRow и tableCell.

Структура AST для таблицы:

{
  "type": "table",
  "align": ["left", "center", "right"],
  "children": [
    {
      "type": "tableRow",
      "children": [
        { "type": "tableCell", "children": [{ "type": "text", "value": "Левый" }] },
        { "type": "tableCell", "children": [{ "type": "text", "value": "Центр" }] },
        { "type": "tableCell", "children": [{ "type": "text", "value": "Правый" }] }
      ]
    }
  ]
}
  • type: "table" — корневой узел таблицы
  • align — массив выравнивания столбцов
  • children — массив строк таблицы (tableRow)
  • tableRow.children — массив ячеек (tableCell)
  • tableCell.children — содержимое ячейки, обычно текст или встроенные элементы Markdown

Для парсинга таблиц используется плагин remark-parse, который по умолчанию поддерживает синтаксис GitHub-flavored Markdown (GFM) и позволяет автоматически распознавать таблицы.

Пример кода на JavaScript с использованием Remark для получения AST таблицы:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';

const markdown = `
| Имя | Возраст | Город |
|-----|:------:|------:|
| Анна | 25 | Москва |
| Борис | 30 | Санкт-Петербург |
`;

const processor = unified()
  .use(remarkParse)
  .use(remarkGfm);

const ast = processor.parse(markdown);
console.log(JSON.stringify(ast, null, 2));

Результат содержит узлы table, tableRow и tableCell, что позволяет работать с таблицей программно.

Рендеринг таблиц с помощью Rehype

После парсинга Markdown в AST через Remark часто возникает задача преобразовать его в HTML. Для этого используется Rehype, который работает с HTML-деревом (hast).

Основные шаги преобразования таблицы:

  1. Преобразовать Markdown AST (mdast) в HTML AST (hast) с помощью remark-rehype.
  2. Рендерить hast в HTML через rehype-stringify.

Пример:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const markdown = `
| Имя | Возраст | Город |
|-----|:------:|------:|
| Анна | 25 | Москва |
| Борис | 30 | Санкт-Петербург |
`;

const html = await unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkRehype)
  .use(rehypeStringify)
  .process(markdown);

console.log(String(html));

Результат:

<table>
  <thead>
    <tr>
      <th>Имя</th>
      <th style="text-align:center">Возраст</th>
      <th style="text-align:right">Город</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Анна</td>
      <td style="text-align:center">25</td>
      <td style="text-align:right">Москва</td>
    </tr>
    <tr>
      <td>Борис</td>
      <td style="text-align:center">30</td>
      <td style="text-align:right">Санкт-Петербург</td>
    </tr>
  </tbody>
</table>

Кастомизация рендеринга таблиц

С помощью Rehype можно изменить структуру или оформление таблицы:

  • Добавление классов CSS:
import rehypeAddClasses from 'rehype-add-classes';

const html = await unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkRehype)
  .use(rehypeAddClasses, {
    'table': 'table table-striped',
    'th': 'table-header',
    'td': 'table-cell'
  })
  .use(rehypeStringify)
  .process(markdown);
  • Изменение содержимого ячеек: можно создать плагин Rehype, который обходит узлы tableCell и изменяет текст или добавляет вложенные элементы.

Валидация и обработка нестандартных таблиц

Не все Markdown-таблицы строго соответствуют стандарту. Remark поддерживает гибкий парсинг, но:

  • Таблицы без разделителя |---| будут проигнорированы.
  • Несоответствие количества ячеек в строках вызывает смещение при рендеринге HTML.
  • Для расширенной валидации можно обойти AST и проверить, что все строки имеют одинаковое количество ячеек:
function validateTable(ast) {
  const tables = ast.children.filter(node => node.type === 'table');
  tables.forEach(table => {
    const columnCount = table.children[0].children.length;
    table.children.forEach(row => {
      if (row.children.length !== columnCount) {
        console.warn('Несоответствие числа ячеек в строке');
      }
    });
  });
}

Интеграция с другими плагинами

Таблицы могут комбинироваться с другими расширениями:

  • Syntax highlighting внутри ячеек: использование remark-prism для подсветки кода.
  • Встроенные элементы: изображения, ссылки, форматированный текст внутри ячеек поддерживаются благодаря AST tableCell.children.
  • Экспорт в PDF или React-компоненты: через rehype-react или remark-pdf таблицы могут отображаться в веб-приложениях или документации.

Оптимизация больших таблиц

Для больших наборов данных рекомендуется:

  • Использовать виртуальный рендеринг в React (react-virtualized) при конвертации в компоненты.
  • Минимизировать преобразования AST, выполняя парсинг и рендеринг в один конвейер unified.
  • Кэшировать результаты парсинга Markdown, если контент статичен.

Таблицы в Remark/Rehype предоставляют мощный механизм для анализа, трансформации и рендеринга структурированных данных, позволяя гибко контролировать как Markdown, так и конечный HTML.