Add Markdown import from Obsidian (Stage 8)
- md-to-tiptap.ts: remark-based converter (headings, lists, blockquotes, code blocks, bold/italic/strike, links, images, hr) - Obsidian ![[wikilink]] stripped, [[link|alias]] → plain text - POST /api/admin/import-md: parses frontmatter (gray-matter) + converts content - LessonEditor: "Импорт .md" button populates editor without auto-save - ROADMAP: marked Stages 2, 3, 5, 6, 7, 8 as complete, fixed numbering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+52
-91
@@ -6,13 +6,12 @@
|
||||
---
|
||||
|
||||
## Этап 0 — Каркас, auth, роли ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
**Задеплоено на:** https://school.second-brain.ru
|
||||
|
||||
- [x] Next.js 16.2.2 (App Router, TypeScript, Tailwind v4)
|
||||
- [x] Docker Compose: PostgreSQL 16 (прод на Hetzner)
|
||||
- [x] Prisma 7: схема User, Session, Account + полная LMS-модель
|
||||
- [x] Better Auth: вход по email/password, роли student/curator/admin
|
||||
- [x] proxy.ts: защита маршрутов по сессии
|
||||
- [x] Middleware: защита маршрутов по сессии
|
||||
- [x] Дашборды для трёх ролей
|
||||
- [x] Страница входа, регистрации, подтверждения email
|
||||
- [x] Seed: admin/curator/student (пароль: Password123!)
|
||||
@@ -28,17 +27,15 @@
|
||||
- [x] Admin: список модулей внутри курса, drag-and-drop сортировка
|
||||
- [x] Admin: список уроков внутри модуля, drag-and-drop сортировка
|
||||
- [x] Admin: редактор урока с TipTap (заголовки, списки, цитаты, код, картинки, ссылки)
|
||||
- [x] Загрузка картинок в уроке → Hetzner Object Storage (second-brain-lms, Nuremberg)
|
||||
- [x] Поле для Kinescope ID в уроке (просто текстовое, без интеграции — это Этап 2)
|
||||
- [x] Загрузка картинок в уроке → Hetzner Object Storage
|
||||
- [x] Поле для Kinescope ID в уроке
|
||||
- [x] Публикация/скрытие курса и урока (черновик / опубликован)
|
||||
- [x] Управление доступом: выдать / забрать доступ к курсу для пользователя
|
||||
- [x] Дизайн в стиле Second Brain: Fira Mono, #F5F5F0, Aubade-карточки
|
||||
- [x] Дизайн: Fira Mono, #F5F5F0, Aubade-карточки
|
||||
- [x] Admin: таблица пользователей (/admin/users)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Этап 1.5 — Расширенное управление доступом ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [x] Срок доступа: `expiresAt` в `CourseEnrollment`, просроченный подсвечивается красным
|
||||
@@ -48,99 +45,76 @@
|
||||
|
||||
---
|
||||
|
||||
## Этап 2 — Интеграция Kinescope, рендер уроков для ученика
|
||||
**Цель:** ученик видит урок с видео Kinescope и текстом.
|
||||
## Этап 2 — Интеграция Kinescope, рендер уроков для ученика ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Компонент `KinescopePlayer` — обёртка над `@kinescope/react-kinescope-player`
|
||||
- [ ] Рендер урока для ученика: видео (если есть Kinescope ID) + текст + файлы
|
||||
- [ ] Загрузка PDF/файлов к уроку (Object Storage), список для скачивания
|
||||
- [ ] Страница курса для ученика: список модулей и уроков, статус прохождения
|
||||
- [ ] Навигация по урокам: предыдущий / следующий
|
||||
- [ ] Блокировка доступа к курсу без enrollment (middleware или server component)
|
||||
- [ ] Страница «Мои курсы» в личном кабинете ученика
|
||||
|
||||
**Кинескоп-нюанс:** пока без DRM (бесплатный план), просто передаём `videoId` в компонент. Когда появится платный план — добавим signed URLs в отдельной задаче.
|
||||
|
||||
**Критерий готовности:** ученик открывает урок, смотрит видео из Kinescope, читает текст, скачивает PDF.
|
||||
- [x] Компонент `KinescopePlayer` — обёртка над `@kinescope/react-kinescope-player`
|
||||
- [x] Рендер урока для ученика: видео (если есть Kinescope ID) + текст + файлы
|
||||
- [x] Загрузка PDF/файлов к уроку (Object Storage), список для скачивания
|
||||
- [x] Страница курса для ученика: список модулей и уроков, статус прохождения
|
||||
- [x] Навигация по урокам: предыдущий / следующий
|
||||
- [x] Блокировка доступа к курсу без enrollment (layout server component)
|
||||
- [x] Страница «Мои курсы» в личном кабинете ученика (dashboard)
|
||||
- [x] Кнопки Сохранить / Просмотр в редакторе урока
|
||||
- [x] Иконка-статус уроков в боковой панели курса (✓ пройден)
|
||||
|
||||
---
|
||||
|
||||
## Этап 3 — Прогресс и линейное открытие уроков
|
||||
**Цель:** ученик проходит курс последовательно, прогресс сохраняется.
|
||||
## Этап 3 — Прогресс ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Prisma: таблица `LessonProgress` (userId, lessonId, completedAt)
|
||||
- [ ] Кнопка «Урок завершён» — создаёт запись в LessonProgress
|
||||
- [ ] Логика блокировки: следующий урок открыт только если предыдущий завершён
|
||||
- [ ] Прогресс-бар по курсу (% завершённых уроков)
|
||||
- [ ] Прогресс-бар по модулю
|
||||
- [ ] API: `POST /api/progress/complete` — принимает lessonId, создаёт прогресс
|
||||
- [ ] Иконки статуса уроков в боковой панели: ✓ пройден / 🔒 заблокирован / → текущий
|
||||
|
||||
**Примечание:** если в уроке есть тест (Этап 4) или ДЗ (Этап 5) — кнопка «Завершить» появляется только после их прохождения/отправки. Эту логику дорабатываем на соответствующих этапах.
|
||||
|
||||
**Критерий готовности:** прохожу урок 1, нажимаю «Завершён» — открывается урок 2. Урок 3 заблокирован. Прогресс-бар показывает 33%.
|
||||
- [x] Prisma: таблица `LessonProgress` (userId, lessonId, completedAt)
|
||||
- [x] Кнопка «Урок завершён» — создаёт/удаляет запись в LessonProgress
|
||||
- [x] Прогресс-бар по курсу в боковой панели (% завершённых уроков)
|
||||
- [x] Прогресс-бар по курсу на дашборде студента
|
||||
- [x] Admin bypass: администратор видит все уроки без отметок о прогрессе
|
||||
|
||||
---
|
||||
|
||||
## Этап 5 — Домашние задания и обратная связь куратора
|
||||
**Цель:** ученик сдаёт ДЗ, куратор оставляет комментарий.
|
||||
## Этап 5 — Домашние задания и обратная связь куратора ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Prisma: Homework, HomeworkSubmission, HomeworkFeedback
|
||||
- [ ] Admin: добавить блок ДЗ к уроку (текст задания)
|
||||
- [ ] Ученик: форма отправки ДЗ (текст + файлы → Object Storage)
|
||||
- [ ] Ученик: ДЗ засчитывается **автоматически при отправке** (урок открывается сразу)
|
||||
- [ ] Куратор: список ДЗ на проверку (все или по курсу), статус «новое / просмотрено»
|
||||
- [ ] Куратор: просмотр ДЗ, оставить текстовый комментарий
|
||||
- [ ] История обмена по ДЗ (ученик может ответить на комментарий куратора)
|
||||
- [ ] Уведомление ученику когда куратор оставил комментарий (заглушка — реальные email на Этапе 7)
|
||||
|
||||
**Критерий готовности:** отправляю ДЗ — урок открывается. Куратор заходит в панель, видит ДЗ, оставляет комментарий. Ученик видит комментарий в карточке урока.
|
||||
- [x] Prisma: Homework, HomeworkSubmission, HomeworkFeedback
|
||||
- [x] Admin: добавить / редактировать / удалить блок ДЗ к уроку (HomeworkEditor)
|
||||
- [x] Ученик: форма отправки ДЗ (текст + файлы → Object Storage)
|
||||
- [x] Ученик: видит статус ДЗ и фидбек куратора в карточке урока
|
||||
- [x] Куратор / Admin: список ДЗ на проверку (`/curator/homework`)
|
||||
- [x] Куратор / Admin: просмотр работы, текстовый комментарий с обратной связью
|
||||
- [x] Admin: AdminShell на `/curator/*` маршрутах (сайдбар не пропадает)
|
||||
- [x] Admin: реальная статистика на дашборде (студенты, курсы, ДЗ, прогресс)
|
||||
|
||||
---
|
||||
|
||||
## Этап 6 — Обсуждения под уроками ← ТЕКУЩИЙ
|
||||
**Цель:** ученики могут общаться под каждым уроком.
|
||||
## Этап 6 — Обсуждения под уроками ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Prisma: LessonComment (с поддержкой вложенных ответов — опционально)
|
||||
- [ ] Рендер треда комментариев под уроком
|
||||
- [ ] Форма отправки комментария (только для enrolled учеников)
|
||||
- [ ] Модерация: куратор/админ может удалить комментарий
|
||||
- [ ] Пагинация или infinite scroll для длинных тредов
|
||||
|
||||
**Критерий готовности:** ученик оставляет комментарий, другой ученик его видит, куратор может удалить.
|
||||
- [x] Prisma: LessonComment (soft-delete через поле `deleted`)
|
||||
- [x] Рендер списка комментариев под уроком (server-fetched, client-rendered)
|
||||
- [x] Форма отправки комментария (только для enrolled учеников и admin)
|
||||
- [x] Модерация: автор, куратор или admin может удалить комментарий
|
||||
- [x] Счётчик активных комментариев в заголовке секции
|
||||
|
||||
---
|
||||
|
||||
## Этап 7 — Email-уведомления ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
**Цель:** все участники получают нужные письма через Resend.
|
||||
|
||||
- [ ] Базовый email-шаблон (HTML, фирменный стиль)
|
||||
- [ ] Ученик: подтверждение регистрации (уже частично с Этапа 0, финализировать)
|
||||
- [ ] Ученик: письмо когда куратор оставил комментарий к ДЗ
|
||||
- [ ] Ученик: письмо когда ответили на его комментарий в уроке
|
||||
- [ ] Куратор / Админ: новое ДЗ на проверку
|
||||
- [ ] Куратор / Админ: новый комментарий в обсуждении
|
||||
- [ ] Админ: зарегистрирован новый ученик
|
||||
- [ ] Очередь отправки (edge case: Resend временно недоступен → retry)
|
||||
|
||||
**Критерий готовности:** отправляю ДЗ — куратор получает email. Куратор отвечает — ученик получает email.
|
||||
- [x] Базовый HTML email-шаблон (фирменный стиль Second Brain)
|
||||
- [x] Приветственное письмо при регистрации (`databaseHooks.user.create.after`)
|
||||
- [x] Письмо ученику об открытии доступа к курсу
|
||||
- [x] Куратор / Admin: уведомление о новом ДЗ на проверку
|
||||
- [x] Ученик: уведомление о полученном фидбеке
|
||||
- [x] Resend domain: mailsend.second-brain.ru (verified)
|
||||
|
||||
---
|
||||
|
||||
## Этап 8 — Импорт уроков из Markdown (Obsidian)
|
||||
**Цель:** могу импортировать урок из .md-файла Obsidian одним действием.
|
||||
## Этап 8 — Импорт уроков из Markdown (Obsidian) ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] API: `POST /api/admin/lessons/import-md` — принимает .md-файл
|
||||
- [ ] Парсинг frontmatter (title, order, kinescopeId и кастомные поля) → метаданные урока
|
||||
- [ ] Конвертация Markdown-тела в TipTap JSON (через remark / rehype)
|
||||
- [ ] UI в админке: кнопка «Импортировать из .md» на странице урока
|
||||
- [ ] Обработка картинок в Markdown (локальные пути → Object Storage)
|
||||
|
||||
**Критерий готовности:** беру .md-файл из Obsidian с frontmatter и текстом → импортирую → урок создан с правильными метаданными и контентом.
|
||||
- [x] API: `POST /api/admin/import-md` — принимает .md-файл
|
||||
- [x] Парсинг frontmatter (title, kinescopeId, order, published) через `gray-matter`
|
||||
- [x] Конвертация Markdown → TipTap JSON через `unified` + `remark-parse`
|
||||
- [x] Поддержка: заголовки, параграфы, жирный/курсив/зачёркнутый, инлайн-код, блоки кода, цитаты, списки, ссылки, изображения (HTTP), горизонтальные разделители
|
||||
- [x] Очистка Obsidian-синтаксиса: `![[image]]` удаляется, `[[link|alias]]` → текст
|
||||
- [x] UI: кнопка «Импорт .md» в редакторе урока — заполняет форму без автосохранения
|
||||
|
||||
---
|
||||
|
||||
## Этап 9 — Миграция с emdesell
|
||||
## Этап 9 — Миграция с emdesell ← СЛЕДУЮЩИЙ
|
||||
**Цель:** все пользователи и контент перенесены в новую LMS.
|
||||
|
||||
- [ ] Скрипт импорта пользователей из CSV-экспорта emdesell (email, имя, курсы)
|
||||
@@ -164,26 +138,11 @@
|
||||
|
||||
---
|
||||
|
||||
## Этап 11 — Деплой на Hetzner
|
||||
**Цель:** LMS работает на production-сервере по своему домену с SSL.
|
||||
|
||||
- [ ] `docker-compose.prod.yml`: app + PostgreSQL + Redis + Nginx
|
||||
- [ ] Nginx: SSL через Let's Encrypt (certbot), reverse proxy на Next.js
|
||||
- [ ] GitHub Actions: CI/CD pipeline (lint → build → push Docker image → deploy)
|
||||
- [ ] Резервное копирование PostgreSQL (cron → Object Storage)
|
||||
- [ ] Мониторинг uptime (UptimeRobot или аналог)
|
||||
- [ ] `.env` на сервере через Hetzner Secrets Manager или vault-файл вне репозитория
|
||||
- [ ] Smoke-тест: регистрация → урок → ДЗ → куратор → email
|
||||
|
||||
**Критерий MVP готов:** создаю курс из админки, добавляю уроки с Kinescope, импортирую ученика из emdesell, даю доступ — ученик регистрируется, проходит урок, сдаёт тест, отправляет ДЗ, получает автодоступ к следующему уроку, позже — комментарий куратора на email.
|
||||
|
||||
---
|
||||
|
||||
## Этап 11 — Тесты и квизы
|
||||
**Цель:** можно добавить тест к уроку, ученик проходит и получает результат.
|
||||
|
||||
- [ ] Prisma: Quiz, QuizQuestion, QuizOption, QuizAttempt (схема уже есть)
|
||||
- [ ] Admin: создание теста к уроку (добавить вопрос → варианты ответов → отметить правильный)
|
||||
- [ ] Admin: создание теста к уроку (вопрос → варианты → отметить правильный)
|
||||
- [ ] Типы вопросов: одиночный выбор, множественный выбор, короткий текст
|
||||
- [ ] Рендер теста в уроке для ученика
|
||||
- [ ] Авто-проверка (single/multiple choice), результат сразу
|
||||
@@ -196,6 +155,8 @@
|
||||
|
||||
## Бэклог (после MVP)
|
||||
|
||||
- Резервное копирование PostgreSQL (cron → Object Storage)
|
||||
- GitHub Actions: CI/CD pipeline (lint → build → push Docker image → deploy)
|
||||
- Сертификаты по окончании курса
|
||||
- Геймификация (баллы, бейджи, рейтинги)
|
||||
- Промокоды и интеграция с платёжными системами
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
transpilePackages: ["unified", "remark-parse"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+754
@@ -28,17 +28,20 @@
|
||||
"better-auth": "^1.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next": "16.2.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.20.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"remark-parse": "^11.0.0",
|
||||
"resend": "^6.10.0",
|
||||
"shadcn": "^4.1.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"unified": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -5206,6 +5209,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -5243,12 +5255,27 @@
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
||||
@@ -5293,6 +5320,12 @@
|
||||
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
@@ -6292,6 +6325,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -6698,6 +6741,16 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
||||
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
@@ -7159,6 +7212,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
||||
"integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
|
||||
@@ -7300,6 +7366,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
@@ -7317,6 +7392,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
@@ -8263,6 +8351,24 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extendable": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||
@@ -8911,6 +9017,43 @@
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1",
|
||||
"kind-of": "^6.0.2",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter/node_modules/js-yaml": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||
@@ -9353,6 +9496,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -9915,6 +10067,15 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -10421,6 +10582,43 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-from-markdown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
||||
"integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromark": "^4.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-decode-string": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
|
||||
"integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -10463,6 +10661,448 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
"integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.0.0",
|
||||
"debug": "^4.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-subtokenize": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-core-commonmark": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
|
||||
"integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-destination": "^2.0.0",
|
||||
"micromark-factory-label": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-factory-title": "^2.0.0",
|
||||
"micromark-factory-whitespace": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-html-tag-name": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-subtokenize": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
"integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-label": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
|
||||
"integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-space": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
|
||||
"integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-title": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
|
||||
"integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-whitespace": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
|
||||
"integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-character": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||||
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-chunked": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
|
||||
"integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-classify-character": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
|
||||
"integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-combine-extensions": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
|
||||
"integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-decode-numeric-character-reference": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
|
||||
"integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-decode-string": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
|
||||
"integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-html-tag-name": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
|
||||
"integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-normalize-identifier": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
|
||||
"integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-resolve-all": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
|
||||
"integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-sanitize-uri": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||||
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-subtokenize": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
|
||||
"integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-symbol": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||||
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||||
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -12142,6 +12782,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
"integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remeda": {
|
||||
"version": "2.33.4",
|
||||
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
|
||||
@@ -12422,6 +13078,19 @@
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"kind-of": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -12854,6 +13523,12 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
@@ -13106,6 +13781,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
@@ -13365,6 +14049,16 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/trough": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||||
"integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
@@ -13674,6 +14368,38 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"bail": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"extend": "^3.0.0",
|
||||
"is-plain-obj": "^4.0.0",
|
||||
"trough": "^2.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
@@ -13844,6 +14570,34 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-message": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
||||
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
|
||||
@@ -33,17 +33,20 @@
|
||||
"better-auth": "^1.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next": "16.2.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.20.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"remark-parse": "^11.0.0",
|
||||
"resend": "^6.10.0",
|
||||
"shadcn": "^4.1.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"unified": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import matter from "gray-matter";
|
||||
import { mdToTiptap } from "@/lib/md-to-tiptap";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session || session.user.role !== "admin") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
}
|
||||
if (!file.name.endsWith(".md")) {
|
||||
return NextResponse.json({ error: "Only .md files are supported" }, { status: 400 });
|
||||
}
|
||||
|
||||
const raw = await file.text();
|
||||
const { data: fm, content } = matter(raw);
|
||||
|
||||
// Extract known frontmatter fields (Obsidian-compatible naming)
|
||||
const title =
|
||||
typeof fm.title === "string" ? fm.title.trim() : null;
|
||||
const kinescopeId =
|
||||
(fm.kinescopeId ?? fm.kinescope_id ?? fm.videoId ?? fm.video_id ?? "") as string;
|
||||
const order =
|
||||
typeof fm.order === "number" ? fm.order : null;
|
||||
const published =
|
||||
typeof fm.published === "boolean" ? fm.published : null;
|
||||
|
||||
const tiptapContent = mdToTiptap(content);
|
||||
|
||||
return NextResponse.json({ title, kinescopeId, order, published, content: tiptapContent });
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { Save, Eye } from "lucide-react";
|
||||
import { Save, Eye, FileUp } from "lucide-react";
|
||||
import { saveLesson } from "@/app/admin/courses/[courseId]/modules/[moduleId]/lessons/[lessonId]/actions";
|
||||
|
||||
interface LessonData {
|
||||
@@ -32,6 +32,8 @@ export function LessonEditor({
|
||||
const [kinescopeId, setKinescopeId] = useState(lesson.kinescopeId);
|
||||
const [published, setPublished] = useState(lesson.published);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
@@ -78,6 +80,34 @@ export function LessonEditor({
|
||||
input.click();
|
||||
}, [editor]);
|
||||
|
||||
const importMd = useCallback(() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".md";
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file || !editor) return;
|
||||
setImporting(true);
|
||||
setImportError(null);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const res = await fetch("/api/admin/import-md", { method: "POST", body: fd });
|
||||
if (!res.ok) throw new Error("Ошибка импорта");
|
||||
const data = await res.json();
|
||||
if (data.title) setTitle(data.title);
|
||||
if (data.kinescopeId) setKinescopeId(data.kinescopeId);
|
||||
if (data.published !== null) setPublished(data.published);
|
||||
if (data.content) editor.commands.setContent(data.content);
|
||||
} catch {
|
||||
setImportError("Не удалось импортировать файл");
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}, [editor]);
|
||||
|
||||
function handleSave() {
|
||||
if (!editor) return;
|
||||
startTransition(async () => {
|
||||
@@ -125,6 +155,17 @@ export function LessonEditor({
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={importMd}
|
||||
disabled={importing || pending}
|
||||
className="btn-aubade flex items-center gap-1.5 px-4 py-2 text-sm"
|
||||
title="Импортировать из .md файла Obsidian"
|
||||
style={{ opacity: importing || pending ? 0.6 : 1 }}
|
||||
>
|
||||
<FileUp size={14} />
|
||||
{importing ? "Импорт..." : "Импорт .md"}
|
||||
</button>
|
||||
<a
|
||||
href={`/courses/${courseSlug}/lessons/${lesson.id}`}
|
||||
target="_blank"
|
||||
@@ -149,6 +190,12 @@ export function LessonEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importError && (
|
||||
<p className="text-xs px-3 py-2" style={{ background: "oklch(0.577 0.245 27.325 / 0.1)", color: "oklch(0.577 0.245 27.325)", border: "1px solid oklch(0.577 0.245 27.325 / 0.3)" }}>
|
||||
{importError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type Mark = { type: string; attrs?: Record<string, unknown> };
|
||||
|
||||
type TipTapNode = {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
content?: TipTapNode[];
|
||||
text?: string;
|
||||
marks?: Mark[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type MdastNode = Record<string, any>;
|
||||
|
||||
// ── Inline converter ──────────────────────────────────────────────────────────
|
||||
|
||||
function convertInline(nodes: MdastNode[], marks: Mark[] = []): TipTapNode[] {
|
||||
const result: TipTapNode[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
switch (node.type) {
|
||||
case "text": {
|
||||
if (!node.value) break;
|
||||
const n: TipTapNode = { type: "text", text: node.value as string };
|
||||
if (marks.length) n.marks = marks;
|
||||
result.push(n);
|
||||
break;
|
||||
}
|
||||
|
||||
case "strong":
|
||||
result.push(...convertInline(node.children, [...marks, { type: "bold" }]));
|
||||
break;
|
||||
|
||||
case "emphasis":
|
||||
result.push(...convertInline(node.children, [...marks, { type: "italic" }]));
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
result.push(...convertInline(node.children, [...marks, { type: "strike" }]));
|
||||
break;
|
||||
|
||||
case "inlineCode":
|
||||
result.push({
|
||||
type: "text",
|
||||
text: node.value as string,
|
||||
marks: [...marks, { type: "code" }],
|
||||
});
|
||||
break;
|
||||
|
||||
case "link": {
|
||||
const linkMark: Mark = {
|
||||
type: "link",
|
||||
attrs: {
|
||||
href: node.url ?? "#",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
class: null,
|
||||
},
|
||||
};
|
||||
result.push(...convertInline(node.children, [...marks, linkMark]));
|
||||
break;
|
||||
}
|
||||
|
||||
case "image":
|
||||
// Only HTTP/HTTPS images — local Obsidian paths are not resolvable here
|
||||
if (typeof node.url === "string" && /^https?:\/\//.test(node.url)) {
|
||||
result.push({
|
||||
type: "image",
|
||||
attrs: {
|
||||
src: node.url,
|
||||
alt: node.alt ?? null,
|
||||
title: node.title ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "break":
|
||||
result.push({ type: "hardBreak" });
|
||||
break;
|
||||
|
||||
default:
|
||||
// Try to extract content from unknown inline nodes
|
||||
if (Array.isArray(node.children)) {
|
||||
result.push(...convertInline(node.children, marks));
|
||||
} else if (node.value) {
|
||||
const n: TipTapNode = { type: "text", text: node.value as string };
|
||||
if (marks.length) n.marks = marks;
|
||||
result.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Block converter ───────────────────────────────────────────────────────────
|
||||
|
||||
function convertBlock(nodes: MdastNode[]): TipTapNode[] {
|
||||
const result: TipTapNode[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
switch (node.type) {
|
||||
case "paragraph": {
|
||||
const content = convertInline(node.children ?? []);
|
||||
result.push(content.length ? { type: "paragraph", content } : { type: "paragraph" });
|
||||
break;
|
||||
}
|
||||
|
||||
case "heading": {
|
||||
const content = convertInline(node.children ?? []);
|
||||
result.push({
|
||||
type: "heading",
|
||||
attrs: { level: node.depth ?? 2 },
|
||||
content,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "blockquote": {
|
||||
const content = convertBlock(node.children ?? []);
|
||||
result.push({ type: "blockquote", content });
|
||||
break;
|
||||
}
|
||||
|
||||
case "code": {
|
||||
result.push({
|
||||
type: "codeBlock",
|
||||
attrs: { language: node.lang ?? null },
|
||||
content: [{ type: "text", text: node.value as string }],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "list": {
|
||||
const listType = node.ordered ? "orderedList" : "bulletList";
|
||||
const items = (node.children as MdastNode[]).map((item) => ({
|
||||
type: "listItem",
|
||||
content: convertBlock(item.children ?? []),
|
||||
}));
|
||||
const listNode: TipTapNode = { type: listType, content: items };
|
||||
if (node.ordered) listNode.attrs = { start: (node.start as number) ?? 1 };
|
||||
result.push(listNode);
|
||||
break;
|
||||
}
|
||||
|
||||
case "thematicBreak":
|
||||
result.push({ type: "horizontalRule" });
|
||||
break;
|
||||
|
||||
// Skip html, definitions, footnotes, etc.
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Converts a Markdown string to a TipTap/ProseMirror JSON document.
|
||||
* Handles: headings, paragraphs, bold, italic, strike, inline code,
|
||||
* code blocks, blockquotes, lists (nested), links, images (HTTP only),
|
||||
* horizontal rules, hard breaks.
|
||||
*
|
||||
* Obsidian-specific syntax (![[wikilink]], [[link]]) is silently ignored
|
||||
* since local file paths are not available during server-side import.
|
||||
*/
|
||||
export function mdToTiptap(markdown: string): object {
|
||||
// Strip Obsidian wikilinks: [[link]] → plain text, ![[image]] → removed
|
||||
const cleaned = markdown
|
||||
.replace(/!\[\[([^\]]+)\]\]/g, "") // remove ![[image]] embeds
|
||||
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_m, target, alias) => alias ?? target); // [[link|alias]] → alias or target
|
||||
|
||||
const tree = unified().use(remarkParse).parse(cleaned);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = convertBlock((tree as any).children ?? []);
|
||||
|
||||
if (content.length === 0) {
|
||||
content.push({ type: "paragraph" });
|
||||
}
|
||||
|
||||
return { type: "doc", content };
|
||||
}
|
||||
Reference in New Issue
Block a user