Множественные расширения

Библиотека Marked в JavaScript предоставляет мощный механизм для обработки Markdown, позволяя подключать и комбинировать различные расширения. Каждое расширение может изменять поведение парсера, добавлять новые синтаксические конструкции или модифицировать существующие. Для начала работы с множественными расширениями необходимо понимать структуру объекта конфигурации, который принимает marked.use().

import { marked } from 'marked';
import myExtension from './myExtension.js';
import anotherExtension from './anotherExtension.js';

marked.use([myExtension, anotherExtension]);

В этом примере массив расширений позволяет подключать несколько модулей одновременно. Порядок подключения влияет на последовательность применения правил: первый элемент массива обрабатывается раньше остальных.

Типы расширений и их функции

Расширения можно классифицировать по типу воздействия на Markdown:

  1. Лексические расширения (lexer) – изменяют или добавляют токены при разборе текста. Пример: поддержка кастомных блоков или специальных синтаксических конструкций.

  2. Парсерные расширения (parser) – преобразуют токены в HTML. Используются для генерации специфической разметки, например, для таблиц нестандартного формата или подсветки кода.

  3. Рендереры (renderer) – переопределяют стандартные методы рендеринга. Это позволяет полностью контролировать конечный HTML, например, оборачивать все заголовки в кастомные элементы или добавлять атрибуты к ссылкам.

Создание собственного расширения

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

const customExtension = {
  extensions: [
    {
      name: 'highlightBlocks',
      level: 'block',
      start(src) { return src.match(/```highlight/)?.index; },
      tokenizer(src) {
        const match = /^```highlight\n([\s\S]+?)\n```/.exec(src);
        if (match) {
          return { type: 'highlight', raw: match[0], text: match[1] };
        }
      },
      renderer(token) {
        return `<div class="highlighted">${token.text}</div>`;
      }
    }
  ]
};

marked.use(customExtension);

В данном примере создается блок highlight, который позволяет оборачивать текст в <div> с классом highlighted. Каждый блок расширения имеет уникальное имя и поддерживает методы tokenizer и renderer.

Взаимодействие нескольких расширений

При подключении нескольких расширений важно учитывать, что они могут влиять друг на друга. Существует несколько стратегий:

  • Последовательная обработка: порядок подключения определяет, какое расширение будет обрабатывать текст первым.
  • Фильтрация токенов: одно расширение может модифицировать токены, а следующее — использовать их для генерации HTML.
  • Переопределение методов: расширение может полностью заменять метод рендеринга, но это может влиять на работу других расширений.

Пример последовательного применения:

marked.use([
  {
    renderer: {
      link(href, title, text) {
        return `<a href="${href}" target="_blank">${text}</a>`;
      }
    }
  },
  {
    extensions: [
      {
        name: 'spoiler',
        level: 'inline',
        start(src) { return src.indexOf('!!') },
        tokenizer(src) {
          const match = /^!!(.+?)!!/.exec(src);
          if (match) return { type: 'spoiler', raw: match[0], text: match[1] };
        },
        renderer(token) {
          return `<span class="spoiler">${token.text}</span>`;
        }
      }
    ]
  }
]);

В этом примере сначала переопределяется рендеринг ссылок, затем добавляется обработка спойлеров. При такой конфигурации спойлеры корректно обрабатываются даже внутри ссылок.

Конфликты и их решение

Основные проблемы при работе с множественными расширениями:

  • Дублирование имен токенов: два расширения не должны использовать одинаковое имя токена.
  • Несовместимые методы рендеринга: если два расширения пытаются переопределить один и тот же метод рендерера, применяется последнее подключенное.
  • Порядок обработки: блоковые и встроенные расширения должны быть подключены с учетом их уровня (block или inline), иначе возможны ошибки при токенизации.

Решение конфликтов:

  1. Всегда использовать уникальные имена для токенов.
  2. Разделять рендереры и токенизаторы в разных расширениях при сложной логике.
  3. Тестировать комбинации расширений на реальных примерах Markdown.

Динамическое подключение и отключение расширений

Marked поддерживает возможность добавления и удаления расширений на лету:

// Добавление
marked.use(dynamicExtension);

// Отключение
marked.use({ extensions: [] });

Это позволяет создавать адаптивные редакторы, где набор расширений меняется в зависимости от контекста, например, для режима предпросмотра или режима редактирования.

Практические рекомендации

  • Использовать массивы расширений для управления сложными конфигурациями.
  • Разделять блоковые и встроенные расширения для предотвращения ошибок токенизации.
  • Всегда проверять совместимость методов рендеринга при подключении нескольких расширений.
  • Подключать расширения в порядке от более специфичных к более общим, чтобы избежать перезаписи токенов.

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