From c647b29712c6a187099b1bddffc108fd4708ad9d Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Tue, 7 Apr 2026 15:44:42 +0500 Subject: [PATCH] Add Markdown import from Obsidian (Stage 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ROADMAP.md | 143 ++--- next.config.ts | 1 + package-lock.json | 754 +++++++++++++++++++++++++ package.json | 3 + src/app/api/admin/import-md/route.ts | 39 ++ src/components/admin/lesson-editor.tsx | 49 +- src/lib/md-to-tiptap.ts | 188 ++++++ 7 files changed, 1085 insertions(+), 92 deletions(-) create mode 100644 src/app/api/admin/import-md/route.ts create mode 100644 src/lib/md-to-tiptap.ts diff --git a/ROADMAP.md b/ROADMAP.md index 35647cb..adc7322 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) - Сертификаты по окончании курса - Геймификация (баллы, бейджи, рейтинги) - Промокоды и интеграция с платёжными системами diff --git a/next.config.ts b/next.config.ts index 68a6c64..deaa0a9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + transpilePackages: ["unified", "remark-parse"], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 069abb6..5e7eaba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 14f18ed..bce00cd 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/api/admin/import-md/route.ts b/src/app/api/admin/import-md/route.ts new file mode 100644 index 0000000..02bf80c --- /dev/null +++ b/src/app/api/admin/import-md/route.ts @@ -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 }); +} diff --git a/src/components/admin/lesson-editor.tsx b/src/components/admin/lesson-editor.tsx index 2d28f46..44fa284 100644 --- a/src/components/admin/lesson-editor.tsx +++ b/src/components/admin/lesson-editor.tsx @@ -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(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({
+
+ {importError && ( +

+ {importError} +

+ )} + {/* Title */}