Создание CLI на основе unified

Unified — это универсальный движок обработки синтаксических деревьев (AST) в JavaScript, который позволяет работать с различными форматами текста, включая Markdown и HTML. Remark и Rehype — это плагины для unified: Remark преобразует Markdown в AST, а Rehype работает с HTML. Создание CLI на их основе подразумевает создание цепочек плагинов для анализа, трансформации и генерации текста.

Основная структура CLI строится вокруг следующих компонентов:

  1. Парсер исходного текста — Remark или Rehype.
  2. Плагины-трансформеры — функции, модифицирующие AST.
  3. Генератор конечного результата — возвращает текст в нужном формате.
  4. Интерфейс командной строки — обеспечивает ввод/вывод файлов и передачу опций.

Настройка проекта

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

npm install unified remark-parse remark-stringify rehype rehype-stringify commander
  • unified — ядро обработки AST.
  • remark-parse — парсер Markdown.
  • remark-stringify — генератор Markdown.
  • rehype — парсер и трансформер HTML.
  • rehype-stringify — генератор HTML.
  • commander — удобная библиотека для CLI.

Структура проекта может быть следующей:

my-cli/
├─ src/
│  ├─ cli.js
│  ├─ processor.js
│  └─ plugins/
│     ├─ customRemarkPlugin.js
│     └─ customRehypePlugin.js
├─ package.json
└─ README.md

Создание процессора текста

Процессор — это функция, которая объединяет парсер, плагины и генератор. Пример для Markdown:

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

export function createMarkdownProcessor() {
  return unified()
    .use(remarkParse)           // Преобразование Markdown в AST
    .use(remarkGfm)             // Поддержка расширений GitHub Flavored Markdown
    .use(customRemarkPlugin)    // Пользовательский плагин для трансформации AST
    .use(remarkStringify);      // Преобразование AST обратно в Markdown
}

Принцип работы: сначала текст разбирается на дерево (AST), затем оно проходит через цепочку плагинов, которые модифицируют узлы дерева, после чего дерево сериализуется обратно в Markdown или HTML.

Создание плагинов Remark и Rehype

Плагины — это функции, которые получают AST и могут изменять его узлы. В Remark используется формат unist. Пример простого плагина для Remark:

export function customRemarkPlugin() {
  return (tree) => {
    visit(tree, 'heading', (node) => {
      node.children.unshift({
        type: 'text',
        value: '[TITLE] '
      });
    });
  };
}

Для Rehype структура плагина аналогична, но используется HTML-дерево (hast):

export function customRehypePlugin() {
  return (tree) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'p') {
        node.children.unshift({
          type: 'text',
          value: '[PARAGRAPH] '
        });
      }
    });
  };
}

Функция visit из пакета unist-util-visit позволяет рекурсивно обходить дерево и изменять узлы.

Создание CLI с commander

Командная строка обеспечивает передачу файлов и параметров:

#!/usr/bin/env node
import { program } from 'commander';
import fs from 'fs';
import { createMarkdownProcessor } from './processor.js';

program
  .version('1.0.0')
  .argument('<input>', 'Входной Markdown файл')
  .argument('<output>', 'Выходной Markdown файл')
  .option('-u, --uppercase', 'Преобразовать текст в верхний регистр')
  .action(async (input, output, options) => {
    const text = fs.readFileSync(input, 'utf-8');
    const processor = createMarkdownProcessor();

    let result = await processor.process(text);

    if (options.uppercase) {
      result = result.toString().toUpperCase();
    }

    fs.writeFileSync(output, result);
  });

program.parse();

Особенности реализации:

  • Использование async/await для обработки промисов, так как unified поддерживает асинхронные плагины.
  • Передача опций через .option().
  • Обработка ввода/вывода файлов через fs.

Асинхронные плагины и обработка больших файлов

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

export function asyncRemarkPlugin() {
  return async (tree) => {
    await visit(tree, 'text', async (node) => {
      node.value = await fetchDataForText(node.value);
    });
  };
}

Unified автоматически поддерживает промисы, если плагин возвращает Promise.

Комбинирование Remark и Rehype

Иногда необходимо преобразовать Markdown в HTML. Для этого используют remark-rehype:

import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const processor = unified()
  .use(remarkParse)
  .use(remarkGfm)
  .use(remarkRehype)       // Преобразование AST Markdown в AST HTML
  .use(customRehypePlugin) // Трансформация HTML AST
  .use(rehypeStringify);   // Генерация HTML

Такой подход позволяет создавать полноценные конвейеры: Markdown → AST → трансформация → HTML.

Советы по организации больших CLI-проектов

  • Разделять плагины по папкам plugins/ и подключать их динамически.
  • Хранить конфигурацию в config.js или JSON-файлах.
  • Использовать отдельный модуль для процессора (processor.js) для повторного использования в тестах и других скриптах.
  • Поддерживать асинхронность плагинов для сетевых операций или работы с большими файлами.

Работа с AST

Понимание структуры AST важно для эффективной трансформации. Основные узлы Remark (mdast):

  • root — корневой узел документа.
  • heading — заголовки.
  • paragraph — параграфы.
  • text — текстовые узлы.
  • list, listItem — списки и элементы списка.

Для Rehype (hast) ключевые узлы:

  • root — корневой элемент.
  • element — HTML-теги с tagName и children.
  • text — текст.
  • comment — комментарии.

Знание этих структур позволяет создавать точные трансформации и оптимизировать CLI для любых текстовых данных.