hub.oscript.io
hub.oscript.io
Войти

oscript-mdОпубликовано доверенным конвейеромпакет

Markdown-парсер и рендерер на чистом OneScript: CommonMark, GFM-расширения и безопасный рендер недоверенного Markdown.

автор: Egor Ivanovскачиваний: 61репозиторий: github.com/segate-ekb/oscript-md

Пакет лежит в основном пуле хаба, а opm знает этот хаб по умолчанию: короткой формы достаточно.

opm install oscript-md

Скачанный файл ставится командой opm install -f <файл> — сеть для этого уже не нужна.

Описание

oscript-md

Markdown-парсер и рендерер на чистом OneScript. Без зависимостей от Markdig или другой нативной библиотеки — реализация работает в любой среде, где есть OneScript.

Поддерживается ядро CommonMark 0.31.2 и набор расширений GitHub-Flavored Markdown (tables, tasklists, strikethrough, autolinks). Недоверенный Markdown рендерится сразу в безопасный HTML — Markdown.ВБезопасныйHTML, см. «Недоверенный Markdown».

Установка

Из хаба opm:

opm install oscript-md

Единственная зависимость времени выполнения — html-sanitizer: её зовёт Markdown.ВБезопасныйHTML. Разбор и рендеринг Markdown сторонних библиотек не требуют.

Из исходников (например, склонированный репозиторий):

opm install --local
opm build .

После установки библиотека доступна в любом скрипте через стандартное:

#Использовать oscript-md

Быстрый старт

#Использовать oscript-md

HTML = Markdown.ВHTML("# Привет, мир!");
// → "<h1>Привет, мир!</h1>" + Символы.ПС

Функции фасада Markdown.*:

Функция Что возвращает Когда полезна
Markdown.ВHTML(текст[, настройки]) HTML-строку Рендеринг своего Markdown в веб/email/preview
Markdown.ВБезопасныйHTML(текст[, настройки[, allowlist]]) санитизированный HTML Рендеринг чужого Markdown: README, комментарии, ввод пользователя
Markdown.Разобрать(текст[, настройки]) MarkdownДокумент (AST) Программная обработка содержимого
Markdown.ВJSON(текст[, настройки]) JSON-сериализация AST Snapshot-тесты, интеграции, отладка
Markdown.ВТекст(текст[, настройки]) plain text без разметки Telegram, email, логи, changelog

Примеры использования

HTML-рендеринг

#Использовать oscript-md

Исходник =
    "# Заголовок"   + Символы.ПС +
    ""              + Символы.ПС +
    "Параграф с **жирным** и *курсивом*." + Символы.ПС +
    ""              + Символы.ПС +
    "- пункт списка" + Символы.ПС +
    "- ещё пункт";

Сообщить(Markdown.ВHTML(Исходник));
// <h1>Заголовок</h1>
// <p>Параграф с <strong>жирным</strong> и <em>курсивом</em>.</p>
// <ul>
// <li>пункт списка</li>
// <li>ещё пункт</li>
// </ul>

Работа с AST

Документ = Markdown.Разобрать("# Привет" + Символы.ПС + Символы.ПС + "Параграф");

Сообщить(Документ.Дети.Количество());        // 2
Сообщить(Документ.Дети[0].ТипУзла());        // heading
Сообщить(Документ.Дети[0].Уровень);          // 1
Сообщить(Документ.Дети[1].ТипУзла());        // paragraph

AST-узлы доступны напрямую — можно собирать документы программно:

Документ = Новый MarkdownДокумент();
Заголовок = Новый MarkdownЗаголовок(2);
Заголовок.Добавить(Новый MarkdownТекст("Сгенерировано"));
Документ.Добавить(Заголовок);

Рендерер = Новый MarkdownHTMLРендерер();
HTML = Рендерер.Отрендерить(Документ);       // <h2>Сгенерировано</h2>

JSON-сериализация AST

JSON = Markdown.ВJSON("# Hi");
// {"type":"document","children":[
//   {"type":"heading","level":1,"children":[
//     {"type":"text","value":"Hi"}
//   ]}
// ]}

Plain text

Текст = Markdown.ВТекст("# Заголовок" + Символы.ПС + Символы.ПС + "Параграф *с курсивом*");
// "Заголовок\n\nПараграф с курсивом"

Пресеты настроек

// Чистый CommonMark без расширений.
Настройки = Markdown.НастройкиCommonMark();

// CommonMark + tables + tasklists + strikethrough + autolinks.
Настройки = Markdown.НастройкиGitHubLike();

// CommonMark + по умолчанию ничего, но безопасный режим включён.
Настройки = Markdown.НастройкиПоУмолчанию();

HTML = Markdown.ВHTML(Текст, Настройки);

Доступные пресеты: commonmark (он же minimal), default, github-like, documentation, zero.

Включение/отключение отдельных расширений

Настройки = Новый MarkdownНастройки("commonmark");
Настройки.Расширения.Включить("tables");
Настройки.Расширения.Включить("strikethrough");

HTML = Markdown.ВHTML(Текст, Настройки);

GFM-таблицы

МД =
    "| Колонка 1 | Колонка 2 |" + Символы.ПС +
    "|-----------|:---------:|" + Символы.ПС +
    "| A         | B         |" + Символы.ПС +
    "| C         | D         |";

HTML = Markdown.ВHTML(МД, Markdown.НастройкиGitHubLike());
// <table>
//   <thead><tr><th>Колонка 1</th><th align="center">Колонка 2</th></tr></thead>
//   <tbody>
//     <tr><td>A</td><td align="center">B</td></tr>
//     <tr><td>C</td><td align="center">D</td></tr>
//   </tbody>
// </table>

Списки задач (tasklists)

МД =
    "- [x] выполнено" + Символы.ПС +
    "- [ ] в работе";

HTML = Markdown.ВHTML(МД, Markdown.НастройкиGitHubLike());
// <ul>
//   <li><input type="checkbox" checked disabled /> выполнено</li>
//   <li><input type="checkbox" disabled /> в работе</li>
// </ul>

Безопасный режим

По умолчанию БезопасныйРежим = Истина: raw HTML экранируется, опасные конструкции не пройдут в выход.

HTML = Markdown.ВHTML("<script>alert(1)</script>");
// "&lt;script&gt;alert(1)&lt;/script&gt;"

Чтобы разрешить raw HTML (например, для доверенного содержимого):

Настройки = Markdown.НастройкиПоУмолчанию();
Настройки.БезопасныйРежим = Ложь;
Настройки.РазрешитьRawHTML = Истина;

Недоверенный Markdown

Безопасный режим экранирует сырой HTML из исходника, но не проверяет то, что собирает сам рендерер: ссылку [зло](javascript:alert(1)) он честно превратит в <a href="javascript:alert(1)">. Поэтому для текста, который написал не вы — README чужого пакета, комментарий, поле формы — нужен второй рубеж: санитизация готового HTML по принципу allowlist.

Её выполняет библиотека html-sanitizer — свой санитайзер oscript-md не пишет. Фасад лишь склеивает рендер и очистку:

#Использовать oscript-md

HTML = Markdown.ВБезопасныйHTML(ТекстREADME);       // рендер + очистка
HTML = Markdown.ОчиститьHTML(НедоверенныйHTML);      // очистка готового HTML
Функция Назначение
Markdown.ВБезопасныйHTML(текст[, настройки[, allowlist]]) разобрать Markdown, отрендерить и санитизировать
Markdown.ОчиститьHTML(HTML[, allowlist]) санитизировать готовый HTML
Markdown.НастройкиСанитизацииПоУмолчанию() профиль allowlist под вывод рендерера (новый объект на каждый вызов)

Что остаётся в выходе по умолчанию: абзацы, заголовки, списки, strong/em, code/pre с классом language-*, цитаты, таблицы с align, details/summary, a с href/title и img с src/alt/title. Схемы URL — http, https, mailto плюс относительные адреса; внешние ссылки получают rel="nofollow noopener". Профиль покрывает и вывод GFM-расширений: del зачёркивания и input с type, checked, disabled — чекбокс списка задач. Полное описание профиля, гарантий и ограничений (гомоглифы в доменах, data:-картинки) — в README html-sanitizer.

Свой allowlist собирается средствами html-sanitizer и передаётся третьим параметром; за основу удобно брать профиль библиотеки — тогда GFM останется на месте:

Настройки = Markdown.НастройкиСанитизацииПоУмолчанию()
    .ЗапретитьТег("img")
    .РазрешитьПротокол("ftp");

HTML = Markdown.ВБезопасныйHTML(Текст, Markdown.НастройкиGitHubLike(), Настройки);

Свои расширения

Расширения регистрируют свои компоненты в MarkdownРеестрКомпонентов через метод Зарегистрировать(Реестр). Полная схема — в исходниках расширений src/extensions/Классы/ (tables / tasklists / strikethrough / autolinks).

Подсветка кода BSL в браузере

Рендер печатает блок ```bsl как <pre><code class="language-bsl">…</code></pre>, и Markdown.ВБезопасныйHTML этот класс сохраняет. Раскрашивает такие блоки в браузере shiki, а тег, который подключает его к странице, отдаёт MarkdownПодсветка.ТегСкрипта():

#Использовать oscript-md

Страница = Markdown.ВБезопасныйHTML(ТекстREADME) + MarkdownПодсветка.ТегСкрипта();

Тег — <script type="module">: он загружает shiki с esm.sh, как в документации shiki, и заменяет блоки ```bsl и ```1c (код 1С), ```sdbl и ```1c-query (язык запросов) разметкой shiki с цветами темы github-light. Запрос в строке кода 1С раскрашивается тоже, блоки других языков тег не трогает. Без доступа к esm.sh — в закрытой сети или под Content-Security-Policy, которая этот адрес не разрешает, — код остаётся обычным текстом.

Версию shiki в адресе поднимает Renovate по правилу из renovate.json. Каждое обновление проверяет тест tests/browser в CI: он открывает страницу с тегом в Chrome без окна и смотрит, что код раскрашен.

Реализованные возможности

Блочный уровень (CommonMark)

Возможность Статус Примечание
ATX-заголовки (#, ##, …) ✅
Setext-заголовки (===, --- под текстом) ✅
Параграфы ✅
Тематические линии (---, ***, ___) ✅
Indented code blocks (4 пробела) ✅
Fenced code blocks (```, ~~~) ✅ С info-string и language-* классом
HTML-блоки ✅
Блок-цитаты (>) ✅
Маркированные списки (-, *, +) ✅
Нумерованные списки ✅
Вложенные списки ✅
Reference link definitions ✅

Inline-уровень (CommonMark)

Возможность Статус Примечание
Жирный (**, __) ✅
Курсив (*, _) ✅
Inline code (`) ✅
Ссылки [text](url "title") ✅
Reference-ссылки [text][ref] ✅
Изображения ![alt](url) ✅
Автоссылки <https://…>, <mail@…> ✅
Raw inline HTML ✅ Экранируется в безопасном режиме
Backslash escapes (\*, \[, …) ✅
HTML-сущности (&amp;, &#42;, &#x2A;) ✅
Hard line break (\ или 2+ пробела + \n) ✅
Soft line break ✅

Расширения (GFM)

Расширение Статус Имя в MarkdownНаборРасширений
Таблицы (pipe tables, alignment :---:) ✅ tables
Списки задач (- [ ], - [x]) ✅ tasklists
Зачёркивание (~~текст~~) ✅ strikethrough
Расширенные автоссылки (www., https:// без <>) ✅ autolinks

Рендереры

Рендерер Назначение
MarkdownHTMLРендерер CommonMark-совместимый HTML
MarkdownJSONРендерер JSON-сериализация AST
MarkdownТекстовыйРендерер Plain text без разметки

Безопасность и инфраструктура

Возможность Статус Примечание
Экранирование HTML в тексте ✅
Безопасный режим (raw HTML → escape) ✅ По умолчанию включён
URL %-encoding по CommonMark ✅
Санитизация недоверенного вывода ✅ Markdown.ВБезопасныйHTML поверх html-sanitizer
Санитайзер опасных URL (javascript: и т. п.) ✅ Allowlist схем в html-sanitizer; сам рендерер по-прежнему только %-encoding
Подсветка кода BSL в браузере ✅ shiki с CDN, тег — MarkdownПодсветка.ТегСкрипта(), см. раздел
Heading id / якоря ⏳ Пресет documentation зарезервирован
Frontmatter (YAML/TOML) ⏳ Пресет documentation зарезервирован
TOC ⏳ Пресет documentation зарезервирован
Markdown normalizer ⏳

CommonMark 0.31.2 conformance: 646 / 652 примеров проходят, 6 помечены как xfail (см. раздел «Известные ограничения CommonMark» ниже).

Известные ограничения CommonMark

Шесть примеров официальной CommonMark 0.31.2 conformance suite сейчас помечены &Выключен и не падают CI. Все случаи касаются тонкостей container-модели (CM §4.7) и относятся к одной из трёх групп.

Табуляция в контейнерных блоках (CM §2.2)

Расширение табов на границе контейнера (список, цитата) рассчитано не до конца — нужна полноценная обработка префикса строки контейнера на смешанных tab/space отступах.

# Что не работает
5 Tab внутри элемента списка — расширение таба на границе контейнера
6 Tab внутри цитаты — расширение через границу маркера >
7 Tab внутри элемента списка — расчёт отступа при двойном табе

Setext-заголовок в lazy continuation

# Что не работает
93 Setext-подчёркивание в строке ленивого продолжения цитаты. CM запрещает создавать setext-заголовок из lazy continuation line, у нас он создаётся

Закрытие/границы блоков внутри цитат

# Что не работает
236 Цитата + indented code block: CM считает внешнее indented-содержимое отдельным блоком, наш парсер цитат захватывает его через lazy continuation
237 Fenced code внутри цитаты без закрывающего ограждения: закрывающий fence тоже должен нести префикс >; без него ограждение остаётся открытым до конца цитаты

Скрипт-генератор тестов хранит этот список внутри tools/sync-commonmark.py (константа SKIP_LIST). При исправлении соответствующего бага в парсере нужно убрать номер из константы и перегенерировать сьюты.

Тестирование

tasks/*.os дёргают oneunit как CLI, поэтому он должен быть в PATH — ставим его глобально. asserts и 1commands подтянутся как локальные dev-зависимости из packagedef, html-sanitizer — как обычная зависимость. Из ./oscript_modules их берёт oneunit, запущенный из корня репозитория: туда указывает oscript.cfg в корне (lib.additional=./oscript_modules).

opm install oneunit                     # test-runner глобально (нужен в PATH)
opm install --local --dev               # asserts, 1commands, html-sanitizer → ./oscript_modules
oscript tasks/test.os                   # юнит- и CommonMark-тесты + JUnit-отчёты в build/reports/
oscript tasks/test_unit.os              # только юнит-тесты
oscript tasks/test_commonmark.os        # только CommonMark conformance
oneunit execute -d tests/browser        # подсветка кода в браузере

Тест подсветки в браузере запускает Chrome без окна — google-chrome из PATH или путь из переменной CHROME_BIN — и ходит в esm.sh за shiki, поэтому в tasks/test.os не входит.

CommonMark-сьюты автогенерируются из официальной спецификации. Список xfail-примеров хранится в самом скрипте (SKIP_LIST в начале файла):

python tools/sync-commonmark.py 0.31.2

Подробнее об устройстве — в paln.md.

Лицензия

MIT © 2026 Egor Ivanov