Библиотека Marked в JavaScript предоставляет мощный движок для разбора Markdown в HTML с возможностью гибкой настройки через расширения. Однако при работе с несколькими расширениями одновременно могут возникать конфликты, влияющие на корректность парсинга и рендеринга. Понимание механизмов работы расширений и стратегий их интеграции критически важно для построения надежных решений.
Расширения в Marked реализуются через объекты, которые содержат функции-парсеры и рендереры для различных токенов Markdown. Основные свойства расширения:
block (блочные элементы) или inline
(встроенные элементы).Пример базового расширения:
import { marked } from 'marked';
const myExtension = {
name: 'highlight',
level: 'inline',
tokenizer(src) {
const match = src.match(/==(.+?)==/);
if (match) {
return {
type: 'highlight',
raw: match[0],
text: match[1]
};
}
},
renderer(token) {
return `<mark>${token.text}</mark>`;
}
};
marked.use({ extensions: [myExtension] });
Конфликты возникают, когда два или более расширения обрабатывают одинаковые сегменты текста или один и тот же уровень Markdown. Основные типы:
Перекрытие токенов Когда несколько токенизаторов
могут совпадать по шаблону. Например, расширение для выделения текста
жирным (**text**) и кастомное расширение для выделения
текста определенным тегом <b> могут одновременно
срабатывать на одном фрагменте.
Порядок применения расширений Marked
обрабатывает расширения в том порядке, в котором они переданы через
marked.use(). Ошибка в порядке может привести к
игнорированию или некорректной генерации HTML.
Неправильная обработка уровней Расширение,
назначенное как inline, может непреднамеренно захватывать
блоки текста, что нарушает структуру Markdown.
Дублирующиеся имена токенов Использование
одинаковых имен type в разных расширениях может вызвать
конфликт в рендеринге.
type. Это предотвращает путаницу
при генерации HTML.const boldExtension = {
name: 'customBold',
level: 'inline',
tokenizer(src) {
const match = src.match(/\*\*(.+?)\*\*/);
if (match) {
return { type: 'customBold', raw: match[0], text: match[1] };
}
},
renderer(token) {
return `<b>${token.text}</b>`;
}
};
start позволяет Marked быстрее определять начало токена и
снижает риск перекрытия с другими расширениями.const highlightExtension = {
name: 'highlight',
level: 'inline',
start(src) { return src.indexOf('=='); },
tokenizer(src) {
const match = src.match(/==(.+?)==/);
if (match) {
return { type: 'highlight', raw: match[0], text: match[1] };
}
},
renderer(token) { return `<mark>${token.text}</mark>`; }
};
Сегрегация уровней Разделение блоковых и встроенных токенов предотвращает непреднамеренное смешивание правил.
Тестирование взаимодействий При добавлении нового расширения важно проверять, как оно работает с уже подключенными. Особенно это важно для сложных документов с nested-разметкой.
Последовательная обработка Установить строгий порядок подключения расширений, начиная с базовых Markdown-токенов, затем расширений для специальных случаев.
Композиция токенов Объединение нескольких токенов в один логический токен, если они часто перекрываются.
Использование fallback Если расширение не
распознает токен, возвращать null и позволять следующему
расширению обрабатывать текст.
tokenizer(src) {
const match = src.match(/custom-pattern/);
if (!match) return null; // fallback
return { type: 'custom', raw: match[0], text: match[1] };
}
Представим расширение для подчёркивания текста и расширение для подсветки:
const underlineExtension = {
name: 'underline',
level: 'inline',
tokenizer(src) {
const match = src.match(/__(.+?)__/);
if (match) return { type: 'underline', raw: match[0], text: match[1] };
},
renderer(token) { return `<u>${token.text}</u>`; }
};
const highlightExtension = {
name: 'highlight',
level: 'inline',
tokenizer(src) {
const match = src.match(/==(.+?)==/);
if (match) return { type: 'highlight', raw: match[0], text: match[1] };
},
renderer(token) { return `<mark>${token.text}</mark>`; }
};
marked.use({ extensions: [underlineExtension, highlightExtension] });
Если текст содержит пересечение __==text==__, то порядок
расширений определяет, что будет обработано первым. В таких случаях
рекомендуется использовать nested-токены, где один
токен может содержать внутренние токены:
tokenizer(src) {
const match = src.match(/__(.+?)__/);
if (match) {
const inner = match[1];
const innerTokens = marked.lexer(inner); // вложенный разбор
return { type: 'underline', raw: match[0], tokens: innerTokens };
}
}
Это позволяет корректно совмещать несколько расширений без потери семантики.
start и fallback для управления
приоритетами.Эти подходы обеспечивают стабильное функционирование Marked при работе с несколькими расширениями и позволяют создавать сложные кастомные решения без потери совместимости.