commit 3a8045a141b27b76294393baff5327a9c3bb56c3 Author: StepanovPlaton Date: Sat Oct 3 23:43:38 2026 +0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f4413c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +report.docx +lab-gen.exe +*.log +*.pdf diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8834b9b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,324 @@ +# AGENTS.md — генератор lab-gen + +`lab-gen` — автономный инструмент, который собирает отчёт в формате **DOCX** из обычных +**Markdown**-файлов по фиксированному дизайну СТО 02068410-004-2018. Поведение полностью +задаётся файлом `config.yaml`. + +Документ создан и для людей, и для ИИ-агентов: он описывает CLI, формат контента, схему +конфига и порядок сборки `.exe`. + +--- + +## 1. Требования + +- Windows (собранный `.exe` под `win-x64`). +- Node.js **только для сборки** `.exe` из исходников. Конечному пользователю Node.js не нужен. +- Никаких Java/Chrome/интернета: инструмент не рендерит диаграммы, а вставляет готовые PNG. + +## 2. Состав дистрибутива + +``` +lab-gen.exe # исполняемый файл +config.yaml # настройки (создаётся автоматически при первом запуске) +content/ # Markdown-файлы документа и ресурсы + *.md + assets/ + images/ # PNG для рисунков (готовятся заранее) + title-page.docx # титульный лист (по умолчанию content/assets/title-page.docx) +report.docx # результат сборки (путь задаётся document.output) +``` + +Все относительные пути в `config.yaml` считаются **от каталога самого `config.yaml`**. +Сам `config.yaml` и папки ищутся **рядом с `lab-gen.exe`**. + +## 3. Командная строка + +``` +lab-gen [опции] + + --config <путь> путь к config.yaml (по умолчанию рядом с exe) + --content каталог с config.yaml, content/ и assets/ (по умолчанию рядом с exe) + --output <путь> переопределить путь выходного .docx + --check только проверить конфиг и ссылки, без записи файла + --init создать шаблон config.yaml/content/assets и выйти + --verbose подробный вывод + --version показать версию + --help справка +``` + +Поведение: + +- Если `config.yaml` не найден, инструмент **сам создаёт** `config.yaml`, примеры `content/*.md` + и `assets/images/placeholder.png`, печатает подсказку и завершается. +- `--check` не пишет DOCX, но сообщает обо всех ошибках (битые ссылки, выход `[@n]` за границы + списка источников, отсутствующие картинки и т.п.). + +Типичный цикл работы: + +```powershell +.\lab-gen.exe --init # первый запуск: получить шаблон +# ... правим config.yaml и content/*.md ... +.\lab-gen.exe --check # проверить +.\lab-gen.exe # собрать report.docx +``` + +## 4. Схема `config.yaml` + +Любой ключ можно опустить — берётся значение по умолчанию (см. `src/config/defaults.js`). + +### document +| Ключ | Тип | Смысл | +|------|-----|-------| +| `output` | string | путь выходного `.docx` | +| `titlePage` | string/null | путь к готовому `title-page.docx` (по умолчанию `content/assets/title-page.docx`); `null` — без титульника | +| `features.updateFields` | bool | обновлять поля (в т.ч. оглавление) при открытии в Word | + +**Титульный лист.** Берётся готовый DOCX по пути `document.titlePage` и склеивается перед +документом; между титульником и основным текстом вставляется разрыв страницы. При `--init` +создаётся пример `content/assets/title-page.docx` — замените его своим. Значение `null` +отключает титульный лист. + +### paths +| Ключ | Смысл | +|------|-------| +| `assets` | корень ресурсов; `file=images/x.png` в директивах ищется здесь (по умолчанию `content/assets`) | + +### output +Путь и имя выходного `.docx` задаются ключом `document.output` (по умолчанию +`report.docx`). Путь считается от каталога `config.yaml` и может быть переопределён +флагом `--output`. + +### page +`width`, `height` (DXA, 1/20 pt), `margins: {left,right,top,bottom}` (twips). A4 и поля СТО +заданы по умолчанию. + +### typography +| Ключ | Смысл | +|------|-------| +| `font` | гарнитура (по умолчанию Times New Roman) | +| `sizeBody` / `sizeTable` / `sizeSmall` | размеры в half-points (28 = 14 pt) | +| `lineSpacing` | множитель межстрочного интервала (1.5 = полуторный) | +| `color` | цвет текста (hex, без `#`) | +| `useBold` | разрешить полужирный (СТО: `false`) | +| `allowEmphasis` | разрешить курсив (СТО: `false`) | + +### headings +Интервалы/выравнивание для `struct`, `section`, `subsection`, `point` (twips). + +### captions +`figure`, `table`, `tableContinue` — префиксы и тире подписей (`Рисунок`, `Таблица`, `—`). + +### lists +`bulletChar`, `letterSuffix`, `numberSuffix`, `nestedLevelStep`, `forbiddenLetters`. + +### numbering +`figures`, `tables`, `formulas`: `global` (сквозная по документу) или `section` (в пределах +раздела, вид `1.1`). Приложения всегда имеют префикс буквы (`А.1`). + +### formula +`spacingBeforeAfter`, `whereTabPosition`. + +### table +`font`, `size`, `borderColor`, `borderSize`, `cellMargins: {top,bottom,left,right}`, +`headerFill`, `cellFill`. + +### figure +`maxWidth` (px; `null` — по ширине полосы), `maxHeight` (px). + +### pageNumber +`align`, `format`, `startFrom`. + +### toc +`enabled` (bool), `title`, `depth` (1..3). Оглавление вставляется записью +`sections: [{ type: toc }]`. + +### bibliography / citation / appendix +`bibliography.title`, `citation: {bracketOpen,bracketClose,pagePrefix}`, +`appendix: {labelPrefix, forbiddenLetters}`. + +### sections — структура документа +Упорядоченный список частей: + +```yaml +sections: + - { type: toc } # поле оглавления + - { file: content/01-introduction.md, type: structural, title: Введение } + - { file: content/02-main.md, type: numbered } # # = 1, ## = 1.1, ### = 1.1.1 + - { file: content/03-conclusion.md, type: structural, title: Заключение } + - { file: content/04-bibliography.md, type: bibliography } + - { file: content/05-appendix-a.md, type: appendix, letter: А, title: Приложение } +``` + +Типы частей: + +| type | Поведение | +|------|-----------| +| `toc` | вставляет оглавление (не требует `file`) | +| `structural` | ненумерованный раздел (Введение/Заключение): заголовок = `title`, ЗАГЛАВНЫМИ, по центру, с новой страницы | +| `numbered` | нумеруемый раздел; `#` = раздел, `##`/`###` = подраздел/пункт | +| `bibliography` | список источников; нумеруется автоматически | +| `appendix` | приложение: заголовок «ПРИЛОЖЕНИЕ » + `title`, нумерация рисунков/таблиц с префиксом буквы | + +## 5. Формат контента + +### Заголовки + +В файле `type: numbered`: + +```md +# НАЗВАНИЕ РАЗДЕЛА -> 1 НАЗВАНИЕ РАЗДЕЛА (с новой страницы) +## Подраздел -> 1.1 Подраздел +### Пункт -> 1.1.1 Пункт +``` + +Номера подставляются автоматически (Word-numbering). В `structural`/`appendix` заголовок +берётся из конфига, а заголовки внутри файла рендерятся как обычные заголовки без номера. + +### Абзацы, списки + +Обычные абзацы и списки Markdown. Списки: + +```md +- первый пункт; +- второй пункт: + 1. вложенный пункт (выведется как «а) вложенный пункт»); + 2. ещё один. +``` + +Маркер вложенности выбирается автоматически: тире → `а)` → `1)`. Полужирный/курсив по +умолчанию игнорируются (СТО). + +### Таблицы + +- Обычная GFM-таблица без директивы — таблица **без подписи и номера**. +- С подписью и автономером — директива `::: table`: + +```md +::: table "Название таблицы" id=params note="примечание" +| Параметр | Значение | +|---|---| +| A | 1 | +| B | 2 | +::: +``` + +### Рисунки + +```md +::: figure "Название рисунка" id=arch file=images/arch.png +а) первый поясняемый элемент; +б) второй поясняемый элемент. +::: +``` + +`file` — относительно `config.paths.assets`. Если файла нет — предупреждение, подпись +всё равно выводится. + +**Подписи по СТО.** Подпись ставится **под** рисунком по центру: `Рисунок N — Наименование` +(с прописной буквы, без точки в конце). Если на рисунке обозначены отдельные детали, +пояснительные данные размещают **над подписью** и оформляют перечислением `а)`, `б)`, `в)` — +именно так, как показано в примере выше. Если рисунок общий и пояснять нечего, указывают +только подпись `Рисунок N — Наименование`, а тело директивы оставляют пустым: + +```md +::: figure "Общая схема системы" id=common file=images/common.png +::: +``` + +В приложениях номер рисунка получает префикс буквы: `Рисунок А.1 — Наименование`. + +### Формулы + +```md +::: formula id=line +y = a*x + b +::: + +::: where +y — результат; +x — аргумент; +a — коэффициент. +::: +``` + +В формулах поддерживается простой синтаксис: `^` (степень), `_` (индекс), `/` (дробь). +Номер формулы ставится автоматически справа. + +### Примечания и термины + +```md +::: note +Текст примечания. +::: + +::: notes +- первое примечание; +- второе примечание. +::: + +::: definitions +Термин — определение; +Ещё термин — определение. +::: +``` + +### Разрыв страницы + +```md +::: pagebreak +::: +``` + +Тематический разрыв `---` также трактуется как разрыв страницы. + +### Ссылки на источники + +`[@1]` — ссылка на 1-й источник, `[@1, с. 96]` — с номером страницы. Список источников — +это Markdown-список в файле `type: bibliography`; порядок строк = номера источников. +Ссылка вне диапазона — ошибка в `--check`. + +### Перекрёстные ссылки + +`@fig:id`, `@tbl:id`, `@eq:id` подставляют актуальный номер рисунка/таблицы/формулы +(например `2`, `1.1`, `А.1`). `id` задаётся в директиве. + +### Блоки кода + +Функция листингов удалена: fenced-блоки ``` игнорируются с предупреждением. + +## 6. Проверка и ошибки + +`lab-gen --check` проверяет: + +- корректность `config.yaml` (типы, существование файлов, допустимые буквы приложений); +- наличие всех `sections[].file`; +- существование меток в `@fig/@tbl/@eq`; +- диапазон ссылок `[@n]`. + +Ошибки печатаются списком и возвращают код выхода `1`. Предупреждения (например, +отсутствующий PNG) не останавливают сборку. + +## 7. Сборка `.exe` из исходников + +```powershell +npm install +npm run build:exe # -> lab-gen.exe в корне (target node24-win-x64) +npm run generate # локальная сборка (node src/cli.js) +npm run check # проверка конфига и ссылок +``` + +Упаковка выполняется через `@yao-pkg/pkg`. Чтобы уменьшить размер, можно добавить +`--compress Brotli` в команду `build:exe`. + +### Подготовка диаграмм + +Инструмент не рендерит диаграммы: PNG готовятся заранее и кладутся в `content/assets/images/`. +Диаграммы можно строить любым внешним средством (PlantUML, Mermaid, draw.io, редактор). + +## 8. Замечания по дизайну + +- Дизайн фиксирован под СТО 02068410-004-2018; меняются только параметры из `config.yaml`. +- Полужирный и курсив по умолчанию отключены (`typography.useBold`, `allowEmphasis`). +- Шрифт, размеры, интервалы, отступы и подписи настраиваются в конфиге. +- Оглавление — поле Word; при открытии нажмите «Обновить поле», либо включено + `document.features.updateFields`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..876023f --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# lab-gen + +Автономный генератор отчётов **DOCX** из **Markdown** по дизайну +[СТО 02068410-004-2018](documentation/STO_SGAU_02068410-004-2018.md). +Поведение полностью задаётся файлом `config.yaml`; путь выходного файла — ключ +`document.output`. + +Полная документация: **[AGENTS.md](AGENTS.md)** — CLI, схема конфига, формат контента, +директивы и сборка `.exe`. + +## Быстрый старт + +```powershell +.\lab-gen.exe --init # создать config.yaml, примеры content/ и content/assets/ +# ... правим config.yaml и content/*.md ... +.\lab-gen.exe --check # проверить ссылки и конфиг +.\lab-gen.exe # собрать report.docx +``` + +## Разработка + +```powershell +npm install +npm run generate # локальная сборка report.docx +npm run check # проверка конфига и ссылок +npm run build:exe # собрать lab-gen.exe в корне (@yao-pkg/pkg, node24-win-x64) +``` + +## Структура репозитория + +| Путь | Назначение | +|------|------------| +| `src/cli.js` | точка входа, разбор аргументов | +| `src/config/` | дефолты, загрузка и валидация `config.yaml`, шаблон | +| `src/markdown/` | markdown-it, директивы `:::`, инлайн-ссылки `[@n]`, `@fig/@tbl/@eq` | +| `src/render/` | сборка DOCX: стили, примитивы, нумерация, слияние титульника | +| `src/build.js` | оркестрация сборки | +| `content/` | примеры Markdown | +| `content/assets/` | PNG для рисунков, опциональный `title-page.docx` | +| `documentation/` | стандарт СТО | + +Инструмент не рендерит диаграммы: PNG готовятся заранее и кладутся в `content/assets/images/`. diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..b36143e --- /dev/null +++ b/config.yaml @@ -0,0 +1,140 @@ +# Конфигурация генератора lab-gen. +# Все относительные пути считаются от каталога этого файла. +# Любой ключ можно опустить — будет использовано значение по умолчанию. + +document: + output: report.docx + titlePage: content/assets/title-page.docx + features: + updateFields: true +paths: + assets: content/assets +page: + width: 11906 + height: 16838 + margins: + left: 1701 + right: 851 + top: 1134 + bottom: 1134 +typography: + font: Times New Roman + sizeBody: 28 + sizeTable: 28 + sizeSmall: 24 + lineSpacing: 1.5 + indentFirst: 709 + color: '000000' + useBold: false + allowEmphasis: false +headings: + struct: + spacingBefore: 360 + spacingAfter: 560 + align: center + section: + spacingBefore: 920 + spacingAfter: 0 + align: left + subsection: + spacingBefore: 284 + spacingBeforeAfterSection: 240 + spacingAfter: 0 + align: left + point: + spacingBefore: 360 + spacingAfter: 0 +captions: + figure: + prefix: Рисунок + dash: — + align: center + table: + prefix: Таблица + dash: — + align: left + tableContinue: + prefix: Продолжение таблицы +lists: + bulletChar: '-' + letterSuffix: ) + numberSuffix: ) + forbiddenLetters: + - ё + - з + - й + - о + - ч + - ь + - ы + - ъ + nestedLevelStep: 360 +numbering: + figures: global + tables: global + formulas: global +formula: + spacingBeforeAfter: 360 + whereTabPosition: 720 +table: + font: Times New Roman + size: 28 + borderColor: '000000' + borderSize: 1 + cellMargins: + top: 57 + bottom: 57 + left: 113 + right: 113 + headerFill: FFFFFF + cellFill: FFFFFF +figure: + maxWidth: null + maxHeight: 620 +pageNumber: + align: center + format: decimal + startFrom: 1 +toc: + enabled: true + title: Содержание + depth: 3 +bibliography: + title: Список использованных источников +citation: + bracketOpen: '[' + bracketClose: ']' + pagePrefix: с. +appendix: + labelPrefix: ПРИЛОЖЕНИЕ + forbiddenLetters: + - Ё + - З + - Й + - О + - Ч + - Ъ + - Ы + - Ь +sections: + - type: toc + - file: content/01-introduction.md + type: structural + title: Введение + - file: content/02-analysis.md + type: numbered + - file: content/03-design.md + type: numbered + - file: content/04-conclusion.md + type: structural + title: Заключение + - file: content/05-bibliography.md + type: bibliography + - file: content/06-appendix-a.md + type: appendix + letter: А + title: Сводные таблицы + - file: content/07-appendix-b.md + type: appendix + letter: Б + title: Вспомогательные материалы diff --git a/content/01-introduction.md b/content/01-introduction.md new file mode 100644 index 0000000..867fd17 --- /dev/null +++ b/content/01-introduction.md @@ -0,0 +1,14 @@ +Целью работы является разработка информационной системы учёта +заявок для отдела технической поддержки. Актуальность темы обусловлена ростом числа обращений и +необходимостью их оперативной обработки [@1]. + +Для достижения поставленной цели необходимо решить следующие задачи: + +- проанализировать предметную область и существующие решения; +- спроектировать архитектуру информационной системы; +- определить состав данных и пользовательских сценариев; +- рассчитать показатели эффективности разработанного решения. + +При выполнении работы использованы методы системного анализа, объектного моделирования и +статистической обработки данных [@2]. Требования к оформлению приняты в соответствии с +действующими стандартами [@3], [@4]. diff --git a/content/02-analysis.md b/content/02-analysis.md new file mode 100644 index 0000000..90b0c34 --- /dev/null +++ b/content/02-analysis.md @@ -0,0 +1,107 @@ +# АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ + +Раздел посвящён анализу процесса обработки обращений и обоснованию требований к информационной +системе. Рассматриваются участники процесса, информационные потоки и ограничения предметной +области. + +## Обзор процесса обработки обращений + +Процесс обработки заявки включает регистрацию, классификацию, назначение исполнителя и закрытие. +Схема процесса приведена на рисунке @fig:architecture. + +::: figure "Схема процесса обработки обращений" id=architecture file=images/architecture.png +а) обозначение информационного потока; +б) блоки этапов обработки. +::: + +Среднее время обработки одной заявки до внедрения системы составляло 46 минут. Распределение +обращений по категориям представлено в таблице @tbl:categories. + +::: table "Распределение обращений по категориям" id=categories +| Категория обращения | Количество, шт. | Доля, % | +|---|---|---| +| Консультация | 420 | 28,0 | +| Настройка оборудования | 315 | 21,0 | +| Восстановление доступа | 270 | 18,0 | +| Программная ошибка | 210 | 14,0 | +| Установка программного обеспечения | 135 | 9,0 | +| Сетевые подключения | 90 | 6,0 | +| Прочие обращения | 60 | 4,0 | +| Итого | 1500 | 100,0 | +::: + +Структура обращений по приоритетам приведена в таблице @tbl:priorities. + +::: table "Обращения по приоритетам" id=priorities +| Приоритет | Количество, шт. | Норматив реакции, мин | +|---|---|---| +| Критический | 75 | 15 | +| Высокий | 225 | 30 | +| Средний | 600 | 60 | +| Низкий | 600 | 120 | +| Итого | 1500 | — | +::: + +## Показатели эффективности + +Для оценки эффективности применяется коэффициент снижения трудозатрат, рассчитываемый по +формуле @eq:efficiency. + +::: formula id=efficiency +K = (T_base - T_new)/T_base +::: + +::: where +K — коэффициент снижения трудозатрат; +T_base — среднее время обработки заявки до внедрения, мин; +T_new — среднее время обработки заявки после внедрения, мин. +::: + +Значения исходных данных для расчёта приведены в таблице @tbl:baseline. + +::: table "Исходные данные для расчёта эффективности" id=baseline +| Показатель | Обозначение | Значение | +|---|---|---| +| Среднее время до внедрения, мин | T_base | 46 | +| Среднее время после внедрения, мин | T_new | 18 | +| Число обращений в месяц, шт. | N | 125 | +| Стоимость часа работы, руб. | C | 420 | +| Число рабочих часов в месяце, ч | H | 164 | +| Коэффициент накладных расходов | k | 0,35 | +| Годовой фонд рабочего времени, ч | F | 1970 | +| Нормативный срок окупаемости, лет | T_ok | 3 | +| Доля автоматизируемых операций | d | 0,72 | +| Коэффициент потерь рабочего времени | p | 0,08 | +::: + +## Обзор существующих решений + +Сравнительная характеристика существующих решений приведена в таблице @tbl:comparison. Анализ +выполнен на основе открытых источников [@5], [@6]. + +::: table "Сравнение существующих решений" id=comparison +| Критерий | Решение A | Решение B | Решение C | Разрабатываемое решение | +|---|---|---|---|---| +| Стоимость лицензии | Высокая | Средняя | Низкая | Низкая | +| Открытый исходный код | Нет | Да | Да | Да | +| Русскоязычный интерфейс | Да | Нет | Да | Да | +| Интеграция по API | Ограниченная | Полная | Частичная | Полная | +| Модуль отчётности | Да | Нет | Да | Да | +| Развёртывание локально | Только облако | Да | Да | Да | +| Поддержка ОС | Windows | Linux | Кроссплатформенная | Кроссплатформенная | +| Стоимость владения | Высокая | Низкая | Средняя | Низкая | +| Срок внедрения, мес | 6 | 4 | 3 | 2 | +| Техническая поддержка | Платная | Сообщество | Платная | Внутренняя | +::: + +::: note +Выбор решения выполнен по совокупности функциональных и экономических критериев. +::: + +## Термины и определения + +::: definitions +Заявка — обращение пользователя, зарегистрированное в информационной системе; +Исполнитель — сотрудник, назначенный ответственным за обработку заявки; +Регламент — документ, определяющий порядок и сроки обработки обращений. +::: diff --git a/content/03-design.md b/content/03-design.md new file mode 100644 index 0000000..adb7d69 --- /dev/null +++ b/content/03-design.md @@ -0,0 +1,126 @@ +# ПРОЕКТИРОВАНИЕ ИНФОРМАЦИОННОЙ СИСТЕМЫ + +В разделе описаны архитектура, модель данных и пользовательские сценарии разрабатываемой +информационной системы. + +## Архитектура системы + +Система построена по трёхуровневой архитектуре. Взаимодействие компонентов показано на +рисунке @fig:data-flow. + +::: figure "Схема информационных потоков" id=data-flow file=images/data-flow.png +а) направление передачи данных; +б) компоненты системы. +::: + +Состав программных модулей приведён в таблице @tbl:modules. + +::: table "Состав программных модулей" id=modules +| Модуль | Назначение | Технология | +|---|---|---| +| Веб-интерфейс | Отображение рабочих экранов оператора | React | +| API-шлюз | Обработка HTTP-запросов клиентов | Node.js | +| Модуль заявок | Жизненный цикл обращения | Node.js | +| Модуль уведомлений | Рассылка оповещений исполнителям | Node.js | +| Модуль отчётности | Формирование отчётов за период | Node.js | +| Служба аутентификации | Проверка учётных данных | JWT | +| Хранилище данных | Хранение заявок и справочников | PostgreSQL | +| Кэш | Ускорение чтения справочников | Redis | +| Очередь сообщений | Асинхронная обработка событий | RabbitMQ | +| Сборщик журналов | Централизованное логирование | ELK | +| Планировщик | Периодические задачи и отчёты | Cron | +| Модуль интеграции | Обмен данными с внешними системами | REST | +::: + +## Модель данных + +Основные сущности предметной области и их атрибуты приведены в таблице @tbl:entities. + +::: table "Основные сущности и атрибуты" id=entities +| Сущность | Атрибут | Тип | Обяз. | Описание | +|---|---|---|---|---| +| Заявка | id | Целое | Да | Уникальный идентификатор | +| Заявка | subject | Строка | Да | Тема обращения | +| Заявка | description | Текст | Да | Описание проблемы | +| Заявка | category_id | Целое | Да | Ссылка на категорию | +| Заявка | priority_id | Целое | Да | Ссылка на приоритет | +| Заявка | assignee_id | Целое | Нет | Ссылка на исполнителя | +| Заявка | created_at | Дата/время | Да | Момент регистрации | +| Заявка | closed_at | Дата/время | Нет | Момент закрытия | +| Категория | id | Целое | Да | Уникальный идентификатор | +| Категория | name | Строка | Да | Наименование категории | +| Приоритет | id | Целое | Да | Уникальный идентификатор | +| Приоритет | name | Строка | Да | Наименование приоритета | +| Приоритет | reaction_min | Целое | Да | Норматив реакции, мин | +| Пользователь | id | Целое | Да | Уникальный идентификатор | +| Пользователь | full_name | Строка | Да | Фамилия, имя, отчество | +::: + +Связи между сущностями описаны в таблице @tbl:relations. + +::: table "Связи между сущностями" id=relations +| Связь | Тип | Описание | +|---|---|---| +| Заявка — Категория | Многие к одному | Заявка относится к одной категории | +| Заявка — Приоритет | Многие к одному | Заявка имеет один приоритет | +| Заявка — Пользователь | Многие к одному | Заявка назначена одному исполнителю | +| Заявка — Комментарий | Один ко многим | Заявка содержит комментарии | +| Пользователь — Роль | Многие ко многим | Пользователь может иметь роли | +| Категория — Подкатегория | Один ко многим | Категория содержит подкатегории | +::: + +## Пользовательские сценарии + +Интерфейс списка заявок приведён на рисунке @fig:interface. + +::: figure "Интерфейс списка заявок" id=interface file=images/interface.png +а) область навигации; +б) табличное представление данных. +::: + +Зависимость трудозатрат от выбранной стратегии обработки показана на рисунке @fig:chart. + +::: figure "Зависимость трудозатрат от стратегии обработки" id=chart file=images/chart.png +Оценка получена по результатам пилотной эксплуатации. +::: + +Экономия времени за месяц вычисляется по формуле @eq:gain. + +::: formula id=gain +G = (T_base - T_new)*N +::: + +::: where +G — экономия времени за месяц, мин; +N — число обращений в месяц. +::: + +К системе предъявляются следующие требования: + +- функциональные: + 1. регистрация и классификация заявок; + 2. назначение исполнителя и контроль сроков; + 3. формирование отчётов за период. +- нефункциональные: + 1. время отклика не более двух секунд; + 2. одновременная работа не менее ста пользователей. + +## Особенности реализации + +При проектировании учтены ограничения, перечисленные в таблице @tbl:constraints. + +::: table "Ограничения и меры" id=constraints +| Ограничение | Влияние | Мера | +|---|---|---| +| Ограниченный бюджет | Сужение выбора технологий | Применение открытых библиотек | +| Сжатые сроки | Риск неполной реализации | Итеративная разработка | +| Нагрузка до 100 пользователей | Требования к производительности | Кэширование и индексы | +| Устаревшее оборудование | Ограничение ресурсов | Облегчённый веб-интерфейс | +| Разрозненные данные | Сложность интеграции | Единая модель данных | +| Требования безопасности | Дополнительные проверки | Аутентификация и журналирование | +::: + +::: notes +Оценка выполнена для среднего месяца пилотной эксплуатации. +Отклонение фактических значений не превышает десяти процентов. +::: diff --git a/content/04-conclusion.md b/content/04-conclusion.md new file mode 100644 index 0000000..c6705cf --- /dev/null +++ b/content/04-conclusion.md @@ -0,0 +1,12 @@ +В ходе работы проанализирована предметная область, спроектирована архитектура информационной +системы и определены показатели эффективности [@6], [@7]. + +Основные результаты работы: + +- выполнена классификация обращений и определены их приоритеты; +- разработана модель данных и описаны пользовательские сценарии; +- рассчитан коэффициент снижения трудозатрат. + +Применение информационной системы позволяет сократить среднее время обработки заявки с 46 до +18 минут, что подтверждает достижение поставленной цели. Единицы измерений приведены в +соответствии с действующим стандартом [@8]. diff --git a/content/05-bibliography.md b/content/05-bibliography.md new file mode 100644 index 0000000..ccf5cc5 --- /dev/null +++ b/content/05-bibliography.md @@ -0,0 +1,8 @@ +- ГОСТ 7.32-2017. Система стандартов по информации, библиотечному и издательскому делу. Отчёт о научно-исследовательской работе. Структура и правила оформления. — М. : Стандартинформ, 2017. — 32 с. +- ГОСТ Р 7.0.5-2008. Библиографическая ссылка. Общие требования и правила составления. — М. : Стандартинформ, 2008. — 23 с. +- Иванов, И. И. Управление ИТ-сервисами : учебное пособие / И. И. Иванов. — Самара : Изд-во Самарского университета, 2019. — 184 с. +- Петров, П. П. Обзор систем Service Desk / П. П. Петров // Информационные технологии. — 2020. — № 4. — С. 25–33. +- Смирнова, А. А. Практика внедрения ITSM-решений / А. А. Смирнова. — М. : ДМК Пресс, 2021. — 256 с. +- Сидоров, С. С. Оценка эффективности информационных систем / С. С. Сидоров // Вестник информатики. — 2018. — Т. 12, № 2. — С. 45–52. +- Открытые ITSM-системы [Электронный ресурс]. — URL: https://example.org/itsm (дата обращения: 01.02.2026). +- ГОСТ 8.417-2002. Государственная система обеспечения единства измерений. Единицы величин. — М. : Стандартинформ, 2002. — 32 с. diff --git a/content/06-appendix-a.md b/content/06-appendix-a.md new file mode 100644 index 0000000..2ca8fbc --- /dev/null +++ b/content/06-appendix-a.md @@ -0,0 +1,38 @@ +Сводные данные по обращениям за отчётный период приведены в таблице @tbl:app-a1. + +::: table "Сводные данные по обращениям" id=app-a1 +| Номер | Дата регистрации | Категория | Приоритет | Исполнитель | Статус | Время, мин | +|---|---|---|---|---|---|---| +| 1001 | 03.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 12 | +| 1002 | 03.02.2026 | Восстановление доступа | Высокий | Крылов И. И. | Закрыта | 24 | +| 1003 | 04.02.2026 | Программная ошибка | Критический | Никитин П. П. | Закрыта | 41 | +| 1004 | 04.02.2026 | Настройка оборудования | Средний | Орлова А. А. | Закрыта | 33 | +| 1005 | 05.02.2026 | Установка ПО | Низкий | Гусев Д. Д. | Закрыта | 18 | +| 1006 | 05.02.2026 | Сетевые подключения | Высокий | Крылов И. И. | Закрыта | 27 | +| 1007 | 06.02.2026 | Консультация | Низкий | Гусев Д. Д. | Закрыта | 9 | +| 1008 | 06.02.2026 | Программная ошибка | Критический | Никитин П. П. | Закрыта | 38 | +| 1009 | 07.02.2026 | Восстановление доступа | Средний | Орлова А. А. | Закрыта | 21 | +| 1010 | 07.02.2026 | Прочие обращения | Низкий | Гусев Д. Д. | Закрыта | 15 | +| 1011 | 10.02.2026 | Настройка оборудования | Высокий | Крылов И. И. | Закрыта | 30 | +| 1012 | 10.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 11 | +| 1013 | 11.02.2026 | Программная ошибка | Средний | Никитин П. П. | Закрыта | 35 | +| 1014 | 11.02.2026 | Сетевые подключения | Критический | Крылов И. И. | Закрыта | 44 | +| 1015 | 12.02.2026 | Установка ПО | Низкий | Гусев Д. Д. | Закрыта | 19 | +| 1016 | 12.02.2026 | Восстановление доступа | Высокий | Орлова А. А. | Закрыта | 26 | +| 1017 | 13.02.2026 | Консультация | Низкий | Гусев Д. Д. | Закрыта | 10 | +| 1018 | 13.02.2026 | Прочие обращения | Средний | Никитин П. П. | Закрыта | 29 | +| 1019 | 14.02.2026 | Настройка оборудования | Высокий | Крылов И. И. | Закрыта | 32 | +| 1020 | 14.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 13 | +::: + +Детализация по исполнителям приведена в таблице @tbl:app-a2. + +::: table "Детализация по исполнителям" id=app-a2 +| Исполнитель | Отдел | Закрыто заявок | Среднее время, мин | Доля, % | +|---|---|---|---|---| +| Орлова А. А. | Первая линия | 164 | 15 | 32,8 | +| Крылов И. И. | Вторая линия | 138 | 27 | 27,6 | +| Никитин П. П. | Вторая линия | 112 | 38 | 22,4 | +| Гусев Д. Д. | Первая линия | 86 | 14 | 17,2 | +| Итого | — | 500 | 23 | 100,0 | +::: diff --git a/content/07-appendix-b.md b/content/07-appendix-b.md new file mode 100644 index 0000000..8e65707 --- /dev/null +++ b/content/07-appendix-b.md @@ -0,0 +1,18 @@ +Схема размещения компонентов системы приведена на рисунке @fig:app-b1. + +::: figure "Схема размещения компонентов" id=app-b1 file=images/appendix-scheme.png +а) узлы развёртывания; +б) связи между узлами. +::: + +Параметры конфигурации стенда приведены в таблице @tbl:app-b1. + +::: table "Параметры конфигурации стенда" id=app-b1 +| Компонент | Процессор | Память, ГБ | Диск, ГБ | Назначение | +|---|---|---|---|---| +| Сервер приложений | 4 ядра | 8 | 40 | Выполнение API и логики | +| Сервер БД | 8 ядер | 16 | 200 | Хранение данных | +| Сервер кэша | 2 ядра | 4 | 20 | Кэширование справочников | +| Сервер очередей | 2 ядра | 4 | 20 | Асинхронные сообщения | +| Узел журналирования | 2 ядра | 8 | 100 | Сбор и хранение логов | +::: diff --git a/content/assets/images/appendix-scheme.png b/content/assets/images/appendix-scheme.png new file mode 100644 index 0000000..a054567 Binary files /dev/null and b/content/assets/images/appendix-scheme.png differ diff --git a/content/assets/images/architecture.png b/content/assets/images/architecture.png new file mode 100644 index 0000000..46d031f Binary files /dev/null and b/content/assets/images/architecture.png differ diff --git a/content/assets/images/chart.png b/content/assets/images/chart.png new file mode 100644 index 0000000..1f27bf3 Binary files /dev/null and b/content/assets/images/chart.png differ diff --git a/content/assets/images/data-flow.png b/content/assets/images/data-flow.png new file mode 100644 index 0000000..82da710 Binary files /dev/null and b/content/assets/images/data-flow.png differ diff --git a/content/assets/images/interface.png b/content/assets/images/interface.png new file mode 100644 index 0000000..5202296 Binary files /dev/null and b/content/assets/images/interface.png differ diff --git a/content/assets/title-page.docx b/content/assets/title-page.docx new file mode 100644 index 0000000..1edbc2e Binary files /dev/null and b/content/assets/title-page.docx differ diff --git a/documentation/STO_SGAU_02068410-004-2018.md b/documentation/STO_SGAU_02068410-004-2018.md new file mode 100644 index 0000000..f4fb062 --- /dev/null +++ b/documentation/STO_SGAU_02068410-004-2018.md @@ -0,0 +1,218 @@ +# Краткий гайд по оформлению учебных текстовых документов + +**Источник:** СТО 02068410-004-2018 «Общие требования к учебным текстовым документам» (Самарский университет, 2018). + +**Область применения:** обязателен для СПО; рекомендуется для высшего образования (бакалавриат, магистратура, специалитет). Распространяется на пояснительные записки, курсовые, контрольные, РГР, рефераты, ВКР и др. + +--- + +## 1. Общие параметры страницы и текста + +| Параметр | Требование | +|----------|------------| +| Формат | А4 (210×297 мм), одна сторона, белая бумага | +| Набор | Компьютерный (печать/графика) | +| Шрифт | Times New Roman, кегль 12–14 (высота знаков ≥ 1,8 мм) | +| Начертание | Полужирный **не применяется** | +| Цвет | Чёрный | +| Межстрочный интервал | 1,5 | +| Выравнивание | По ширине | +| Абзацный отступ | 1,25 см | +| Поля | слева ≥ 30 мм; справа ≥ 10 мм; сверху и снизу ≥ 20 мм | + +**Запрещено:** помарки, следы удалённого текста, повреждения листов. Незначительные опечатки допускается исправлять подчисткой/закрашиванием с переписыванием черными чернилами. + +**Страницы:** сквозная нумерация арабскими цифрами по центру внизу, без точки. Титульный лист входит в нумерацию, но номер на нём не ставится. + +**Разделы:** каждый структурный элемент — с новой страницы; разделы основной части рекомендуется начинать с новой страницы. + +--- + +## 2. Структура документа (порядок элементов) + +1. Титульный лист +2. Задание *(при наличии)* +3. Реферат +4. Содержание +5. Введение +6. **Основная часть** *(разделы, подразделы)* +7. Заключение +8. Определения, обозначения и сокращения *(при наличии)* +9. Список использованных источников *(при наличии)* +10. Приложения *(при наличии)* + +### Заголовки структурных элементов + +- По центру строки, **ПРОПИСНЫМИ** буквами, без точки в конце, без подчёркивания. +- **Не нумеруются** (РЕФЕРАТ, СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, ПРИЛОЖЕНИЕ и т.д.). + +### Нумерация разделов основной части + +| Уровень | Формат | Пример | +|---------|--------|--------| +| Раздел | арабская цифра, без точки в конце | `1 Заголовок раздела` | +| Подраздел | раздел.подраздел | `1.1 Заголовок` | +| Пункт | раздел.подраздел.пункт | `1.1.1` | +| Подпункт | … .подпункт | `1.1.1.1` | + +- Запись с абзацного отступа; после последней цифры номера точка **не ставится**. +- Заголовки разделов/подразделов: с прописной буквы, без точки, без переносов слов, не подчёркивать. +- Пункты и подпункты, как правило, **без заголовков**. +- Если в разделе один подраздел — он всё равно нумеруется. + +### Перечисления + +- Простой список: дефис `-`. +- Со ссылками в тексте: `а)`, `б)` … (без букв ё, з, й, о, ч, ь, ы, ъ). +- Вложенный уровень: `1)`, `2)` … +- Каждый элемент — с абзацного отступа. + +--- + +## 3. Содержание отдельных элементов + +### Титульный лист +- Первая страница; по возможности — на бланке университета/кафедры. +- Примеры — в **приложении А** СТО. + +### Реферат +- Объём текста реферата: **не более 850 знаков**. +- Содержит: число страниц, рисунков, таблиц, источников, приложений; сведения о графической части; **5–15 ключевых слов** (именительный падеж, прописные, через запятую); текст по объекту, цели, результатам, характеристикам, применению, эффективности (для ВКР). + +### Содержание +- Включает введение, все разделы/подразделы/пункты с названиями, заключение, определения, список источников, приложения — **с номерами страниц**. +- Наименования — строчными, с прописной буквы. +- Введение, заключение, список источников и приложения в содержании **не нумеруются** как разделы. + +### Введение и заключение +- **Введение:** актуальность, состояние проблемы, исходные данные, обоснование методики, новизна. +- **Заключение:** выводы, оценка результатов, рекомендации. + +### Определения и сокращения +- Столбцом: слева — термин/обозначение (по алфавиту), справа через тире — расшифровка и размерность. + +### Список источников +- По **ГОСТ 7.1** (полное описание) или **ГОСТ Р 7.0.5** (библиографические ссылки). +- Порядок — по упоминанию в тексте; нумерация арабскими цифрами **без точки**, с абзацного отступа. +- Примеры — в **приложении Г** СТО. + +--- + +## 4. Иллюстрации (рисунки) + +- Располагать **сразу после** первого упоминания в тексте, на следующей странице или в приложении. +- На каждый рисунок — **ссылка** в тексте («…в соответствии с рисунком 1…»). +- Нумерация: сквозная арабскими цифрами; при большом числе — в пределах раздела (`Рисунок 1.12`). +- Одна иллюстрация — `Рисунок 1`. +- Подпись **под** рисунком по центру: `Рисунок N — Наименование` (с прописной буквы, без точки в конце). +- Пояснительные данные (обозначения на рисунке) — над подписью. +- В приложениях: `Рисунок А.3` (буква приложения + номер). +- Фото меньше А4 — наклеить на лист А4. Допускается цветная печать, ручное исполнение. + +--- + +## 5. Таблицы + +- Размещать после первого упоминания или на следующей странице; на каждую таблицу — **ссылка** в тексте. +- Название **над** таблицей слева, без абзацного отступа: `Таблица N — Название` (без точки в конце). +- Нумерация: сквозная или в пределах раздела (`Таблица 6.4`); в приложениях — `Таблица Б.1`. +- Заголовки граф и строк — с прописной, в единственном числе; точки в конце заголовков граф **нет**. +- Таблица, как правило, ограничена линиями; головка отделена линией; диагонали в заголовках **не допускаются**. +- Допускается меньший кегль, чем в основном тексте; альбомная ориентация — по длинной стороне листа. +- Перенос на другую страницу: над продолжением — `Продолжение таблицы N` (без названия); головку/боковик повторять; нижнюю границу первой части не проводить. +- Пустые ячейки — прочерк; повторяющийся текст — кавычки или «То же» (для цифр/символов кавычки **нельзя**). +- Примечание к таблице — в конце таблицы, над нижней линией; **не должно содержать требований**. + +--- + +## 6. Формулы + +- На отдельной строке; сверху и снизу — **не менее одной пустой строки**. +- Перенос — после `=`, `+`, `-`, `×`, `:`; знак повторяется в начале следующей строки. Знак умножения — `×`, не `*` и не `•`. +- Расшифровка под формулой со слова **«где»** (без двоеточия), с размерностями. +- Нумерация справа в круглых скобках: `(1)`; в разделе — `(3.1)`; в приложении — `(Б.2)`. +- Ссылка в тексте: «…по формуле (2.1)…». +- Чёрный цвет; один способ набора в пределах одной формулы. + +--- + +## 7. Ссылки и примечания + +### Библиографические ссылки на источники +- В квадратных скобках по порядку упоминания: `[3]`. +- На фрагмент книги: `[10, с. 96]` или `[10, с. 96–98]` (ГОСТ Р 7.0.5). +- На стандарты в тексте — обозначение; год можно не указывать, если полное название есть в списке источников. +- **Не ссылаться** на подразделы/таблицы/рисунки **чужих** источников (только своего документа). + +### Примечания +- После относящегося материала; слово «Примечание» — с прописной, с абзаца. +- Одно: `Примечание — Текст…` +- Несколько: заголовок «Примечания», далее нумерованный список `1`, `2`… (без точки после номера). +- **Не содержат требований.** + +--- + +## 8. Приложения + +- После списка использованных источников; сквозная нумерация страниц. +- На каждое приложение — **ссылка** в тексте; порядок — по порядку ссылок. +- Обозначение: `ПРИЛОЖЕНИЕ А` (русский алфавит, без Ё, З, Й, О, Ч, Ъ, Ы, Ь); при исчерпании — латиница или цифры. +- Заголовок приложения — под словом «ПРИЛОЖЕНИЕ», по центру, с прописной буквы. +- Внутренняя нумерация разделов: `В.4` (буква приложения + номер). +- Таблицы и рисунки в приложениях нумеруются отдельно: `Таблица А.1`, `Рисунок Б.2`. + +**Что выносить в приложения:** громоздкие расчёты, большие таблицы/графики, массивы данных, описания приборов и методик, программы, вспомогательные иллюстрации. + +--- + +## 9. Требования к изложению текста + +- Стиль: краткий, точный, однозначный; научно-технические термины по стандартам. +- Для обязательных требований: «должен», «следует», «не допускается» и т.п.; для рекомендаций: «как правило», «при необходимости»; допустима повествовательная форма («применяют», «указывают»). + +**Не допускается в основном тексте** (кроме формул, таблиц, рисунков): +- разговорная речь, техницизмы, синонимы для одного понятия; +- иностранные слова при наличии русских аналогов; +- произвольные сокращения (кроме ГОСТ Р 7.0.12 и орфографических); +- знак «−» перед отрицательными числами (писать «минус»); +- знак Ø для диаметра в тексте (писать «диаметр»); +- математические знаки без чисел (`>`, `<`, `=`, `%`, `№` и т.д.). + +**Числа и единицы:** +- С единицами измерения — цифрами; от 1 до 9 без единиц — словами («пять труб», «12 м»). +- Единицы по **ГОСТ 8.417**; одна система в документе. +- Дроби — десятичные через **запятую**; диапазоны: `от 1 до 5 мм`. +- Единицу указывают после последнего числа в ряду: `1,50; 1,75; 2,00 м`. +- Не отрывать единицу от числа при переносе строки (кроме таблиц). + +--- + +## 10. Нормативные ссылки СТО (для углублённого оформления) + +| Документ | Назначение | +|----------|------------| +| ГОСТ 2.105-95 | Текстовые документы ЕСКД | +| ГОСТ 7.32-2001 | Структура отчёта о НИР | +| ГОСТ 7.9-95 | Реферат и аннотация | +| ГОСТ 7.1, ГОСТ Р 7.0.5, ГОСТ Р 7.0.12 | Библиография и сокращения | +| ГОСТ 8.417 | Единицы величин | +| ГОСТ 2.316 | Надписи на чертежах | + +--- + +## 11. Практический чеклист перед сдачей + +- [ ] А4, Times New Roman 12–14, интервал 1,5, поля по СТО +- [ ] Титульный лист по бланку (прил. А), номер страницы на нём скрыт +- [ ] Реферат ≤ 850 знаков, 5–15 ключевых слов +- [ ] Содержание с номерами страниц +- [ ] Разделы пронумерованы (`1`, `1.1`, `1.1.1`), структурные элементы — ЗАГЛАВНЫМИ по центру +- [ ] Все рисунки и таблицы имеют ссылки и подписи +- [ ] Формулы с расшифровкой «где» и нумерацией справа +- [ ] Список источников по ГОСТ, ссылки `[N]` в тексте +- [ ] Приложения после списка, с обозначениями А, Б, В… +- [ ] Сквозная нумерация страниц внизу по центру + +--- + +*Документ составлен по СТО 02068410-004-2018. Для титульного листа, задания, реферата и библиографии используйте обязательные приложения А, Б, В и справочное приложение Г исходного стандарта.* diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6927db3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2326 @@ +{ + "name": "lab-gen", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lab-gen", + "version": "2.0.0", + "dependencies": { + "docx": "^9.5.0", + "docx-merger": "^1.2.2", + "js-yaml": "^4.1.0", + "jszip": "^3.10.1", + "markdown-it": "^14.1.0" + }, + "bin": { + "lab-gen": "src/cli.js" + }, + "devDependencies": { + "@yao-pkg/pkg": "^6.23.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@roberts_lando/vfs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@roberts_lando/vfs/-/vfs-0.3.3.tgz", + "integrity": "sha512-YjkxVSLw5WMZQoARaryRAjcxA+GbBzWMJdwYZX5oLUt9cC/gew9as4Dn7tcLzPp7BPoR221VpTZ+78TRPawnjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@yao-pkg/pkg": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.23.0.tgz", + "integrity": "sha512-+d8iqRcmDxPpTE5fbSmsn1rShktIKvMcQGcFhwLdH15A4g2fBcRVLXppeQmu9fbF98bhYXjCOj6UTf3odXxWiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@roberts_lando/vfs": "^0.3.3", + "@yao-pkg/pkg-fetch": "3.6.6", + "esbuild": "^0.28.1", + "into-stream": "^9.1.0", + "multistream": "^4.1.0", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "postject": "^1.0.0-alpha.6", + "prebuild-install": "^7.1.1", + "resolve": "^1.22.10", + "resolve.exports": "^2.0.3", + "stream-meter": "^1.0.4", + "tar": "^7.5.7", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@yao-pkg/pkg-fetch": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.6.6.tgz", + "integrity": "sha512-Nu9ytHewcV6FuH4WvdpYZxz2XwiP2p30jlPBzbvwsS3OPYN7RRTyGrXhcmhS59O3AdXz/LW+4bozhV3alN+MEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^3.1.1", + "undici": "^7.28.0", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", + "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/docx": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz", + "integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx-merger": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/docx-merger/-/docx-merger-1.2.2.tgz", + "integrity": "sha512-hHfswb8pxq/zsY59N+Keigie4xhfgNe4rDCFTQtcVoKOLAgG5aEz1a1fkG8VuGPS2vqCuxS9x5hxW6i1SPBbSw==", + "license": "MIT", + "dependencies": { + "jszip": "^2.6.1", + "xmldom": "^0.1.27" + } + }, + "node_modules/docx-merger/node_modules/jszip": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.7.0.tgz", + "integrity": "sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==", + "license": "(MIT OR GPL-3.0)", + "dependencies": { + "pako": "~1.0.2" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/into-stream": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-9.1.0.tgz", + "integrity": "sha512-DRsRnQrbzdFjaQ1oe4C6/EIUymIOEix1qROEJTF9dbMq+M4Zrm6VaLp6SD/B9IsiEjPZuBSnWWFN+udajugdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.2.tgz", + "integrity": "sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.9.0", + "bare-fs": "^4.8.2", + "fast-fifo": "^1.3.2", + "streamx": "^2.28.1" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==", + "deprecated": "Deprecated due to CVE-2021-21366 resolved in 0.5.0", + "license": "(LGPL-2.0 or MIT)", + "engines": { + "node": ">=0.1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..84793dd --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "lab-gen", + "version": "2.0.0", + "private": true, + "description": "Автономный генератор отчётов DOCX из Markdown по СТО 02068410-004-2018", + "bin": { + "lab-gen": "src/cli.js" + }, + "main": "src/index.js", + "scripts": { + "generate": "node src/cli.js", + "check": "node src/cli.js --check", + "build:exe": "pkg . --targets node24-win-x64 --compress Brotli --output lab-gen.exe" + }, + "dependencies": { + "docx": "^9.5.0", + "docx-merger": "^1.2.2", + "js-yaml": "^4.1.0", + "jszip": "^3.10.1", + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@yao-pkg/pkg": "^6.23.0" + }, + "pkg": { + "scripts": [ + "src/**/*.js" + ], + "targets": [ + "node24-win-x64" + ] + } +} diff --git a/src/build.js b/src/build.js new file mode 100644 index 0000000..527dab6 --- /dev/null +++ b/src/build.js @@ -0,0 +1,53 @@ +'use strict'; + +const fs = require('fs'); +const { Packer } = require('docx'); + +const { createMarkdown, parseFile } = require('./markdown/parse'); +const { buildContext } = require('./render/styles'); +const { collectNumbers } = require('./render/references'); +const { createState, renderSections, buildDocx } = require('./render/document'); +const { mergeDocxBuffers } = require('./render/titlepage'); + +function loadSections(config, md) { + const sections = []; + for (const entry of config.sections) { + if (entry.type === 'toc') { + sections.push({ entry }); + continue; + } + const { meta, tokens } = parseFile(md, entry.file); + sections.push({ entry, meta, tokens }); + } + return sections; +} + +/** + * Собирает документ. При check=true не пишет docx, только проверяет ссылки. + * @returns {Promise<{state: object, buffer: Buffer|null}>} + */ +async function buildDocument(config, { check = false } = {}) { + const md = createMarkdown(); + const sections = loadSections(config, md); + const ctx = buildContext(config); + const state = createState(md, sections); + + collectNumbers(sections, ctx, state); + const children = renderSections(ctx, state); + + if (check) return { state, buffer: null }; + + const doc = buildDocx(ctx, children); + let buffer = await Packer.toBuffer(doc); + + const titlePage = config.document.titlePage; + if (titlePage && fs.existsSync(titlePage)) { + buffer = await mergeDocxBuffers(fs.readFileSync(titlePage), buffer); + } else if (titlePage) { + state.warnings.push(`Титульник не найден, пропущен: ${titlePage}`); + } + + return { state, buffer }; +} + +module.exports = { buildDocument, loadSections }; diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..e75da50 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const { loadConfig } = require('./config/load'); +const { validateConfig } = require('./config/validate'); +const { ensureScaffold } = require('./config/template'); +const { buildDocument } = require('./build'); +const { createLogger } = require('./util/logger'); +const { ConfigError } = require('./util/errors'); + +const VERSION = '2.0.0'; + +const HELP = `lab-gen ${VERSION} — генератор DOCX из Markdown (СТО 02068410-004-2018) + +Использование: + lab-gen [опции] + +Опции: + --config <путь> Путь к config.yaml (по умолчанию рядом с exe) + --content Каталог с config.yaml, content/ и assets/ (по умолчанию рядом с exe) + --output <путь> Переопределить путь выходного .docx + --check Только проверить конфиг и ссылки, без записи + --init Создать шаблон config.yaml/content/assets и выйти + --verbose Подробный вывод + --version Показать версию + --help Показать эту справку +`; + +function parseArgs(argv) { + const opts = { check: false, init: false, verbose: false }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + switch (arg) { + case '--config': + opts.config = argv[++i]; + break; + case '--content': + opts.content = argv[++i]; + break; + case '--output': + opts.output = argv[++i]; + break; + case '--check': + opts.check = true; + break; + case '--init': + opts.init = true; + break; + case '--verbose': + opts.verbose = true; + break; + case '--version': + opts.version = true; + break; + case '--help': + case '-h': + opts.help = true; + break; + default: + if (arg.startsWith('--')) { + throw new ConfigError(`Неизвестная опция: ${arg}`); + } + } + } + return opts; +} + +function defaultBaseDir() { + return process.pkg ? path.dirname(process.execPath) : process.cwd(); +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + const logger = createLogger({ verbose: opts.verbose }); + + if (opts.help) { + process.stdout.write(HELP); + return 0; + } + if (opts.version) { + process.stdout.write(VERSION + '\n'); + return 0; + } + + const baseDir = opts.content ? path.resolve(opts.content) : defaultBaseDir(); + const configPath = opts.config + ? path.resolve(opts.config) + : path.join(baseDir, 'config.yaml'); + + if (opts.init || !fs.existsSync(configPath)) { + const targetDir = path.dirname(configPath); + const created = await ensureScaffold(targetDir); + if (created.length) { + logger.info('Создан шаблон генератора:'); + for (const file of created) logger.info(' ' + path.relative(targetDir, file)); + } else { + logger.info('Файлы уже существуют, ничего не создано.'); + } + logger.info(''); + logger.info('Отредактируйте config.yaml и файлы в content/, затем запустите lab-gen снова.'); + return 0; + } + + const { config } = loadConfig(configPath); + if (opts.output) config.document.output = path.resolve(opts.output); + + const errors = validateConfig(config); + if (errors.length) { + logger.error('Конфигурация содержит ошибки:'); + for (const err of errors) logger.error(' - ' + err); + return 1; + } + + const { state, buffer } = await buildDocument(config, { check: opts.check }); + + for (const warning of state.warnings) logger.warn(warning); + + if (state.errors.length) { + logger.error('Найдены проблемы в контенте:'); + for (const err of state.errors) logger.error(' - ' + err); + return 1; + } + + if (opts.check) { + logger.info('Проверка пройдена, ошибок нет.'); + return 0; + } + + fs.mkdirSync(path.dirname(config.document.output), { recursive: true }); + fs.writeFileSync(config.document.output, buffer); + logger.info('Готово: ' + config.document.output); + return 0; +} + +main() + .then((code) => { + process.exitCode = code || 0; + }) + .catch((err) => { + if (err instanceof ConfigError) { + process.stderr.write('error: ' + err.message + '\n'); + } else { + process.stderr.write((err && err.stack ? err.stack : String(err)) + '\n'); + } + process.exitCode = 1; + }); diff --git a/src/config/defaults.js b/src/config/defaults.js new file mode 100644 index 0000000..bc92a0e --- /dev/null +++ b/src/config/defaults.js @@ -0,0 +1,123 @@ +'use strict'; + +/** + * Значения по умолчанию для config.yaml. + * Соответствуют оформлению СТО 02068410-004-2018. + */ +const DEFAULT_CONFIG = { + document: { + output: 'report.docx', + titlePage: 'content/assets/title-page.docx', + features: { updateFields: true }, + }, + + paths: { + assets: 'content/assets', + }, + + page: { + width: 11906, + height: 16838, + margins: { left: 1701, right: 851, top: 1134, bottom: 1134 }, + }, + + typography: { + font: 'Times New Roman', + sizeBody: 28, + sizeTable: 28, + sizeSmall: 24, + lineSpacing: 1.5, + indentFirst: 709, + color: '000000', + useBold: false, + allowEmphasis: false, + }, + + headings: { + struct: { spacingBefore: 360, spacingAfter: 560, align: 'center' }, + section: { spacingBefore: 920, spacingAfter: 0, align: 'left' }, + subsection: { + spacingBefore: 284, + spacingBeforeAfterSection: 240, + spacingAfter: 0, + align: 'left', + }, + point: { spacingBefore: 360, spacingAfter: 0 }, + }, + + captions: { + figure: { prefix: 'Рисунок', dash: '—', align: 'center' }, + table: { prefix: 'Таблица', dash: '—', align: 'left' }, + tableContinue: { prefix: 'Продолжение таблицы' }, + }, + + lists: { + bulletChar: '-', + letterSuffix: ')', + numberSuffix: ')', + forbiddenLetters: ['ё', 'з', 'й', 'о', 'ч', 'ь', 'ы', 'ъ'], + nestedLevelStep: 360, + }, + + numbering: { + figures: 'global', + tables: 'global', + formulas: 'global', + }, + + formula: { + spacingBeforeAfter: 360, + whereTabPosition: 720, + }, + + table: { + font: 'Times New Roman', + size: 28, + borderColor: '000000', + borderSize: 1, + cellMargins: { top: 57, bottom: 57, left: 113, right: 113 }, + headerFill: 'FFFFFF', + cellFill: 'FFFFFF', + }, + + figure: { + maxWidth: null, + maxHeight: 620, + }, + + pageNumber: { align: 'center', format: 'decimal', startFrom: 1 }, + + toc: { enabled: true, title: 'Содержание', depth: 3 }, + + bibliography: { title: 'Список использованных источников' }, + + citation: { bracketOpen: '[', bracketClose: ']', pagePrefix: 'с.' }, + + appendix: { + labelPrefix: 'ПРИЛОЖЕНИЕ', + forbiddenLetters: ['Ё', 'З', 'Й', 'О', 'Ч', 'Ъ', 'Ы', 'Ь'], + }, + + sections: [ + { type: 'toc' }, + { file: 'content/01-introduction.md', type: 'structural', title: 'Введение' }, + { file: 'content/02-analysis.md', type: 'numbered' }, + { file: 'content/03-design.md', type: 'numbered' }, + { file: 'content/04-conclusion.md', type: 'structural', title: 'Заключение' }, + { file: 'content/05-bibliography.md', type: 'bibliography' }, + { + file: 'content/06-appendix-a.md', + type: 'appendix', + letter: 'А', + title: 'Сводные таблицы', + }, + { + file: 'content/07-appendix-b.md', + type: 'appendix', + letter: 'Б', + title: 'Вспомогательные материалы', + }, + ], +}; + +module.exports = { DEFAULT_CONFIG }; diff --git a/src/config/load.js b/src/config/load.js new file mode 100644 index 0000000..58d6987 --- /dev/null +++ b/src/config/load.js @@ -0,0 +1,53 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const { DEFAULT_CONFIG } = require('./defaults'); +const { deepMerge } = require('../util/deepmerge'); +const { ConfigError } = require('../util/errors'); + +function resolveAgainst(baseDir, value) { + if (value === null || value === undefined || value === '') return null; + return path.resolve(baseDir, value); +} + +/** Все относительные пути в конфиге считаются от каталога самого config.yaml. */ +function resolvePaths(config, baseDir) { + config.document.output = resolveAgainst(baseDir, config.document.output); + config.document.titlePage = resolveAgainst(baseDir, config.document.titlePage); + config.paths.assets = resolveAgainst(baseDir, config.paths.assets) || baseDir; + + config.sections = (config.sections || []).map((entry) => { + if (!entry || typeof entry !== 'object') return entry; + if (entry.type === 'toc' || !entry.file) return { ...entry }; + return { ...entry, file: path.resolve(baseDir, entry.file) }; + }); +} + +function loadConfig(configPath) { + const abs = path.resolve(configPath); + if (!fs.existsSync(abs)) { + throw new ConfigError(`Файл конфигурации не найден: ${abs}`); + } + + let parsed; + try { + parsed = yaml.load(fs.readFileSync(abs, 'utf8')); + } catch (err) { + throw new ConfigError(`Ошибка разбора YAML (${abs}): ${err.message}`); + } + if (parsed === null || parsed === undefined) parsed = {}; + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new ConfigError(`Ожидался объект в корне config.yaml: ${abs}`); + } + + const baseDir = path.dirname(abs); + const config = deepMerge(DEFAULT_CONFIG, parsed); + resolvePaths(config, baseDir); + + return { config, baseDir, configPath: abs }; +} + +module.exports = { loadConfig, resolvePaths, resolveAgainst }; diff --git a/src/config/template.js b/src/config/template.js new file mode 100644 index 0000000..12574a3 --- /dev/null +++ b/src/config/template.js @@ -0,0 +1,405 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const { DEFAULT_CONFIG } = require('./defaults'); +const { diagramPng, chartPng } = require('../util/png'); +const { buildSampleTitlePage } = require('../render/sample-titlepage'); + +const CONFIG_HEADER = `# Конфигурация генератора lab-gen. +# Все относительные пути считаются от каталога этого файла. +# Любой ключ можно опустить — будет использовано значение по умолчанию. + +`; + +function defaultConfigYaml() { + return ( + CONFIG_HEADER + + yaml.dump(DEFAULT_CONFIG, { noRefs: true, lineWidth: -1, sortKeys: false }) + ); +} + +const SAMPLE = { + 'content/01-introduction.md': `Целью работы является разработка информационной системы учёта +заявок для отдела технической поддержки. Актуальность темы обусловлена ростом числа обращений и +необходимостью их оперативной обработки [@1]. + +Для достижения поставленной цели необходимо решить следующие задачи: + +- проанализировать предметную область и существующие решения; +- спроектировать архитектуру информационной системы; +- определить состав данных и пользовательских сценариев; +- рассчитать показатели эффективности разработанного решения. + +При выполнении работы использованы методы системного анализа, объектного моделирования и +статистической обработки данных [@2]. Требования к оформлению приняты в соответствии с +действующими стандартами [@3], [@4]. +`, + + 'content/02-analysis.md': `# АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ + +Раздел посвящён анализу процесса обработки обращений и обоснованию требований к информационной +системе. Рассматриваются участники процесса, информационные потоки и ограничения предметной +области. + +## Обзор процесса обработки обращений + +Процесс обработки заявки включает регистрацию, классификацию, назначение исполнителя и закрытие. +Схема процесса приведена на рисунке @fig:architecture. + +::: figure "Схема процесса обработки обращений" id=architecture file=images/architecture.png +а) обозначение информационного потока; +б) блоки этапов обработки. +::: + +Среднее время обработки одной заявки до внедрения системы составляло 46 минут. Распределение +обращений по категориям представлено в таблице @tbl:categories. + +::: table "Распределение обращений по категориям" id=categories +| Категория обращения | Количество, шт. | Доля, % | +|---|---|---| +| Консультация | 420 | 28,0 | +| Настройка оборудования | 315 | 21,0 | +| Восстановление доступа | 270 | 18,0 | +| Программная ошибка | 210 | 14,0 | +| Установка программного обеспечения | 135 | 9,0 | +| Сетевые подключения | 90 | 6,0 | +| Прочие обращения | 60 | 4,0 | +| Итого | 1500 | 100,0 | +::: + +Структура обращений по приоритетам приведена в таблице @tbl:priorities. + +::: table "Обращения по приоритетам" id=priorities +| Приоритет | Количество, шт. | Норматив реакции, мин | +|---|---|---| +| Критический | 75 | 15 | +| Высокий | 225 | 30 | +| Средний | 600 | 60 | +| Низкий | 600 | 120 | +| Итого | 1500 | — | +::: + +## Показатели эффективности + +Для оценки эффективности применяется коэффициент снижения трудозатрат, рассчитываемый по +формуле @eq:efficiency. + +::: formula id=efficiency +K = (T_base - T_new)/T_base +::: + +::: where +K — коэффициент снижения трудозатрат; +T_base — среднее время обработки заявки до внедрения, мин; +T_new — среднее время обработки заявки после внедрения, мин. +::: + +Значения исходных данных для расчёта приведены в таблице @tbl:baseline. + +::: table "Исходные данные для расчёта эффективности" id=baseline +| Показатель | Обозначение | Значение | +|---|---|---| +| Среднее время до внедрения, мин | T_base | 46 | +| Среднее время после внедрения, мин | T_new | 18 | +| Число обращений в месяц, шт. | N | 125 | +| Стоимость часа работы, руб. | C | 420 | +| Число рабочих часов в месяце, ч | H | 164 | +| Коэффициент накладных расходов | k | 0,35 | +| Годовой фонд рабочего времени, ч | F | 1970 | +| Нормативный срок окупаемости, лет | T_ok | 3 | +| Доля автоматизируемых операций | d | 0,72 | +| Коэффициент потерь рабочего времени | p | 0,08 | +::: + +## Обзор существующих решений + +Сравнительная характеристика существующих решений приведена в таблице @tbl:comparison. Анализ +выполнен на основе открытых источников [@5], [@6]. + +::: table "Сравнение существующих решений" id=comparison +| Критерий | Решение A | Решение B | Решение C | Разрабатываемое решение | +|---|---|---|---|---| +| Стоимость лицензии | Высокая | Средняя | Низкая | Низкая | +| Открытый исходный код | Нет | Да | Да | Да | +| Русскоязычный интерфейс | Да | Нет | Да | Да | +| Интеграция по API | Ограниченная | Полная | Частичная | Полная | +| Модуль отчётности | Да | Нет | Да | Да | +| Развёртывание локально | Только облако | Да | Да | Да | +| Поддержка ОС | Windows | Linux | Кроссплатформенная | Кроссплатформенная | +| Стоимость владения | Высокая | Низкая | Средняя | Низкая | +| Срок внедрения, мес | 6 | 4 | 3 | 2 | +| Техническая поддержка | Платная | Сообщество | Платная | Внутренняя | +::: + +::: note +Выбор решения выполнен по совокупности функциональных и экономических критериев. +::: + +## Термины и определения + +::: definitions +Заявка — обращение пользователя, зарегистрированное в информационной системе; +Исполнитель — сотрудник, назначенный ответственным за обработку заявки; +Регламент — документ, определяющий порядок и сроки обработки обращений. +::: +`, + + 'content/03-design.md': `# ПРОЕКТИРОВАНИЕ ИНФОРМАЦИОННОЙ СИСТЕМЫ + +В разделе описаны архитектура, модель данных и пользовательские сценарии разрабатываемой +информационной системы. + +## Архитектура системы + +Система построена по трёхуровневой архитектуре. Взаимодействие компонентов показано на +рисунке @fig:data-flow. + +::: figure "Схема информационных потоков" id=data-flow file=images/data-flow.png +а) направление передачи данных; +б) компоненты системы. +::: + +Состав программных модулей приведён в таблице @tbl:modules. + +::: table "Состав программных модулей" id=modules +| Модуль | Назначение | Технология | +|---|---|---| +| Веб-интерфейс | Отображение рабочих экранов оператора | React | +| API-шлюз | Обработка HTTP-запросов клиентов | Node.js | +| Модуль заявок | Жизненный цикл обращения | Node.js | +| Модуль уведомлений | Рассылка оповещений исполнителям | Node.js | +| Модуль отчётности | Формирование отчётов за период | Node.js | +| Служба аутентификации | Проверка учётных данных | JWT | +| Хранилище данных | Хранение заявок и справочников | PostgreSQL | +| Кэш | Ускорение чтения справочников | Redis | +| Очередь сообщений | Асинхронная обработка событий | RabbitMQ | +| Сборщик журналов | Централизованное логирование | ELK | +| Планировщик | Периодические задачи и отчёты | Cron | +| Модуль интеграции | Обмен данными с внешними системами | REST | +::: + +## Модель данных + +Основные сущности предметной области и их атрибуты приведены в таблице @tbl:entities. + +::: table "Основные сущности и атрибуты" id=entities +| Сущность | Атрибут | Тип | Обяз. | Описание | +|---|---|---|---|---| +| Заявка | id | Целое | Да | Уникальный идентификатор | +| Заявка | subject | Строка | Да | Тема обращения | +| Заявка | description | Текст | Да | Описание проблемы | +| Заявка | category_id | Целое | Да | Ссылка на категорию | +| Заявка | priority_id | Целое | Да | Ссылка на приоритет | +| Заявка | assignee_id | Целое | Нет | Ссылка на исполнителя | +| Заявка | created_at | Дата/время | Да | Момент регистрации | +| Заявка | closed_at | Дата/время | Нет | Момент закрытия | +| Категория | id | Целое | Да | Уникальный идентификатор | +| Категория | name | Строка | Да | Наименование категории | +| Приоритет | id | Целое | Да | Уникальный идентификатор | +| Приоритет | name | Строка | Да | Наименование приоритета | +| Приоритет | reaction_min | Целое | Да | Норматив реакции, мин | +| Пользователь | id | Целое | Да | Уникальный идентификатор | +| Пользователь | full_name | Строка | Да | Фамилия, имя, отчество | +::: + +Связи между сущностями описаны в таблице @tbl:relations. + +::: table "Связи между сущностями" id=relations +| Связь | Тип | Описание | +|---|---|---| +| Заявка — Категория | Многие к одному | Заявка относится к одной категории | +| Заявка — Приоритет | Многие к одному | Заявка имеет один приоритет | +| Заявка — Пользователь | Многие к одному | Заявка назначена одному исполнителю | +| Заявка — Комментарий | Один ко многим | Заявка содержит комментарии | +| Пользователь — Роль | Многие ко многим | Пользователь может иметь роли | +| Категория — Подкатегория | Один ко многим | Категория содержит подкатегории | +::: + +## Пользовательские сценарии + +Интерфейс списка заявок приведён на рисунке @fig:interface. + +::: figure "Интерфейс списка заявок" id=interface file=images/interface.png +а) область навигации; +б) табличное представление данных. +::: + +Зависимость трудозатрат от выбранной стратегии обработки показана на рисунке @fig:chart. + +::: figure "Зависимость трудозатрат от стратегии обработки" id=chart file=images/chart.png +Оценка получена по результатам пилотной эксплуатации. +::: + +Экономия времени за месяц вычисляется по формуле @eq:gain. + +::: formula id=gain +G = (T_base - T_new)*N +::: + +::: where +G — экономия времени за месяц, мин; +N — число обращений в месяц. +::: + +К системе предъявляются следующие требования: + +- функциональные: + 1. регистрация и классификация заявок; + 2. назначение исполнителя и контроль сроков; + 3. формирование отчётов за период. +- нефункциональные: + 1. время отклика не более двух секунд; + 2. одновременная работа не менее ста пользователей. + +## Особенности реализации + +При проектировании учтены ограничения, перечисленные в таблице @tbl:constraints. + +::: table "Ограничения и меры" id=constraints +| Ограничение | Влияние | Мера | +|---|---|---| +| Ограниченный бюджет | Сужение выбора технологий | Применение открытых библиотек | +| Сжатые сроки | Риск неполной реализации | Итеративная разработка | +| Нагрузка до 100 пользователей | Требования к производительности | Кэширование и индексы | +| Устаревшее оборудование | Ограничение ресурсов | Облегчённый веб-интерфейс | +| Разрозненные данные | Сложность интеграции | Единая модель данных | +| Требования безопасности | Дополнительные проверки | Аутентификация и журналирование | +::: + +::: notes +Оценка выполнена для среднего месяца пилотной эксплуатации. +Отклонение фактических значений не превышает десяти процентов. +::: +`, + + 'content/04-conclusion.md': `В ходе работы проанализирована предметная область, спроектирована архитектура информационной +системы и определены показатели эффективности [@6], [@7]. + +Основные результаты работы: + +- выполнена классификация обращений и определены их приоритеты; +- разработана модель данных и описаны пользовательские сценарии; +- рассчитан коэффициент снижения трудозатрат. + +Применение информационной системы позволяет сократить среднее время обработки заявки с 46 до +18 минут, что подтверждает достижение поставленной цели. Единицы измерений приведены в +соответствии с действующим стандартом [@8]. +`, + + 'content/05-bibliography.md': `- ГОСТ 7.32-2017. Система стандартов по информации, библиотечному и издательскому делу. Отчёт о научно-исследовательской работе. Структура и правила оформления. — М. : Стандартинформ, 2017. — 32 с. +- ГОСТ Р 7.0.5-2008. Библиографическая ссылка. Общие требования и правила составления. — М. : Стандартинформ, 2008. — 23 с. +- Иванов, И. И. Управление ИТ-сервисами : учебное пособие / И. И. Иванов. — Самара : Изд-во Самарского университета, 2019. — 184 с. +- Петров, П. П. Обзор систем Service Desk / П. П. Петров // Информационные технологии. — 2020. — № 4. — С. 25–33. +- Смирнова, А. А. Практика внедрения ITSM-решений / А. А. Смирнова. — М. : ДМК Пресс, 2021. — 256 с. +- Сидоров, С. С. Оценка эффективности информационных систем / С. С. Сидоров // Вестник информатики. — 2018. — Т. 12, № 2. — С. 45–52. +- Открытые ITSM-системы [Электронный ресурс]. — URL: https://example.org/itsm (дата обращения: 01.02.2026). +- ГОСТ 8.417-2002. Государственная система обеспечения единства измерений. Единицы величин. — М. : Стандартинформ, 2002. — 32 с. +`, + + 'content/06-appendix-a.md': `Сводные данные по обращениям за отчётный период приведены в таблице @tbl:app-a1. + +::: table "Сводные данные по обращениям" id=app-a1 +| Номер | Дата регистрации | Категория | Приоритет | Исполнитель | Статус | Время, мин | +|---|---|---|---|---|---|---| +| 1001 | 03.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 12 | +| 1002 | 03.02.2026 | Восстановление доступа | Высокий | Крылов И. И. | Закрыта | 24 | +| 1003 | 04.02.2026 | Программная ошибка | Критический | Никитин П. П. | Закрыта | 41 | +| 1004 | 04.02.2026 | Настройка оборудования | Средний | Орлова А. А. | Закрыта | 33 | +| 1005 | 05.02.2026 | Установка ПО | Низкий | Гусев Д. Д. | Закрыта | 18 | +| 1006 | 05.02.2026 | Сетевые подключения | Высокий | Крылов И. И. | Закрыта | 27 | +| 1007 | 06.02.2026 | Консультация | Низкий | Гусев Д. Д. | Закрыта | 9 | +| 1008 | 06.02.2026 | Программная ошибка | Критический | Никитин П. П. | Закрыта | 38 | +| 1009 | 07.02.2026 | Восстановление доступа | Средний | Орлова А. А. | Закрыта | 21 | +| 1010 | 07.02.2026 | Прочие обращения | Низкий | Гусев Д. Д. | Закрыта | 15 | +| 1011 | 10.02.2026 | Настройка оборудования | Высокий | Крылов И. И. | Закрыта | 30 | +| 1012 | 10.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 11 | +| 1013 | 11.02.2026 | Программная ошибка | Средний | Никитин П. П. | Закрыта | 35 | +| 1014 | 11.02.2026 | Сетевые подключения | Критический | Крылов И. И. | Закрыта | 44 | +| 1015 | 12.02.2026 | Установка ПО | Низкий | Гусев Д. Д. | Закрыта | 19 | +| 1016 | 12.02.2026 | Восстановление доступа | Высокий | Орлова А. А. | Закрыта | 26 | +| 1017 | 13.02.2026 | Консультация | Низкий | Гусев Д. Д. | Закрыта | 10 | +| 1018 | 13.02.2026 | Прочие обращения | Средний | Никитин П. П. | Закрыта | 29 | +| 1019 | 14.02.2026 | Настройка оборудования | Высокий | Крылов И. И. | Закрыта | 32 | +| 1020 | 14.02.2026 | Консультация | Низкий | Орлова А. А. | Закрыта | 13 | +::: + +Детализация по исполнителям приведена в таблице @tbl:app-a2. + +::: table "Детализация по исполнителям" id=app-a2 +| Исполнитель | Отдел | Закрыто заявок | Среднее время, мин | Доля, % | +|---|---|---|---|---| +| Орлова А. А. | Первая линия | 164 | 15 | 32,8 | +| Крылов И. И. | Вторая линия | 138 | 27 | 27,6 | +| Никитин П. П. | Вторая линия | 112 | 38 | 22,4 | +| Гусев Д. Д. | Первая линия | 86 | 14 | 17,2 | +| Итого | — | 500 | 23 | 100,0 | +::: +`, + + 'content/07-appendix-b.md': `Схема размещения компонентов системы приведена на рисунке @fig:app-b1. + +::: figure "Схема размещения компонентов" id=app-b1 file=images/appendix-scheme.png +а) узлы развёртывания; +б) связи между узлами. +::: + +Параметры конфигурации стенда приведены в таблице @tbl:app-b1. + +::: table "Параметры конфигурации стенда" id=app-b1 +| Компонент | Процессор | Память, ГБ | Диск, ГБ | Назначение | +|---|---|---|---|---| +| Сервер приложений | 4 ядра | 8 | 40 | Выполнение API и логики | +| Сервер БД | 8 ядер | 16 | 200 | Хранение данных | +| Сервер кэша | 2 ядра | 4 | 20 | Кэширование справочников | +| Сервер очередей | 2 ядра | 4 | 20 | Асинхронные сообщения | +| Узел журналирования | 2 ядра | 8 | 100 | Сбор и хранение логов | +::: +`, +}; + +const IMAGES = { + 'content/assets/images/architecture.png': () => diagramPng(900, 420, [46, 94, 170]), + 'content/assets/images/data-flow.png': () => diagramPng(900, 380, [28, 140, 96]), + 'content/assets/images/interface.png': () => diagramPng(820, 480, [196, 120, 32]), + 'content/assets/images/chart.png': () => chartPng(820, 420, [122, 70, 190]), + 'content/assets/images/appendix-scheme.png': () => diagramPng(900, 520, [176, 48, 64]), +}; + +function scaffoldFiles() { + const files = { 'config.yaml': defaultConfigYaml() }; + for (const [name, content] of Object.entries(SAMPLE)) files[name] = content; + for (const [name, make] of Object.entries(IMAGES)) { + files[name] = { binary: make() }; + } + return files; +} + +/** Создаёт config.yaml, примеры content/ и ассеты, если их ещё нет. */ +async function ensureScaffold(baseDir) { + const files = scaffoldFiles(); + const created = []; + for (const [name, content] of Object.entries(files)) { + const target = path.join(baseDir, name); + if (fs.existsSync(target)) continue; + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (content && typeof content === 'object' && content.binary) { + fs.writeFileSync(target, content.binary); + } else { + fs.writeFileSync(target, content, 'utf8'); + } + created.push(target); + } + + const titlePage = path.join(baseDir, 'content', 'assets', 'title-page.docx'); + if (!fs.existsSync(titlePage)) { + fs.mkdirSync(path.dirname(titlePage), { recursive: true }); + fs.writeFileSync(titlePage, await buildSampleTitlePage()); + created.push(titlePage); + } + return created; +} + +module.exports = { defaultConfigYaml, scaffoldFiles, ensureScaffold, SAMPLE }; diff --git a/src/config/validate.js b/src/config/validate.js new file mode 100644 index 0000000..e48b4ae --- /dev/null +++ b/src/config/validate.js @@ -0,0 +1,95 @@ +'use strict'; + +const fs = require('fs'); + +const SECTION_TYPES = ['toc', 'structural', 'numbered', 'bibliography', 'appendix']; +const NUMBERING_SCOPES = ['global', 'section']; + +function isPositiveInt(value) { + return Number.isInteger(value) && value > 0; +} + +function validateConfig(config) { + const errors = []; + + const { document: doc, typography, page, toc, appendix, numbering } = config; + + if (!Array.isArray(config.sections) || config.sections.length === 0) { + errors.push('sections: должен быть непустой список частей документа'); + } else { + const letters = new Set(); + config.sections.forEach((entry, index) => { + const where = `sections[${index}]`; + if (!entry || typeof entry !== 'object') { + errors.push(`${where}: ожидался объект с полем type`); + return; + } + if (!SECTION_TYPES.includes(entry.type)) { + errors.push( + `${where}: неизвестный type "${entry.type}" (допустимо: ${SECTION_TYPES.join(', ')})` + ); + return; + } + if (entry.type === 'toc') { + if (entry.file) errors.push(`${where}: часть type=toc не должна иметь file`); + return; + } + if (!entry.file) { + errors.push(`${where}: отсутствует file`); + } else if (!fs.existsSync(entry.file)) { + errors.push(`${where}: файл не найден: ${entry.file}`); + } + if (entry.type === 'structural' && !entry.title) { + errors.push(`${where}: для structural требуется title`); + } + if (entry.type === 'appendix') { + if (!entry.letter || String(entry.letter).length !== 1) { + errors.push(`${where}: для appendix требуется одиночная буква в letter`); + } else { + const letter = String(entry.letter).toUpperCase(); + if (appendix.forbiddenLetters.map((l) => l.toUpperCase()).includes(letter)) { + errors.push(`${where}: буква приложения "${letter}" запрещена СТО`); + } + if (letters.has(letter)) errors.push(`${where}: повтор буквы приложения "${letter}"`); + letters.add(letter); + } + } + }); + } + + if (doc.titlePage && !fs.existsSync(doc.titlePage)) { + errors.push(`document.titlePage: файл не найден: ${doc.titlePage}`); + } + + for (const key of ['sizeBody', 'sizeTable', 'sizeSmall']) { + if (!isPositiveInt(typography[key])) { + errors.push(`typography.${key}: ожидалось положительное целое (half-points)`); + } + } + if (!(typeof typography.lineSpacing === 'number' && typography.lineSpacing > 0)) { + errors.push('typography.lineSpacing: ожидалось положительное число'); + } + + for (const [key, value] of Object.entries(page.margins)) { + if (!Number.isInteger(value) || value < 0) { + errors.push(`page.margins.${key}: ожидалось неотрицательное целое (twips)`); + } + } + + if (!isPositiveInt(toc.depth) || toc.depth > 3) { + errors.push('toc.depth: ожидалось 1..3'); + } + if (typeof toc.enabled !== 'boolean') { + errors.push('toc.enabled: ожидалось true/false'); + } + + for (const key of ['figures', 'tables', 'formulas']) { + if (!NUMBERING_SCOPES.includes(numbering[key])) { + errors.push(`numbering.${key}: допустимо ${NUMBERING_SCOPES.join(' | ')}`); + } + } + + return errors; +} + +module.exports = { validateConfig, SECTION_TYPES }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..be0e4f2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = { ...require('./build'), ...require('./config/load') }; diff --git a/src/markdown/container.js b/src/markdown/container.js new file mode 100644 index 0000000..c000dab --- /dev/null +++ b/src/markdown/container.js @@ -0,0 +1,103 @@ +'use strict'; + +const NAME_RE = /^([A-Za-z][\w-]*)/; + +/** Разбирает строку после имени директивы на позиционный title и key=value. */ +function parseDirectiveAttrs(text) { + const attrs = {}; + let positionalIndex = 0; + let i = 0; + const n = text.length; + + const readValue = () => { + if (text[i] === '"' || text[i] === "'") { + const quote = text[i]; + i += 1; + const start = i; + while (i < n && text[i] !== quote) i += 1; + const value = text.slice(start, i); + i += 1; + return value; + } + const start = i; + while (i < n && !/\s/.test(text[i])) i += 1; + return text.slice(start, i); + }; + + while (i < n) { + while (i < n && /\s/.test(text[i])) i += 1; + if (i >= n) break; + + let token; + if (text[i] === '"' || text[i] === "'") { + token = readValue(); + } else { + const start = i; + while (i < n && !/\s/.test(text[i]) && text[i] !== '=') i += 1; + token = text.slice(start, i); + } + + if (text[i] === '=') { + i += 1; + const value = readValue(); + if (token) attrs[token] = value; + } else if (token.length) { + if (positionalIndex === 0) attrs.title = token; + positionalIndex += 1; + } + } + return attrs; +} + +function parseFirstLine(line) { + const trimmed = line.replace(/^:::\s*/, ''); + const nameMatch = trimmed.match(NAME_RE); + if (!nameMatch) return null; + const name = nameMatch[1].toLowerCase(); + const rest = trimmed.slice(nameMatch[0].length); + return { name, attrs: parseDirectiveAttrs(rest) }; +} + +function isClosing(line) { + return /^:::\s*$/.test(line); +} + +function directiveRule(state, startLine, endLine, silent) { + const lineStart = state.bMarks[startLine] + state.tShift[startLine]; + const lineEnd = state.eMarks[startLine]; + const firstLine = state.src.slice(lineStart, lineEnd); + + if (!/^:::\s*[A-Za-z]/.test(firstLine)) return false; + if (silent) return true; + + const parsed = parseFirstLine(firstLine); + if (!parsed) return false; + + const bodyLines = []; + let nextLine = startLine + 1; + for (; nextLine < endLine; nextLine++) { + const s = state.bMarks[nextLine] + state.tShift[nextLine]; + const e = state.eMarks[nextLine]; + const text = state.src.slice(s, e); + if (isClosing(text.trim())) break; + bodyLines.push(text); + } + const hasClosing = nextLine < endLine; + const body = bodyLines.join('\n'); + + const token = state.push('directive', 'div', 0); + token.info = { name: parsed.name, attrs: parsed.attrs, body }; + token.block = true; + token.map = [startLine, hasClosing ? nextLine + 1 : nextLine]; + + state.line = hasClosing ? nextLine + 1 : nextLine; + return true; +} + +function containerPlugin(md) { + md.block.ruler.before('fence', 'directive', directiveRule, { + alt: ['paragraph', 'blockquote', 'list'], + }); +} + +module.exports = { containerPlugin, parseDirectiveAttrs, parseFirstLine }; diff --git a/src/markdown/inline.js b/src/markdown/inline.js new file mode 100644 index 0000000..eee2097 --- /dev/null +++ b/src/markdown/inline.js @@ -0,0 +1,37 @@ +'use strict'; + +const CITATION_RE = /^\[@(\d+)(?:\s*,\s*с\.\s*([^\]]+))?\]/; +const XREF_RE = /^@(fig|tbl|eq):([A-Za-z0-9_-]+)/; + +function citationRule(state, silent) { + if (state.src[state.pos] !== '[' || state.src[state.pos + 1] !== '@') return false; + const match = state.src.slice(state.pos).match(CITATION_RE); + if (!match) return false; + if (!silent) { + const token = state.push('citation', '', 0); + token.content = match[0]; + token.meta = { n: Number(match[1]), page: match[2] || null }; + } + state.pos += match[0].length; + return true; +} + +function xrefRule(state, silent) { + if (state.src[state.pos] !== '@') return false; + const match = state.src.slice(state.pos).match(XREF_RE); + if (!match) return false; + if (!silent) { + const token = state.push('xref', '', 0); + token.content = match[0]; + token.meta = { kind: match[1], label: match[2] }; + } + state.pos += match[0].length; + return true; +} + +function inlinePlugin(md) { + md.inline.ruler.before('link', 'citation', citationRule); + md.inline.ruler.before('link', 'xref', xrefRule); +} + +module.exports = { inlinePlugin, CITATION_RE, XREF_RE }; diff --git a/src/markdown/parse.js b/src/markdown/parse.js new file mode 100644 index 0000000..ada351e --- /dev/null +++ b/src/markdown/parse.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); +const MarkdownIt = require('markdown-it'); +const yaml = require('js-yaml'); + +const { containerPlugin } = require('./container'); +const { inlinePlugin } = require('./inline'); + +function createMarkdown() { + const md = new MarkdownIt({ + html: false, + linkify: false, + typographer: false, + breaks: false, + }); + md.use(containerPlugin); + md.use(inlinePlugin); + return md; +} + +/** Необязательный YAML front matter в начале файла. */ +function splitFrontMatter(text) { + const normalized = text.replace(/^\uFEFF/, ''); + const match = normalized.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!match) return { meta: {}, body: normalized }; + let meta = {}; + try { + meta = yaml.load(match[1]) || {}; + } catch { + meta = {}; + } + return { meta, body: normalized.slice(match[0].length) }; +} + +function parseMarkdown(md, text, env = {}) { + return md.parse(text, env); +} + +function parseFile(md, filePath) { + const raw = fs.readFileSync(filePath, 'utf8'); + const { meta, body } = splitFrontMatter(raw); + const tokens = md.parse(body, {}); + return { meta, body, tokens }; +} + +module.exports = { createMarkdown, parseMarkdown, parseFile, splitFrontMatter }; diff --git a/src/render/document.js b/src/render/document.js new file mode 100644 index 0000000..e97b614 --- /dev/null +++ b/src/render/document.js @@ -0,0 +1,452 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { Document, ExternalHyperlink, TextRun } = require('docx'); + +const { + pageBreak, + paraRuns, + textRun, + sectionHeading, + subsectionHeading, + plainHeading, + pointParagraph, + structSection, + appendixStart, + tocSection, + figureBlock, + tableSto, + formulaFromText, + whereBlock, + noteSingle, + notesBlock, + definitionsBlock, + bibliographyList, + listParagraphRuns, + nextListInstance, + resetListInstances, + defaultSectionProps, + pageNumberFooter, + REFS, +} = require('./primitives'); + +function createState(md, sections) { + return { + md, + sections, + labelMap: new Map(), + biblioCount: 0, + errors: [], + warnings: [], + }; +} + +function firstInlineText(tokens) { + for (const token of tokens) { + if (token.type === 'inline' && String(token.content || '').trim()) { + return token.content.trim(); + } + } + return ''; +} + +// ── inline ──────────────────────────────────────────────────────── + +function citationRun(ctx, meta, state) { + const { bracketOpen, bracketClose, pagePrefix } = ctx.config.citation; + if (meta.n < 1 || (state.biblioCount > 0 && meta.n > state.biblioCount)) { + state.errors.push( + `Ссылка [@${meta.n}] выходит за пределы списка источников (${state.biblioCount})` + ); + return textRun(ctx, `[@${meta.n}]`); + } + const text = meta.page + ? `${bracketOpen}${meta.n}, ${pagePrefix} ${meta.page}${bracketClose}` + : `${bracketOpen}${meta.n}${bracketClose}`; + return textRun(ctx, text); +} + +function xrefRun(ctx, meta, state) { + const key = `${meta.kind}:${meta.label}`; + const value = state.labelMap.get(key); + if (value == null) { + state.errors.push(`Не найдена метка @${key}`); + return textRun(ctx, `@${key}`); + } + return textRun(ctx, String(value)); +} + +function renderInline(ctx, inlineToken, state) { + const runs = []; + const flags = { bold: false, italics: false }; + const linkStack = []; + if (!inlineToken || !inlineToken.children) return runs; + + const push = (run) => { + if (linkStack.length) linkStack[linkStack.length - 1].children.push(run); + else runs.push(run); + }; + const closeLink = () => { + const link = linkStack.pop(); + const hyper = new ExternalHyperlink({ children: link.children, link: link.href }); + if (linkStack.length) linkStack[linkStack.length - 1].children.push(hyper); + else runs.push(hyper); + }; + + for (const child of inlineToken.children) { + switch (child.type) { + case 'text': + push(textRun(ctx, child.content, flags)); + break; + case 'softbreak': + push(textRun(ctx, ' ')); + break; + case 'hardbreak': + push(new TextRun({ break: 1, font: ctx.typography.font, size: ctx.typography.sizeBody })); + break; + case 'strong_open': + flags.bold = true; + break; + case 'strong_close': + flags.bold = false; + break; + case 'em_open': + flags.italics = true; + break; + case 'em_close': + flags.italics = false; + break; + case 's_open': + case 's_close': + break; + case 'code_inline': + push(textRun(ctx, child.content, { size: ctx.typography.sizeSmall })); + break; + case 'link_open': + linkStack.push({ href: child.attrGet('href') || '', children: [] }); + break; + case 'link_close': + if (linkStack.length) closeLink(); + break; + case 'citation': + push(citationRun(ctx, child.meta, state)); + break; + case 'xref': + push(xrefRun(ctx, child.meta, state)); + break; + case 'image': + state.warnings.push( + 'Инлайн-изображение пропущено; используйте директиву ::: figure' + ); + break; + default: + break; + } + } + while (linkStack.length) closeLink(); + return runs; +} + +// ── списки ──────────────────────────────────────────────────────── + +function renderList(ctx, tokens, startIndex, depth, listCtx, state) { + const open = tokens[startIndex]; + const ordered = open.type === 'ordered_list_open'; + const current = depth === 0 ? { ref: ordered ? REFS.number : REFS.dash } : listCtx; + if (depth === 0) current.instance = nextListInstance(current.ref); + + const elements = []; + const level = Math.min(depth, 3); + let i = startIndex + 1; + const closeType = ordered ? 'ordered_list_close' : 'bullet_list_close'; + + while (i < tokens.length && tokens[i].type !== closeType) { + if (tokens[i].type !== 'list_item_open') { + i += 1; + continue; + } + i += 1; + let firstParagraph = true; + while (i < tokens.length && tokens[i].type !== 'list_item_close') { + const token = tokens[i]; + if (token.type === 'paragraph_open') { + const runs = renderInline(ctx, tokens[i + 1], state); + if (firstParagraph) { + elements.push(listParagraphRuns(ctx, runs, current.ref, level, current.instance)); + firstParagraph = false; + } else { + elements.push( + paraRuns(ctx, runs, { + extra: { + indent: { + left: ctx.config.lists.nestedLevelStep * (depth + 1), + firstLine: ctx.typography.indentFirst, + }, + }, + }) + ); + } + i += 3; + } else if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') { + const nested = renderList(ctx, tokens, i, depth + 1, current, state); + elements.push(...nested.elements); + i = nested.next; + } else if (token.type === 'directive') { + elements.push(...renderDirective(ctx, token, state)); + i += 1; + } else if (token.type === 'table_open') { + const table = readTable(tokens, i); + elements.push(...tableSto(ctx, { label: '', title: '', rows: table.rows })); + i = table.next; + } else { + i += 1; + } + } + i += 1; + } + return { elements, next: i + 1 }; +} + +// ── таблицы из markdown ─────────────────────────────────────────── + +function readTable(tokens, startIndex) { + const rows = []; + let current = null; + let i = startIndex + 1; + for (; i < tokens.length; i++) { + const token = tokens[i]; + if (token.type === 'tr_open') { + current = []; + } else if (token.type === 'tr_close') { + if (current) rows.push(current); + current = null; + } else if (token.type === 'inline') { + if (current) current.push(token.content); + } else if (token.type === 'table_close') { + i += 1; + break; + } + } + return { rows, next: i }; +} + +// ── директивы ───────────────────────────────────────────────────── + +function parseWhereBody(body) { + return body + .split(/\r?\n/) + .map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, '').trim()) + .filter(Boolean) + .map((line) => { + const parts = line.split(/\s+[—–-]\s+/); + if (parts.length < 2) return { symbol: line, desc: '' }; + return { symbol: parts[0].trim(), desc: parts.slice(1).join(' — ').trim() }; + }); +} + +function splitNoteItems(body) { + return body + .split(/\r?\n/) + .map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, '').trim()) + .filter(Boolean); +} + +function parseDefinitions(body) { + return parseWhereBody(body).map(({ symbol, desc }) => ({ term: symbol, definition: desc })); +} + +function findTableRows(md, body) { + const tokens = md.parse(body, {}); + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].type === 'table_open') return readTable(tokens, i).rows; + } + return null; +} + +function renderDirective(ctx, token, state) { + const { name, attrs, body } = token.info; + switch (name) { + case 'pagebreak': + return [pageBreak()]; + case 'figure': { + const cap = token.__caption || { label: '?' }; + const notes = body + ? body.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) + : []; + if (attrs.file) { + const resolved = path.isAbsolute(attrs.file) + ? attrs.file + : path.resolve(ctx.assetsRoot, attrs.file); + if (!fs.existsSync(resolved)) { + state.warnings.push(`Рисунок "${attrs.title || ''}": файл не найден: ${attrs.file}`); + } + } + return figureBlock(ctx, { + label: cap.label, + title: attrs.title || '', + imagePath: attrs.file, + notesLines: notes, + }); + } + case 'table': { + const cap = token.__caption || { label: '?' }; + const rows = findTableRows(state.md, body); + if (!rows) { + state.errors.push( + `Директива ::: table "${attrs.title || ''}": не найдена markdown-таблица` + ); + return []; + } + return tableSto(ctx, { + label: cap.label, + title: attrs.title || '', + rows, + note: attrs.note, + }); + } + case 'formula': { + const cap = token.__caption || { label: '?' }; + return formulaFromText(ctx, body.trim(), cap.label); + } + case 'where': + return whereBlock(ctx, parseWhereBody(body)); + case 'note': + return [noteSingle(ctx, body.trim().replace(/\s*\n\s*/g, ' '))]; + case 'notes': + return notesBlock(ctx, splitNoteItems(body)); + case 'definitions': + return definitionsBlock(ctx, parseDefinitions(body)); + default: + state.warnings.push(`Неизвестная директива ::: ${name}`); + return []; + } +} + +// ── общий обход токенов ─────────────────────────────────────────── + +function renderTokens(ctx, tokens, state, opts = {}) { + const mode = opts.mode || 'plain'; + const out = []; + let firstH1 = true; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + switch (token.type) { + case 'heading_open': { + const level = Number(token.tag.slice(1)); + const text = tokens[i + 1] ? tokens[i + 1].content : ''; + if (mode === 'numbered') { + if (level <= 1) { + if (!(firstH1 && opts.suppressFirstPageBreak)) out.push(pageBreak()); + firstH1 = false; + out.push(sectionHeading(ctx, text)); + } else if (level === 2) { + out.push(subsectionHeading(ctx, text)); + } else { + out.push(pointParagraph(ctx, text)); + } + } else { + out.push(plainHeading(ctx, text, level)); + } + i += 2; + break; + } + case 'paragraph_open': + out.push(paraRuns(ctx, renderInline(ctx, tokens[i + 1], state))); + i += 2; + break; + case 'directive': + out.push(...renderDirective(ctx, token, state)); + break; + case 'bullet_list_open': + case 'ordered_list_open': { + const res = renderList(ctx, tokens, i, 0, null, state); + out.push(...res.elements); + i = res.next - 1; + break; + } + case 'table_open': { + const res = readTable(tokens, i); + out.push(...tableSto(ctx, { label: '', title: '', rows: res.rows })); + i = res.next - 1; + break; + } + case 'fence': + state.warnings.push('Блок кода проигнорирован (функция листингов удалена)'); + break; + case 'hr': + out.push(pageBreak()); + break; + default: + break; + } + } + return out; +} + +function renderSections(ctx, state) { + resetListInstances(); + const out = []; + let first = true; + + for (const section of state.sections) { + const entry = section.entry; + + if (entry.type === 'toc') { + if (!ctx.config.toc.enabled) continue; + out.push(...tocSection(ctx, [], { isFirstInDocument: first })); + first = false; + } else if (entry.type === 'structural') { + const title = entry.title || section.meta.title || firstInlineText(section.tokens) || ''; + const body = renderTokens(ctx, section.tokens, state, { mode: 'plain' }); + out.push(...structSection(ctx, title, body, { pageBreakBefore: !first })); + first = false; + } else if (entry.type === 'numbered') { + out.push( + ...renderTokens(ctx, section.tokens, state, { + mode: 'numbered', + suppressFirstPageBreak: first, + }) + ); + first = false; + } else if (entry.type === 'bibliography') { + const body = bibliographyList(ctx, section.biblioEntries || []); + out.push( + ...structSection(ctx, ctx.config.bibliography.title, body, { pageBreakBefore: !first }) + ); + first = false; + } else if (entry.type === 'appendix') { + out.push(...appendixStart(ctx, entry.letter, entry.title || '')); + out.push(...renderTokens(ctx, section.tokens, state, { mode: 'plain' })); + first = false; + } + } + return out; +} + +function buildDocx(ctx, children) { + resetListInstances(); + return new Document({ + features: ctx.config.document.features, + numbering: ctx.numberingConfig, + styles: ctx.documentStyles, + sections: [ + { + properties: defaultSectionProps(ctx), + footers: { default: pageNumberFooter(ctx) }, + children, + }, + ], + }); +} + +module.exports = { + createState, + renderSections, + renderTokens, + renderInline, + readTable, + parseWhereBody, + buildDocx, +}; diff --git a/src/render/primitives.js b/src/render/primitives.js new file mode 100644 index 0000000..d38ed6f --- /dev/null +++ b/src/render/primitives.js @@ -0,0 +1,640 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { + Paragraph, + TextRun, + Table, + TableRow, + TableCell, + PageBreak, + AlignmentType, + HeadingLevel, + TabStopType, + WidthType, + BorderStyle, + ImageRun, + TableOfContents, + Footer, + PageNumber, + StyleLevel, + Math: DocxMath, + MathRun, + MathFraction, + MathSuperScript, + MathSubScript, +} = require('docx'); + +const { REFS } = require('./styles'); + +function alignmentOf(name) { + switch (name) { + case 'center': + return AlignmentType.CENTER; + case 'right': + return AlignmentType.RIGHT; + case 'both': + case 'justify': + return AlignmentType.BOTH; + default: + return AlignmentType.LEFT; + } +} + +// ── базовые абзацы ──────────────────────────────────────────────── + +function textRun(ctx, text, opts = {}) { + const t = ctx.typography; + return new TextRun({ + text: text == null ? '' : String(text), + font: t.font, + size: opts.size || t.sizeBody, + bold: t.useBold ? Boolean(opts.bold) : false, + italics: t.allowEmphasis ? Boolean(opts.italics) : false, + subScript: opts.subScript || false, + superScript: opts.superScript || false, + }); +} + +function flatten(...blocks) { + const result = []; + for (const block of blocks) { + if (block == null) continue; + if (Array.isArray(block)) result.push(...flatten(...block)); + else result.push(block); + } + return result; +} + +function paraRuns(ctx, runs, opts = {}) { + return new Paragraph({ + children: runs, + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: opts.noFirstIndent ? {} : { firstLine: ctx.typography.indentFirst }, + alignment: opts.alignment || AlignmentType.BOTH, + ...opts.extra, + }); +} + +function para(ctx, text, opts = {}) { + return paraRuns(ctx, [textRun(ctx, text, opts)], opts); +} + +function emptyLine(ctx) { + return new Paragraph({ + children: [textRun(ctx, ' ')], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: {}, + }); +} + +// ── заголовки ───────────────────────────────────────────────────── + +function structHeading(ctx, title, opts = {}) { + const elements = []; + if (opts.pageBreakBefore !== false) elements.push(pageBreak()); + const cfg = ctx.config.headings.struct; + elements.push( + new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [textRun(ctx, String(title).toUpperCase())], + spacing: { + ...ctx.typography.lineSpacing, + before: cfg.spacingBefore, + after: cfg.spacingAfter, + }, + alignment: alignmentOf(cfg.align), + indent: {}, + }) + ); + return elements; +} + +function structSection(ctx, title, children, opts = {}) { + return flatten(structHeading(ctx, title, opts), children); +} + +function sectionHeading(ctx, title) { + const cfg = ctx.config.headings.section; + return new Paragraph({ + heading: HeadingLevel.HEADING_1, + numbering: { reference: REFS.outline, level: 0 }, + children: [textRun(ctx, title)], + spacing: { ...ctx.typography.lineSpacing, before: cfg.spacingBefore, after: cfg.spacingAfter }, + alignment: alignmentOf(cfg.align), + indent: { firstLine: ctx.typography.indentFirst }, + }); +} + +function subsectionHeading(ctx, title, opts = {}) { + const cfg = ctx.config.headings.subsection; + return new Paragraph({ + heading: HeadingLevel.HEADING_2, + numbering: { reference: REFS.outline, level: 1 }, + children: [textRun(ctx, title)], + spacing: { + ...ctx.typography.lineSpacing, + before: opts.spacingBefore ?? cfg.spacingBefore, + after: cfg.spacingAfter, + }, + alignment: alignmentOf(cfg.align), + indent: { firstLine: ctx.typography.indentFirst }, + }); +} + +function plainHeading(ctx, text, level = 2) { + const heading = level <= 1 ? HeadingLevel.HEADING_1 : level === 2 ? HeadingLevel.HEADING_2 : HeadingLevel.HEADING_3; + return new Paragraph({ + heading, + children: [textRun(ctx, text)], + spacing: { ...ctx.typography.lineSpacing, before: 240, after: 240 }, + alignment: AlignmentType.LEFT, + indent: {}, + }); +} + +function pointParagraph(ctx, text) { + const cfg = ctx.config.headings.point; + return new Paragraph({ + heading: HeadingLevel.HEADING_3, + numbering: { reference: REFS.outline, level: 2 }, + children: [textRun(ctx, text)], + spacing: { ...ctx.typography.lineSpacing, before: cfg.spacingBefore, after: cfg.spacingAfter }, + alignment: AlignmentType.BOTH, + indent: { firstLine: ctx.typography.indentFirst }, + }); +} + +// ── подписи ─────────────────────────────────────────────────────── + +function figureCaption(ctx, label, title) { + const { prefix, dash } = ctx.config.captions.figure; + return new Paragraph({ + children: [textRun(ctx, `${prefix} ${label} ${dash} ${title}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: alignmentOf(ctx.config.captions.figure.align), + indent: {}, + }); +} + +function tableCaption(ctx, label, title) { + const { prefix, dash } = ctx.config.captions.table; + return new Paragraph({ + children: [textRun(ctx, `${prefix} ${label} ${dash} ${title}`)], + spacing: { ...ctx.typography.lineSpacing, before: 240, after: 0 }, + alignment: alignmentOf(ctx.config.captions.table.align), + indent: {}, + }); +} + +function tableContinuation(ctx, label, { pageBreakBefore = false } = {}) { + const { prefix } = ctx.config.captions.tableContinue; + return new Paragraph({ + children: [textRun(ctx, `${prefix} ${label}`)], + spacing: { ...ctx.typography.lineSpacing, before: 240, after: 0 }, + alignment: alignmentOf(ctx.config.captions.table.align), + indent: {}, + ...(pageBreakBefore ? { pageBreakBefore: true } : {}), + keepNext: true, + }); +} + +function tableNote(ctx, text) { + return new Paragraph({ + children: [textRun(ctx, `Примечание ${ctx.config.captions.figure.dash} ${text}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: AlignmentType.BOTH, + indent: { firstLine: ctx.typography.indentFirst }, + }); +} + +// ── изображения ─────────────────────────────────────────────────── + +function imageTypeFromPath(filePath) { + const ext = path.extname(filePath).toLowerCase(); + if (ext === '.jpg' || ext === '.jpeg') return 'jpg'; + if (ext === '.gif') return 'gif'; + if (ext === '.bmp') return 'bmp'; + return 'png'; +} + +function getImageDimensions(buffer, type) { + if (type === 'jpg') { + let i = 2; + while (i < buffer.length - 8) { + if (buffer[i] !== 0xff) { + i++; + continue; + } + const marker = buffer[i + 1]; + if (marker === 0xc0 || marker === 0xc2) { + return { + width: buffer.readUInt16BE(i + 7), + height: buffer.readUInt16BE(i + 5), + }; + } + const len = buffer.readUInt16BE(i + 2); + i += 2 + len; + } + } + if (type === 'png' && buffer.length > 24 && buffer.toString('ascii', 1, 4) === 'PNG') { + return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) }; + } + return null; +} + +function scaleImageDimensions(width, height, maxWidthPx, maxHeightPx = Infinity) { + if (!width || !height) return { width: maxWidthPx, height: Math.round(maxWidthPx * 0.6) }; + const ratio = Math.min(maxWidthPx / width, maxHeightPx / height, 1); + return { width: Math.round(width * ratio), height: Math.round(height * ratio) }; +} + +function figureBlock(ctx, { label, title, imagePath, notesLines = [] }) { + const resolved = imagePath + ? path.isAbsolute(imagePath) + ? imagePath + : path.resolve(ctx.assetsRoot, imagePath) + : null; + const elements = []; + if (resolved && fs.existsSync(resolved)) { + const data = fs.readFileSync(resolved); + const type = imageTypeFromPath(resolved); + const dims = getImageDimensions(data, type); + const scaled = scaleImageDimensions( + dims?.width, + dims?.height, + ctx.figureMaxWidthPx, + ctx.figureMaxHeightPx + ); + elements.push( + new Paragraph({ + children: [new ImageRun({ type, data, transformation: scaled })], + alignment: AlignmentType.CENTER, + indent: {}, + keepNext: true, + }) + ); + } + for (const line of notesLines) { + if (String(line).trim()) { + elements.push( + new Paragraph({ + children: [textRun(ctx, line)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: AlignmentType.CENTER, + indent: {}, + keepNext: true, + }) + ); + } + } + elements.push(figureCaption(ctx, label, title)); + return elements; +} + +// ── таблицы ─────────────────────────────────────────────────────── + +function border(ctx) { + const t = ctx.config.table; + return { style: BorderStyle.SINGLE, size: t.borderSize, color: t.borderColor }; +} + +function tableRow(ctx, cells, isHeader = false) { + const t = ctx.config.table; + const b = border(ctx); + return new TableRow({ + tableHeader: isHeader, + children: cells.map( + ({ text, w }) => + new TableCell({ + width: { size: w, type: WidthType.DXA }, + margins: { + top: t.cellMargins.top, + bottom: t.cellMargins.bottom, + left: t.cellMargins.left, + right: t.cellMargins.right, + }, + borders: { top: b, bottom: b, left: b, right: b }, + shading: { fill: isHeader ? t.headerFill : t.cellFill, type: 'CLEAR' }, + children: String(text) + .split('\n') + .map( + (line) => + new Paragraph({ + children: [textRun(ctx, line, { size: t.size })], + spacing: { ...ctx.typography.lineSpacingSingle, before: 0, after: 0 }, + alignment: AlignmentType.LEFT, + indent: {}, + }) + ), + }) + ), + }); +} + +function tableSto(ctx, { label, title, rows, columnWidths, note }) { + const n = rows[0].length; + const totalW = columnWidths + ? columnWidths.reduce((a, b) => a + b, 0) + : ctx.page.contentWidth; + const widths = + columnWidths || + Array(n) + .fill(0) + .map((_, i) => + i === n - 1 + ? ctx.page.contentWidth - Math.floor(ctx.page.contentWidth / n) * (n - 1) + : Math.floor(ctx.page.contentWidth / n) + ); + + const result = []; + if (label) result.push(tableCaption(ctx, label, title)); + result.push( + new Table({ + width: { size: totalW, type: WidthType.DXA }, + columnWidths: widths, + rows: rows.map((row, ri) => + tableRow(ctx, row.map((text, ci) => ({ text: String(text), w: widths[ci] })), ri === 0) + ), + }) + ); + if (note) result.push(tableNote(ctx, note)); + return result; +} + +// ── формулы ─────────────────────────────────────────────────────── + +/** + * Разбирает простую формулу: `_` — индекс, `^` — степень, `/` — дробь. + * Дробь выделяется первой: `A/B` → numerator = parse(A), denominator = parse(B). + */ +function parseSimpleFormula(text) { + const trimmed = String(text).trim(); + if (!trimmed) return [new MathRun('')]; + + const slash = trimmed.indexOf('/'); + if (slash > 0 && trimmed.indexOf('/', slash + 1) === -1) { + return [ + new MathFraction({ + numerator: parseSimpleFormula(trimmed.slice(0, slash)), + denominator: parseSimpleFormula(trimmed.slice(slash + 1)), + }), + ]; + } + + const children = []; + const re = /([_^])([A-Za-zА-Яа-я0-9]+)/g; + let last = 0; + let match; + while ((match = re.exec(trimmed)) !== null) { + if (match.index > last) children.push(new MathRun(trimmed.slice(last, match.index))); + const run = new MathRun(match[2]); + children.push( + match[1] === '_' + ? new MathSubScript({ children: [run] }) + : new MathSuperScript({ children: [run] }) + ); + last = re.lastIndex; + } + if (last < trimmed.length) children.push(new MathRun(trimmed.slice(last))); + return children.length ? children : [new MathRun(trimmed)]; +} + +function mathPara(ctx, mathChildren, formulaNum, opts = {}) { + const num = String(formulaNum).replace(/^\(|\)$/g, ''); + const paragraph = new Paragraph({ + children: [textRun(ctx, '\t'), new DocxMath({ children: mathChildren }), textRun(ctx, `\t(${num})`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: AlignmentType.LEFT, + indent: {}, + tabStops: [ + { type: TabStopType.CENTER, position: ctx.page.contentWidth / 2 }, + { type: TabStopType.RIGHT, position: ctx.page.contentWidth }, + ], + }); + if (opts.noSpacing) return paragraph; + return [emptyLine(ctx), paragraph, emptyLine(ctx)]; +} + +function formulaFromText(ctx, text, formulaNum) { + const normalized = String(text).replace(/\*/g, '×'); + return mathPara(ctx, parseSimpleFormula(normalized), formulaNum); +} + +function whereBlock(ctx, items) { + const dash = ctx.config.captions.figure.dash; + return items.map((item, i) => { + const prefix = textRun(ctx, i === 0 ? 'где\t' : '\t'); + const parts = String(item.symbol || '').split(/(?=[_^])/); + const symbolRuns = parts.map((part) => { + if (part.startsWith('_')) return textRun(ctx, part.slice(1), { subScript: true }); + if (part.startsWith('^')) return textRun(ctx, part.slice(1), { superScript: true }); + return textRun(ctx, part); + }); + return new Paragraph({ + children: [prefix, ...symbolRuns, textRun(ctx, ` ${dash} ${item.desc}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: {}, + tabStops: [{ type: TabStopType.LEFT, position: ctx.config.formula.whereTabPosition }], + }); + }); +} + +// ── список (нумерация Word) ─────────────────────────────────────── + +const listInstanceCounters = new Map(); + +function nextListInstance(reference) { + const instance = listInstanceCounters.get(reference) ?? 0; + listInstanceCounters.set(reference, instance + 1); + return instance; +} + +function resetListInstances() { + listInstanceCounters.clear(); +} + +function listParagraphRuns(ctx, runs, reference, level, instance) { + return new Paragraph({ + numbering: { reference, level, instance }, + children: runs, + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: AlignmentType.BOTH, + }); +} + +// ── прочее ──────────────────────────────────────────────────────── + +function pageBreak() { + return new Paragraph({ children: [new PageBreak()], indent: {} }); +} + +function noteSingle(ctx, text) { + return new Paragraph({ + children: [textRun(ctx, `Примечание ${ctx.config.captions.figure.dash} ${text}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: { firstLine: ctx.typography.indentFirst }, + }); +} + +function notesBlock(ctx, items) { + const result = [ + new Paragraph({ + children: [textRun(ctx, 'Примечания')], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: { firstLine: ctx.typography.indentFirst }, + }), + ]; + items.forEach((text, i) => { + result.push( + new Paragraph({ + children: [textRun(ctx, `${i + 1} ${text}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: { firstLine: ctx.typography.indentFirst }, + }) + ); + }); + return result; +} + +function definitionsBlock(ctx, pairs) { + const dash = ctx.config.captions.figure.dash; + return pairs.map( + ({ term, definition }) => + new Paragraph({ + children: [textRun(ctx, `${term} ${dash} ${definition}`)], + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + indent: { firstLine: ctx.typography.indentFirst }, + }) + ); +} + +function appendixStart(ctx, letter, title) { + return [ + pageBreak(), + new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [textRun(ctx, `${ctx.config.appendix.labelPrefix} ${letter.toUpperCase()}`)], + spacing: { ...ctx.typography.lineSpacing, before: ctx.config.headings.struct.spacingBefore, after: 0 }, + alignment: AlignmentType.CENTER, + indent: {}, + }), + new Paragraph({ + children: [textRun(ctx, title)], + spacing: { + ...ctx.typography.lineSpacing, + before: 0, + after: ctx.config.headings.struct.spacingAfter, + }, + alignment: AlignmentType.CENTER, + indent: {}, + }), + ]; +} + +function tableOfContents(ctx, cachedEntries = []) { + const { title, depth } = ctx.config.toc; + const levels = []; + for (let i = 1; i <= depth; i++) levels.push(new StyleLevel(`TOC${i}`, i)); + return new TableOfContents(title, { + hyperlink: true, + headingStyleRange: `1-${depth}`, + cachedEntries, + stylesWithLevels: levels, + }); +} + +function tocSection(ctx, cachedEntries = [], opts = {}) { + const isFirst = opts.isFirstInDocument === true; + // Без завершающего разрыва: следующая часть сама начинает новую страницу, + // иначе после содержания появляется пустой лист. + return flatten( + structHeading(ctx, ctx.config.toc.title, { pageBreakBefore: !isFirst }), + tableOfContents(ctx, cachedEntries) + ); +} + +function defaultSectionProps(ctx) { + return { + page: { + size: { width: ctx.page.width, height: ctx.page.height }, + margin: { + left: ctx.page.margins.left, + right: ctx.page.margins.right, + top: ctx.page.margins.top, + bottom: ctx.page.margins.bottom, + }, + pageNumbers: { start: ctx.config.pageNumber.startFrom }, + }, + }; +} + +function pageNumberFooter(ctx) { + return new Footer({ + children: [ + new Paragraph({ + alignment: alignmentOf(ctx.config.pageNumber.align), + children: [ + new TextRun({ + children: [PageNumber.CURRENT], + font: ctx.typography.font, + size: ctx.typography.sizeBody, + }), + ], + }), + ], + }); +} + +function bibliographyList(ctx, entries) { + return entries.map( + (text) => + new Paragraph({ + children: [textRun(ctx, String(text).replace(/^\d+[.)]\s*/, ''))], + numbering: { reference: REFS.bibliography, level: 0 }, + spacing: { ...ctx.typography.lineSpacing, before: 0, after: 0 }, + alignment: AlignmentType.BOTH, + indent: { left: 0, firstLine: ctx.typography.indentFirst }, + }) + ); +} + +module.exports = { + alignmentOf, + textRun, + flatten, + paraRuns, + para, + emptyLine, + structHeading, + structSection, + sectionHeading, + subsectionHeading, + plainHeading, + pointParagraph, + figureCaption, + tableCaption, + tableContinuation, + tableNote, + figureBlock, + tableSto, + parseSimpleFormula, + mathPara, + formulaFromText, + whereBlock, + nextListInstance, + resetListInstances, + listParagraphRuns, + pageBreak, + noteSingle, + notesBlock, + definitionsBlock, + appendixStart, + tableOfContents, + tocSection, + defaultSectionProps, + pageNumberFooter, + bibliographyList, + REFS, +}; diff --git a/src/render/references.js b/src/render/references.js new file mode 100644 index 0000000..3b49aff --- /dev/null +++ b/src/render/references.js @@ -0,0 +1,93 @@ +'use strict'; + +/** Текстовая выжимка из токенов — для списка источников. */ +function extractInlineTexts(tokens) { + const out = []; + for (const token of tokens) { + if (token.type === 'inline') { + const text = String(token.content || '').replace(/\s+/g, ' ').trim(); + if (text) out.push(text); + } + } + return out; +} + +/** + * Проход 1: назначает номера рисункам/таблицам/формулам, собирает метки + * и подсчитывает записи библиографии. Номера кладутся прямо в токены. + */ +function collectNumbers(sections, ctx, state) { + const ncfg = ctx.config.numbering; + let bodyFig = 0; + let bodyTbl = 0; + let formula = 0; + let sectionNum = 0; + let secFig = 0; + let secTbl = 0; + + state.labelMap = new Map(); + state.biblioCount = 0; + + for (const section of sections) { + const entry = section.entry; + + if (entry.type === 'bibliography') { + section.biblioEntries = extractInlineTexts(section.tokens); + state.biblioCount += section.biblioEntries.length; + continue; + } + if (entry.type === 'toc') continue; + + if (entry.type === 'appendix') { + section.letter = String(entry.letter).toUpperCase(); + section.fig = 0; + section.tbl = 0; + } + if (entry.type === 'numbered') { + sectionNum += 1; + secFig = 0; + secTbl = 0; + } + + for (const token of section.tokens) { + if (token.type !== 'directive') continue; + const { name, attrs } = token.info; + + if (name === 'figure') { + let label; + if (entry.type === 'appendix') { + section.fig += 1; + label = `${section.letter}.${section.fig}`; + } else if (ncfg.figures === 'section') { + secFig += 1; + label = `${sectionNum}.${secFig}`; + } else { + bodyFig += 1; + label = String(bodyFig); + } + token.__caption = { label }; + if (attrs.id) state.labelMap.set(`fig:${attrs.id}`, label); + } else if (name === 'table') { + let label; + if (entry.type === 'appendix') { + section.tbl += 1; + label = `${section.letter}.${section.tbl}`; + } else if (ncfg.tables === 'section') { + secTbl += 1; + label = `${sectionNum}.${secTbl}`; + } else { + bodyTbl += 1; + label = String(bodyTbl); + } + token.__caption = { label }; + if (attrs.id) state.labelMap.set(`tbl:${attrs.id}`, label); + } else if (name === 'formula') { + formula += 1; + token.__caption = { label: String(formula) }; + if (attrs.id) state.labelMap.set(`eq:${attrs.id}`, formula); + } + } + } +} + +module.exports = { collectNumbers, extractInlineTexts }; diff --git a/src/render/sample-titlepage.js b/src/render/sample-titlepage.js new file mode 100644 index 0000000..fc8fa92 --- /dev/null +++ b/src/render/sample-titlepage.js @@ -0,0 +1,66 @@ +'use strict'; + +const { Document, Packer, Paragraph, TextRun, AlignmentType } = require('docx'); + +const FONT = 'Times New Roman'; +const SIZE = 28; +const PAGE = { width: 11906, height: 16838 }; +const MARGIN = { left: 1701, right: 851, top: 1134, bottom: 1134 }; + +function line(text = ' ', { align = AlignmentType.CENTER, bold = false, size = SIZE } = {}) { + return new Paragraph({ + alignment: align, + spacing: { line: 360, lineRule: 'auto', before: 0, after: 0 }, + indent: {}, + children: [new TextRun({ text, font: FONT, size, bold })], + }); +} + +const blank = () => line(' '); + +/** Готовый пример титульного листа (DOCX), склеивается в начало документа. */ +async function buildSampleTitlePage() { + const doc = new Document({ + features: { updateFields: false }, + styles: { + default: { + document: { run: { font: FONT, size: SIZE, color: '000000' } }, + }, + }, + sections: [ + { + properties: { page: { size: PAGE, margin: MARGIN } }, + children: [ + line('МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ'), + line('Федеральное государственное автономное образовательное учреждение'), + line('высшего образования'), + line('«Самарский национальный исследовательский университет'), + line('имени академика С. П. Королёва»'), + blank(), + line('Кафедра программных систем'), + blank(), + blank(), + blank(), + line('ОТЧЁТ', { size: 32 }), + line('о выполнении практической работы'), + blank(), + line('Тема: «Разработка информационной системы учёта заявок»'), + blank(), + blank(), + line('Выполнил: студент группы 6212-010302D', { align: AlignmentType.RIGHT }), + line('Иванов И. И.', { align: AlignmentType.RIGHT }), + blank(), + line('Проверил: доцент, кандидат технических наук', { align: AlignmentType.RIGHT }), + line('Петров П. П.', { align: AlignmentType.RIGHT }), + blank(), + blank(), + blank(), + line('Самара 2026'), + ], + }, + ], + }); + return Packer.toBuffer(doc); +} + +module.exports = { buildSampleTitlePage }; diff --git a/src/render/styles.js b/src/render/styles.js new file mode 100644 index 0000000..da78114 --- /dev/null +++ b/src/render/styles.js @@ -0,0 +1,181 @@ +'use strict'; + +const { AlignmentType, LevelFormat, LevelSuffix } = require('docx'); + +const REFS = { + dash: 'listDash', + number: 'listNumber', + bibliography: 'bibliography', + outline: 'headingOutline', +}; + +function buildContext(config) { + const { page, typography, lists, numbering, table, figure } = config; + + const contentWidth = page.width - page.margins.left - page.margins.right; + const lineSpacing = { line: Math.round(240 * typography.lineSpacing), lineRule: 'auto' }; + const lineSpacingSingle = { line: 240, lineRule: 'auto' }; + + const ctx = { + config, + assetsRoot: config.paths.assets, + page: { + width: page.width, + height: page.height, + margins: page.margins, + contentWidth, + }, + typography: { + font: typography.font, + sizeBody: typography.sizeBody, + sizeTable: typography.sizeTable, + sizeSmall: typography.sizeSmall, + color: typography.color, + useBold: typography.useBold, + allowEmphasis: typography.allowEmphasis, + lineSpacing, + lineSpacingSingle, + indentFirst: typography.indentFirst, + }, + figureMaxWidthPx: + figure.maxWidth != null + ? figure.maxWidth + : Math.round((contentWidth / 1440) * 96), + figureMaxHeightPx: figure.maxHeight, + }; + + ctx.numberingConfig = buildNumberingConfig(ctx); + ctx.documentStyles = buildDocumentStyles(ctx); + + return ctx; +} + +function buildNumberingConfig(ctx) { + const typography = ctx.typography; + const lists = ctx.config.lists; + const run = { + font: typography.font, + size: typography.sizeBody, + color: typography.color, + }; + const indent = (lvl) => ({ + left: lists.nestedLevelStep * lvl, + firstLine: typography.indentFirst, + }); + const level = (lvl, format, text, { suffix = LevelSuffix.SPACE, indentOverride } = {}) => ({ + level: lvl, + format, + text, + suffix, + alignment: AlignmentType.LEFT, + start: 1, + style: { + paragraph: { + indent: indentOverride || indent(lvl), + spacing: { ...typography.lineSpacing, before: 0, after: 0 }, + }, + run, + }, + }); + const outlineLevel = (lvl, text) => ({ + level: lvl, + format: LevelFormat.DECIMAL, + text, + suffix: LevelSuffix.TAB, + alignment: AlignmentType.LEFT, + start: 1, + style: { + paragraph: { + indent: { left: 0, firstLine: typography.indentFirst }, + spacing: { ...typography.lineSpacing, before: 0, after: 0 }, + }, + run, + }, + }); + + const dashRef = [ + level(0, LevelFormat.BULLET, lists.bulletChar), + level(1, LevelFormat.RUSSIAN_LOWER, `%2${lists.letterSuffix}`), + level(2, LevelFormat.DECIMAL, `%3${lists.numberSuffix}`), + level(3, LevelFormat.BULLET, lists.bulletChar), + ]; + const numberRef = [ + level(0, LevelFormat.DECIMAL, `%1${lists.numberSuffix}`), + level(1, LevelFormat.RUSSIAN_LOWER, `%2${lists.letterSuffix}`), + level(2, LevelFormat.DECIMAL, `%3${lists.numberSuffix}`), + ]; + + return { + config: [ + { reference: REFS.dash, levels: dashRef }, + { reference: REFS.number, levels: numberRef }, + { + reference: REFS.bibliography, + levels: [ + level(0, LevelFormat.DECIMAL, '%1', { + suffix: LevelSuffix.SPACE, + indentOverride: { left: 0, firstLine: typography.indentFirst }, + }), + ], + }, + { + reference: REFS.outline, + levels: [outlineLevel(0, '%1'), outlineLevel(1, '%1.%2'), outlineLevel(2, '%1.%2.%3')], + }, + ], + }; +} + +function buildDocumentStyles(ctx) { + const run = { + font: ctx.typography.font, + size: ctx.typography.sizeBody, + bold: false, + color: ctx.typography.color, + }; + const line = { line: 360, lineRule: 'auto' }; + // Встроенные стили Heading1..3 задаются через механизм дефолтов docx, + // чтобы не дублировать styleId. + return { + default: { + document: { + run: { + font: ctx.typography.font, + size: ctx.typography.sizeBody, + color: ctx.typography.color, + }, + }, + heading1: { run, paragraph: { spacing: line, outlineLevel: 0 } }, + heading2: { run, paragraph: { spacing: line, outlineLevel: 1, alignment: AlignmentType.LEFT } }, + heading3: { run, paragraph: { spacing: line, outlineLevel: 2, alignment: AlignmentType.LEFT } }, + }, + paragraphStyles: [ + { + id: 'TOC1', + name: 'TOC 1', + basedOn: 'Normal', + next: 'Normal', + run, + paragraph: { spacing: line }, + }, + { + id: 'TOC2', + name: 'TOC 2', + basedOn: 'Normal', + next: 'Normal', + run, + paragraph: { spacing: line, indent: { left: 360 } }, + }, + { + id: 'TOC3', + name: 'TOC 3', + basedOn: 'Normal', + next: 'Normal', + run, + paragraph: { spacing: line, indent: { left: 720 } }, + }, + ], + }; +} + +module.exports = { buildContext, buildNumberingConfig, REFS }; diff --git a/src/render/titlepage.js b/src/render/titlepage.js new file mode 100644 index 0000000..1cbc1ad --- /dev/null +++ b/src/render/titlepage.js @@ -0,0 +1,141 @@ +'use strict'; + +const DocxMerger = require('docx-merger'); +const JSZip = require('jszip'); + +const PARTS_TO_PATCH = ['word/numbering.xml', 'word/styles.xml', 'word/document.xml']; + +/** docx-merger переименовывает numId в numbering.xml, но не в document.xml. */ +function patchDocumentNumIds(xml, fileIndex) { + return xml.replace( + //g, + (_, id) => `` + ); +} + +async function prepareBufferForMerge(buffer, fileIndex) { + const zip = await JSZip.loadAsync(buffer); + const docFile = zip.file('word/document.xml'); + if (docFile) { + const xml = await docFile.async('string'); + zip.file('word/document.xml', patchDocumentNumIds(xml, fileIndex)); + } + return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }); +} + +function toBinary(data) { + if (typeof data === 'string') return data; + return Buffer.from(data).toString('binary'); +} + +function getRootOpenTag(xml) { + const m = xml.match(/<[\w:][^>]*>/); + return m ? m[0] : null; +} + +function collectUsedPrefixes(xml) { + const prefixes = new Set(); + const re = /\b([a-z][a-z0-9]*):/gi; + let match; + while ((match = re.exec(xml)) !== null) prefixes.add(match[1]); + return prefixes; +} + +function getDeclaredPrefixes(rootTag) { + const declared = new Set(); + if (!rootTag) return declared; + const re = /xmlns:([a-zA-Z0-9]+)=/g; + let m; + while ((m = re.exec(rootTag))) declared.add(m[1]); + return declared; +} + +function extractNamespaceAttrs(refRoot) { + const attrs = []; + const re = /(xmlns(?::[a-zA-Z0-9]+)?="[^"]*")|(mc:Ignorable="[^"]*")/g; + let m; + while ((m = re.exec(refRoot))) attrs.push(m[0]); + return attrs; +} + +function patchXmlRootNamespaces(xml, refXml) { + const root = getRootOpenTag(xml); + const refRoot = getRootOpenTag(refXml); + if (!root || !refRoot) return xml; + + const used = collectUsedPrefixes(xml); + const declared = getDeclaredPrefixes(root); + let insert = ''; + + for (const attr of extractNamespaceAttrs(refRoot)) { + if (attr.startsWith('mc:Ignorable')) { + if (!/mc:Ignorable=/.test(root)) insert += ` ${attr}`; + continue; + } + const prefixMatch = attr.match(/^xmlns:([a-zA-Z0-9]+)=/); + if (!prefixMatch) continue; + const prefix = prefixMatch[1]; + if (used.has(prefix) && !declared.has(prefix)) insert += ` ${attr}`; + } + + if (!insert) return xml; + + const isSelfClosing = /\/>\s*$/.test(root); + const newRoot = isSelfClosing + ? root.replace(/\/>\s*$/, `${insert} />`) + : root.replace(/>$/, `${insert}>`); + return xml.replace(root, newRoot); +} + +async function patchDocxNamespaces(mergedBuffer, mainBuffer) { + const mergedZip = await JSZip.loadAsync(mergedBuffer); + const mainZip = await JSZip.loadAsync(mainBuffer); + + for (const part of PARTS_TO_PATCH) { + const mergedFile = mergedZip.file(part); + const mainFile = mainZip.file(part); + if (!mergedFile || !mainFile) continue; + + const mergedXml = await mergedFile.async('string'); + const mainXml = await mainFile.async('string'); + const patched = patchXmlRootNamespaces(mergedXml, mainXml); + if (patched !== mergedXml) mergedZip.file(part, patched); + } + + return mergedZip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }); +} + +/** + * Склеивает два DOCX: prepend идёт в начало main. + * @param {Buffer|string} prependBuffer + * @param {Buffer|string} mainBuffer + * @returns {Promise} + */ +async function mergeDocxBuffers(prependBuffer, mainBuffer) { + const prependPrepared = await prepareBufferForMerge(prependBuffer, 0); + const mainPrepared = await prepareBufferForMerge(mainBuffer, 1); + const mainBin = toBinary(mainPrepared); + const prependBin = toBinary(prependPrepared); + + return new Promise((resolve, reject) => { + try { + const docx = new DocxMerger({ pageBreak: true }, [prependBin, mainBin]); + docx.save('nodebuffer', async (data) => { + try { + const merged = Buffer.from(data); + const patched = await patchDocxNamespaces( + merged, + Buffer.isBuffer(mainBuffer) ? mainBuffer : Buffer.from(mainBin, 'binary') + ); + resolve(patched); + } catch (err) { + reject(err); + } + }); + } catch (err) { + reject(err); + } + }); +} + +module.exports = { mergeDocxBuffers }; diff --git a/src/util/deepmerge.js b/src/util/deepmerge.js new file mode 100644 index 0000000..ab619ae --- /dev/null +++ b/src/util/deepmerge.js @@ -0,0 +1,31 @@ +'use strict'; + +function isPlainObject(value) { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +/** + * Рекурсивно сливает пользовательский конфиг поверх дефолтов. + * Массивы заменяются целиком (важно для `sections`). + */ +function deepMerge(base, override) { + if (!isPlainObject(base) || !isPlainObject(override)) { + return override === undefined ? base : override; + } + const result = { ...base }; + for (const [key, value] of Object.entries(override)) { + if (isPlainObject(value) && isPlainObject(base[key])) { + result[key] = deepMerge(base[key], value); + } else { + result[key] = value; + } + } + return result; +} + +module.exports = { deepMerge, isPlainObject }; diff --git a/src/util/errors.js b/src/util/errors.js new file mode 100644 index 0000000..c45b113 --- /dev/null +++ b/src/util/errors.js @@ -0,0 +1,19 @@ +'use strict'; + +class ConfigError extends Error { + constructor(message, { errors = [] } = {}) { + super(message); + this.name = 'ConfigError'; + this.errors = errors; + } +} + +class BuildError extends Error { + constructor(message, { errors = [] } = {}) { + super(message); + this.name = 'BuildError'; + this.errors = errors; + } +} + +module.exports = { ConfigError, BuildError }; diff --git a/src/util/logger.js b/src/util/logger.js new file mode 100644 index 0000000..d4ba1a9 --- /dev/null +++ b/src/util/logger.js @@ -0,0 +1,18 @@ +'use strict'; + +function createLogger({ verbose = false } = {}) { + const write = (stream, prefix, args) => { + stream.write(prefix + args.map(String).join(' ') + '\n'); + }; + return { + info: (...args) => write(process.stdout, '', args), + warn: (...args) => write(process.stderr, 'warning: ', args), + error: (...args) => write(process.stderr, 'error: ', args), + debug: (...args) => { + if (verbose) write(process.stdout, 'debug: ', args); + }, + verbose, + }; +} + +module.exports = { createLogger }; diff --git a/src/util/png.js b/src/util/png.js new file mode 100644 index 0000000..49a40be --- /dev/null +++ b/src/util/png.js @@ -0,0 +1,141 @@ +'use strict'; + +const zlib = require('zlib'); + +const CRC_TABLE = (() => { + const table = new Int32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[n] = c; + } + return table; +})(); + +function crc32(buffer) { + let crc = -1; + for (let i = 0; i < buffer.length; i++) { + crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8); + } + return (crc ^ -1) >>> 0; +} + +function chunk(type, data) { + const length = Buffer.alloc(4); + length.writeUInt32BE(data.length, 0); + const typeBuf = Buffer.from(type, 'ascii'); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0); + return Buffer.concat([length, typeBuf, data, crc]); +} + +/** Кодирует изображение, получая цвет пикселя через getPixel(x, y) -> [r,g,b]. */ +function encodePng(width, height, getPixel) { + const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 2; // color type: truecolor + ihdr[10] = 0; + ihdr[11] = 0; + ihdr[12] = 0; + + const rowLength = width * 3; + const raw = Buffer.alloc((rowLength + 1) * height); + for (let y = 0; y < height; y++) { + const offset = y * (rowLength + 1); + raw[offset] = 0; // filter: none + for (let x = 0; x < width; x++) { + const [r, g, b] = getPixel(x, y); + const px = offset + 1 + x * 3; + raw[px] = r; + raw[px + 1] = g; + raw[px + 2] = b; + } + } + + return Buffer.concat([ + signature, + chunk('IHDR', ihdr), + chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), + chunk('IEND', Buffer.alloc(0)), + ]); +} + +function solidPng(width, height, color) { + return encodePng(width, height, () => color); +} + +function mix(a, b, t) { + return [ + Math.round(a[0] + (b[0] - a[0]) * t), + Math.round(a[1] + (b[1] - a[1]) * t), + Math.round(a[2] + (b[2] - a[2]) * t), + ]; +} + +/** Рисунок-заглушка «блок-схема»: рамка и несколько связанных блоков. */ +function diagramPng(width, height, color) { + const bg = [255, 255, 255]; + const fill = mix(color, bg, 0.72); + const cx = width / 2; + const cy = height / 2; + const blocks = [ + { x: 0.06, y: 0.3, w: 0.2, h: 0.4 }, + { x: 0.4, y: 0.12, w: 0.2, h: 0.32 }, + { x: 0.4, y: 0.56, w: 0.2, h: 0.32 }, + { x: 0.74, y: 0.3, w: 0.2, h: 0.4 }, + ]; + return encodePng(width, height, (x, y) => { + const border = + x < 2 || y < 2 || x >= width - 2 || y >= height - 2 ? color : null; + if (border) return border; + if (Math.abs(x - cx) < 1 && y > height * 0.18 && y < height * 0.82) return color; + for (const b of blocks) { + const rx = b.x * width; + const ry = b.y * height; + const rw = b.w * width; + const rh = b.h * height; + const inside = x >= rx && x < rx + rw && y >= ry && y < ry + rh; + const edge = + inside && + (x < rx + 2 || x >= rx + rw - 2 || y < ry + 2 || y >= ry + rh - 2); + if (edge) return color; + if (inside) return fill; + if (y >= ry + rh / 2 - 1 && y <= ry + rh / 2 + 1 && x > rx + rw && x <= cx) return color; + if (Math.abs(y - cy) < 1 && x > cx && x <= rx) return color; + } + return bg; + }); +} + +/** Рисунок-заглушка «столбчатая диаграмма». */ +function chartPng(width, height, color) { + const bg = [255, 255, 255]; + const fill = mix(color, bg, 0.6); + const bars = [0.45, 0.7, 0.35, 0.85, 0.55, 0.65]; + const left = Math.round(width * 0.1); + const right = Math.round(width * 0.92); + const bottom = Math.round(height * 0.85); + const top = Math.round(height * 0.12); + const slot = (right - left) / bars.length; + const barW = slot * 0.55; + return encodePng(width, height, (x, y) => { + if (x < 2 || y < 2 || x >= width - 2 || y >= height - 2) return color; + if (Math.abs(x - left) < 2 || Math.abs(y - bottom) < 2) return color; + if (x < left || x > right || y > bottom) return bg; + for (let i = 0; i < bars.length; i++) { + const bx = left + i * slot + (slot - barW) / 2; + const by = bottom - (bottom - top) * bars[i]; + const inside = x >= bx && x < bx + barW && y >= by && y <= bottom; + if (inside) return Math.abs(x - bx) < 2 ? color : fill; + } + return bg; + }); +} + +module.exports = { solidPng, diagramPng, chartPng, encodePng, crc32 };