From 26888a07d58351755fb8e487727c00d5b611eb95 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 24 Jun 2026 14:22:45 +0300 Subject: [PATCH] =?UTF-8?q?chore(repo):=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20EOL=20=D0=BA=20LF?= =?UTF-8?q?=20+=20.gitattributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Приводим авторский контент (.ps1/.psm1/.py/.mjs/.md/.json, пин .bsl) к единому LF и закрепляем политикой в .gitattributes. Инструмент правки всегда пишет LF, поэтому единый LF убирает EOL-шум в диффах, ложные срабатывания blame и налог на ручную синхронизацию CRLF-файлов. BOM на .ps1 сохранён (git с eol=lf меняет только CR<->LF, BOM не трогает). Данные 1С (*.xml) и бинарники под нормализацию не берём. Гейт: PS-порт 459/459, Python-порт 459/459, web-test E2E 22/22 (с пересборкой стенда). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/cf-edit/SKILL.md | 120 +- .claude/skills/cf-edit/reference.md | 300 +- .claude/skills/cf-edit/scripts/cf-edit.ps1 | 1978 +-- .claude/skills/cf-info/SKILL.md | 108 +- .claude/skills/cf-info/scripts/cf-info.ps1 | 1308 +- .claude/skills/cf-init/SKILL.md | 98 +- .claude/skills/cf-init/scripts/cf-init.ps1 | 498 +- .claude/skills/cf-validate/SKILL.md | 58 +- .../cf-validate/scripts/cf-validate.ps1 | 1222 +- .claude/skills/cfe-borrow/SKILL.md | 202 +- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 3732 ++--- .claude/skills/cfe-diff/SKILL.md | 114 +- .claude/skills/cfe-diff/scripts/cfe-diff.ps1 | 942 +- .claude/skills/cfe-init/SKILL.md | 142 +- .claude/skills/cfe-init/scripts/cfe-init.ps1 | 558 +- .claude/skills/cfe-patch-method/SKILL.md | 156 +- .claude/skills/cfe-validate/SKILL.md | 58 +- .../cfe-validate/scripts/cfe-validate.ps1 | 1878 +-- .claude/skills/db-create/SKILL.md | 140 +- .../skills/db-create/scripts/db-create.ps1 | 440 +- .claude/skills/db-dump-cf/SKILL.md | 136 +- .../skills/db-dump-cf/scripts/db-dump-cf.ps1 | 448 +- .claude/skills/db-dump-dt/SKILL.md | 144 +- .claude/skills/db-dump-xml/SKILL.md | 180 +- .../db-dump-xml/scripts/db-dump-xml.ps1 | 588 +- .claude/skills/db-list/SKILL.md | 316 +- .claude/skills/db-load-cf/SKILL.md | 146 +- .../skills/db-load-cf/scripts/db-load-cf.ps1 | 448 +- .claude/skills/db-load-dt/SKILL.md | 184 +- .claude/skills/db-load-git/SKILL.md | 154 +- .../db-load-git/scripts/db-load-git.ps1 | 890 +- .claude/skills/db-load-xml/SKILL.md | 202 +- .../db-load-xml/scripts/db-load-xml.ps1 | 776 +- .claude/skills/db-run/SKILL.md | 152 +- .claude/skills/db-run/scripts/db-run.ps1 | 340 +- .claude/skills/db-update/SKILL.md | 172 +- .../skills/db-update/scripts/db-update.ps1 | 486 +- .claude/skills/epf-bsp-add-command/SKILL.md | 392 +- .claude/skills/epf-bsp-init/SKILL.md | 416 +- .claude/skills/epf-build/SKILL.md | 138 +- .../skills/epf-build/scripts/epf-build.ps1 | 450 +- .claude/skills/epf-dump/SKILL.md | 138 +- .claude/skills/epf-dump/scripts/epf-dump.ps1 | 448 +- .claude/skills/epf-init/SKILL.md | 82 +- .claude/skills/epf-init/scripts/init.ps1 | 180 +- .claude/skills/epf-validate/SKILL.md | 60 +- .../epf-validate/scripts/epf-validate.ps1 | 1684 +- .claude/skills/erf-build/SKILL.md | 142 +- .claude/skills/erf-dump/SKILL.md | 142 +- .claude/skills/erf-init/SKILL.md | 84 +- .claude/skills/erf-init/scripts/init.ps1 | 360 +- .claude/skills/erf-validate/SKILL.md | 64 +- .claude/skills/form-add/SKILL.md | 142 +- .claude/skills/form-compile/SKILL.md | 1134 +- .../form-compile/scripts/form-compile.ps1 | 13366 ++++++++-------- .../form-decompile/scripts/form-decompile.ps1 | 5916 +++---- .claude/skills/form-edit/SKILL.md | 284 +- .../skills/form-edit/scripts/form-edit.ps1 | 2860 ++-- .claude/skills/form-info/SKILL.md | 60 +- .claude/skills/form-patterns/SKILL.md | 506 +- .claude/skills/form-remove/SKILL.md | 94 +- .../form-remove/scripts/remove-form.ps1 | 178 +- .claude/skills/form-validate/SKILL.md | 58 +- .../form-validate/scripts/form-validate.ps1 | 1740 +- .claude/skills/help-add/SKILL.md | 88 +- .claude/skills/help-add/scripts/add-help.ps1 | 518 +- .claude/skills/img-grid/SKILL.md | 154 +- .claude/skills/interface-edit/SKILL.md | 150 +- .../interface-edit/scripts/interface-edit.ps1 | 1366 +- .claude/skills/interface-validate/SKILL.md | 58 +- .../scripts/interface-validate.ps1 | 806 +- .claude/skills/meta-compile/SKILL.md | 238 +- .../meta-compile/scripts/meta-compile.ps1 | 6490 ++++---- .claude/skills/meta-edit/SKILL.md | 216 +- .claude/skills/meta-edit/child-operations.md | 232 +- .claude/skills/meta-edit/json-dsl.md | 296 +- .../skills/meta-edit/properties-reference.md | 108 +- .../skills/meta-edit/scripts/meta-edit.ps1 | 5076 +++--- .claude/skills/meta-info/SKILL.md | 174 +- .../skills/meta-info/scripts/meta-info.ps1 | 2398 +-- .claude/skills/meta-remove/SKILL.md | 120 +- .claude/skills/meta-validate/SKILL.md | 58 +- .../meta-validate/scripts/meta-validate.ps1 | 2680 ++-- .claude/skills/mxl-compile/SKILL.md | 130 +- .claude/skills/mxl-decompile/SKILL.md | 114 +- .../mxl-decompile/scripts/mxl-decompile.ps1 | 1292 +- .claude/skills/mxl-info/SKILL.md | 264 +- .claude/skills/mxl-info/scripts/mxl-info.ps1 | 1088 +- .claude/skills/mxl-validate/SKILL.md | 58 +- .../mxl-validate/scripts/mxl-validate.ps1 | 846 +- .claude/skills/role-compile/SKILL.md | 218 +- .claude/skills/role-compile/dsl-reference.md | 626 +- .claude/skills/role-info/SKILL.md | 88 +- .../skills/role-info/scripts/role-info.ps1 | 610 +- .claude/skills/role-validate/SKILL.md | 54 +- .../role-validate/scripts/role-validate.ps1 | 1020 +- .claude/skills/skd-compile/SKILL.md | 872 +- .../skd-compile/scripts/skd-compile.ps1 | 7372 ++++----- .claude/skills/skd-edit/SKILL.md | 746 +- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 8098 +++++----- .claude/skills/skd-info/SKILL.md | 160 +- .claude/skills/skd-info/scripts/skd-info.ps1 | 3944 ++--- .claude/skills/skd-validate/SKILL.md | 58 +- .../skd-validate/scripts/skd-validate.ps1 | 1870 +-- .claude/skills/subsystem-compile/SKILL.md | 118 +- .claude/skills/subsystem-edit/SKILL.md | 114 +- .../subsystem-edit/scripts/subsystem-edit.ps1 | 1332 +- .claude/skills/subsystem-info/SKILL.md | 124 +- .../subsystem-info/scripts/subsystem-info.ps1 | 1148 +- .claude/skills/subsystem-validate/SKILL.md | 58 +- .../scripts/subsystem-validate.ps1 | 704 +- .claude/skills/template-add/SKILL.md | 178 +- .../template-add/scripts/add-template.ps1 | 772 +- .claude/skills/template-remove/SKILL.md | 94 +- .../scripts/remove-template.ps1 | 180 +- .claude/skills/web-info/SKILL.md | 124 +- .claude/skills/web-info/scripts/web-info.ps1 | 296 +- .claude/skills/web-publish/SKILL.md | 202 +- .../web-publish/scripts/web-publish.ps1 | 844 +- .claude/skills/web-stop/SKILL.md | 104 +- .claude/skills/web-stop/scripts/web-stop.ps1 | 184 +- .claude/skills/web-test/SKILL.md | 1158 +- .claude/skills/web-test/recording.md | 696 +- .claude/skills/web-test/scripts/browser.mjs | 112 +- .../scripts/engine/core/clipboard.mjs | 194 +- .../web-test/scripts/engine/core/errors.mjs | 620 +- .../web-test/scripts/engine/core/helpers.mjs | 356 +- .../web-test/scripts/engine/core/session.mjs | 808 +- .../web-test/scripts/engine/core/state.mjs | 226 +- .../web-test/scripts/engine/core/wait.mjs | 246 +- .../web-test/scripts/engine/forms/close.mjs | 112 +- .../web-test/scripts/engine/forms/fill.mjs | 294 +- .../scripts/engine/forms/select-value.mjs | 1724 +- .../web-test/scripts/engine/forms/state.mjs | 64 +- .../scripts/engine/nav/navigation.mjs | 506 +- .../scripts/engine/recording/captions.mjs | 584 +- .../scripts/engine/recording/capture.mjs | 486 +- .../scripts/engine/recording/highlight.mjs | 680 +- .../scripts/engine/recording/narration.mjs | 392 +- .../web-test/scripts/engine/recording/tts.mjs | 350 +- .../engine/spreadsheet/spreadsheet.mjs | 1122 +- .../scripts/engine/table/grid-toggle.mjs | 128 +- .../web-test/scripts/engine/table/grid.mjs | 190 +- .../scripts/engine/table/row-fill.mjs | 1926 +-- .../skills/web-test/scripts/package-lock.json | 118 +- .claude/skills/web-test/scripts/package.json | 20 +- .claude/skills/web-unpublish/SKILL.md | 124 +- .../web-unpublish/scripts/web-unpublish.ps1 | 318 +- .gitattributes | 22 + README.md | 558 +- docs/1c-config-objects-spec.md | 3664 ++--- docs/1c-configuration-spec.md | 2048 +-- docs/1c-dcs-spec.md | 2086 +-- docs/1c-erf-spec.md | 1256 +- docs/1c-extension-spec.md | 2480 +-- docs/1c-form-spec.md | 2440 +-- docs/1c-role-spec.md | 1690 +- docs/1c-specs-index.md | 346 +- docs/1c-spreadsheet-spec.md | 898 +- docs/1c-subsystem-spec.md | 2044 +-- docs/cf-guide.md | 364 +- docs/cfe-guide.md | 404 +- docs/db-guide.md | 256 +- docs/epf-guide.md | 380 +- docs/form-dsl-spec.md | 2612 +-- docs/form-guide.md | 532 +- docs/form-patterns.md | 674 +- docs/meta-dsl-spec.md | 2226 +-- docs/meta-guide.md | 374 +- docs/mxl-dsl-spec.md | 320 +- docs/mxl-guide.md | 224 +- docs/python-porting-guide.md | 470 +- docs/role-dsl-spec.md | 224 +- docs/role-guide.md | 310 +- docs/skd-dsl-spec.md | 2696 ++-- docs/skd-guide.md | 452 +- docs/subsystem-guide.md | 410 +- docs/v8-project-guide.md | 502 +- docs/web-guide.md | 294 +- docs/web-spec.md | 406 +- docs/web-test-guide.md | 822 +- docs/web-test-recording-guide.md | 772 +- scripts/switch-to-powershell.py | 120 +- scripts/switch-to-python.py | 112 +- scripts/switch.py | 1544 +- tests/skills/cases/cf-edit/guard-deny.json | 28 +- tests/skills/cases/form-add/guard-deny.json | 18 +- .../form-compile/chart-gantt-settings.json | 702 +- .../cases/form-compile/chart-settings.json | 712 +- .../skills/cases/form-compile/guard-deny.json | 24 +- tests/skills/cases/form-edit/guard-deny.json | 36 +- tests/skills/cases/help-add/guard-deny.json | 12 +- .../cases/interface-edit/guard-deny.json | 24 +- .../skills/cases/mxl-compile/guard-deny.json | 48 +- .../skills/cases/role-compile/guard-deny.json | 18 +- .../skills/cases/skd-compile/guard-deny.json | 32 +- tests/skills/cases/skd-edit/guard-deny.json | 20 +- .../cases/subsystem-compile/guard-deny.json | 16 +- .../cases/subsystem-edit/guard-deny.json | 36 +- tests/skills/cases/support-edit/_skill.json | 30 +- .../cases/support-edit/capability-off.json | 28 +- .../cases/support-edit/capability-on.json | 28 +- .../cases/support-edit/set-editable.json | 28 +- .../cases/support-edit/set-off-support.json | 28 +- .../cases/support-edit/set-prereq-g1.json | 24 +- .../skills/cases/template-add/guard-deny.json | 16 +- 206 files changed, 80389 insertions(+), 80367 deletions(-) create mode 100644 .gitattributes diff --git a/.claude/skills/cf-edit/SKILL.md b/.claude/skills/cf-edit/SKILL.md index c8c1d4b1..b903189a 100644 --- a/.claude/skills/cf-edit/SKILL.md +++ b/.claude/skills/cf-edit/SKILL.md @@ -1,60 +1,60 @@ ---- -name: cf-edit -description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу -argument-hint: -ConfigPath -Operation -Value -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /cf-edit — редактирование конфигурации 1С - -Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | -| `Operation` | Операция (см. таблицу) | -| `Value` | Значение для операции (batch через `;;`) | -| `DefinitionFile` | JSON-файл с массивом операций | -| `NoValidate` | Пропустить авто-валидацию | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '' -Operation modify-property -Value 'Version=1.0.0.1' -``` - -## Операции - -| Операция | Формат Value | Описание | -|----------|-------------|----------| -| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | -| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически | -| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | -| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | -| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | -| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | -| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) | -| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) | - -Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) - -## Примеры - -```powershell -# Изменить версию и поставщика -... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" - -# Добавить объекты -... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" - -# Удалить объект -... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший" - -# Роли по умолчанию -... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава" -... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" -``` +--- +name: cf-edit +description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу +argument-hint: -ConfigPath -Operation -Value +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /cf-edit — редактирование конфигурации 1С + +Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Operation` | Операция (см. таблицу) | +| `Value` | Значение для операции (batch через `;;`) | +| `DefinitionFile` | JSON-файл с массивом операций | +| `NoValidate` | Пропустить авто-валидацию | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '' -Operation modify-property -Value 'Version=1.0.0.1' +``` + +## Операции + +| Операция | Формат Value | Описание | +|----------|-------------|----------| +| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | +| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически | +| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | +| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | +| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | +| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | +| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) | +| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) | + +Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) + +## Примеры + +```powershell +# Изменить версию и поставщика +... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" + +# Добавить объекты +... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" + +# Удалить объект +... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший" + +# Роли по умолчанию +... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава" +... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" +``` diff --git a/.claude/skills/cf-edit/reference.md b/.claude/skills/cf-edit/reference.md index efaa5231..f5b67cdd 100644 --- a/.claude/skills/cf-edit/reference.md +++ b/.claude/skills/cf-edit/reference.md @@ -1,150 +1,150 @@ -# cf-edit — справочник операций - -## modify-property - -Свойства для редактирования: - -### Скалярные -`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` - -### LocalString (многоязычные) -`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` - -### Enum -| Свойство | Допустимые значения | -|----------|---------------------| -| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` | -| `ConfigurationExtensionCompatibilityMode` | то же | -| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | -| `ScriptVariant` | `Russian`, `English` | -| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | -| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | -| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | -| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | -| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` | -| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | -| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | - -### Ref -`DefaultLanguage` — значение вида `Language.Русский` - -### Формат batch -`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` - -## add-childObject / remove-childObject - -Формат: `Type.Name` — XML-тип и имя объекта через точку. - -**Важно про `add-childObject`**: регистрирует в `` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов. - -Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` - -## add-defaultRole / remove-defaultRole / set-defaultRoles - -Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). - -`set-defaultRoles` полностью заменяет список ролей. - -## set-panels - -Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране. - -`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона). - -**Запись** — одна из: -- Строка-алиас (одна панель в этом слоте) -- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом) - -**Алиасы панелей:** - -| Алиас | Панель | -|-------|--------| -| `sections` | Панель разделов | -| `open` | Панель открытых | -| `favorites` | Панель избранного | -| `history` | Панель истории | -| `functions` | Панель функций текущего раздела | - -**Семантика:** -- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов ``/...) -- `{"group":[...]}` → один тег с ``-обёрткой, элементы внутри идут стеком - -**Пример** (DefinitionFile): -```json -[ - { - "operation": "set-panels", - "value": { - "top": ["open"], - "left": ["sections"], - "right": [{ "group": ["favorites", "history"] }], - "bottom": ["functions"] - } - } -] -``` - -Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`. - -## set-home-page - -Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует. - -`value` — объект: - -| Ключ | Канонич. (XML) | Описание | -|------|----------------|----------| -| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` | -| `left` | `LeftColumn` | массив записей форм | -| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) | - -Принимаются и короткие и канонич. ключи (XML-имена) — оба работают. - -**Запись формы** — одна из: -- Строка `"
"` — только имя формы, дефолты `height=10`, `visibility=true` -- Объект `{form, height?, visibility?, roles?}` - -| Поле | Канонич. | Дефолт | Описание | -|------|----------|--------|----------| -| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) | -| `height` | `Height` | `10` | Высота | -| `visibility` | `Visibility` | `true` | Общая видимость (``) | -| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям | - -**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`. - -**Пример:** -```json -[ - { - "operation": "set-home-page", - "value": { - "template": "TwoColumnsVariableWidth", - "left": [ - "CommonForm.НачалоРаботы", - { "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false }, - { "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 }, - { - "form": "CommonForm.РабочийСтолОператора", - "visibility": false, - "roles": { "Role.Оператор": true, "Role.ПолныеПрава": false } - } - ], - "right": [ - { "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 } - ] - } - } -] -``` - -## DefinitionFile (JSON) - -```json -[ - { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, - { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, - { "operation": "add-defaultRole", "value": "ПолныеПрава" } -] -``` - +# cf-edit — справочник операций + +## modify-property + +Свойства для редактирования: + +### Скалярные +`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` + +### LocalString (многоязычные) +`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` + +### Enum +| Свойство | Допустимые значения | +|----------|---------------------| +| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` | +| `ConfigurationExtensionCompatibilityMode` | то же | +| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | +| `ScriptVariant` | `Russian`, `English` | +| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | +| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | +| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` | +| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | +| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | + +### Ref +`DefaultLanguage` — значение вида `Language.Русский` + +### Формат batch +`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` + +## add-childObject / remove-childObject + +Формат: `Type.Name` — XML-тип и имя объекта через точку. + +**Важно про `add-childObject`**: регистрирует в `` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов. + +Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` + +## add-defaultRole / remove-defaultRole / set-defaultRoles + +Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). + +`set-defaultRoles` полностью заменяет список ролей. + +## set-panels + +Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране. + +`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона). + +**Запись** — одна из: +- Строка-алиас (одна панель в этом слоте) +- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом) + +**Алиасы панелей:** + +| Алиас | Панель | +|-------|--------| +| `sections` | Панель разделов | +| `open` | Панель открытых | +| `favorites` | Панель избранного | +| `history` | Панель истории | +| `functions` | Панель функций текущего раздела | + +**Семантика:** +- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов ``/...) +- `{"group":[...]}` → один тег с ``-обёрткой, элементы внутри идут стеком + +**Пример** (DefinitionFile): +```json +[ + { + "operation": "set-panels", + "value": { + "top": ["open"], + "left": ["sections"], + "right": [{ "group": ["favorites", "history"] }], + "bottom": ["functions"] + } + } +] +``` + +Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`. + +## set-home-page + +Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует. + +`value` — объект: + +| Ключ | Канонич. (XML) | Описание | +|------|----------------|----------| +| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` | +| `left` | `LeftColumn` | массив записей форм | +| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) | + +Принимаются и короткие и канонич. ключи (XML-имена) — оба работают. + +**Запись формы** — одна из: +- Строка `""` — только имя формы, дефолты `height=10`, `visibility=true` +- Объект `{form, height?, visibility?, roles?}` + +| Поле | Канонич. | Дефолт | Описание | +|------|----------|--------|----------| +| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) | +| `height` | `Height` | `10` | Высота | +| `visibility` | `Visibility` | `true` | Общая видимость (``) | +| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям | + +**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`. + +**Пример:** +```json +[ + { + "operation": "set-home-page", + "value": { + "template": "TwoColumnsVariableWidth", + "left": [ + "CommonForm.НачалоРаботы", + { "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false }, + { "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 }, + { + "form": "CommonForm.РабочийСтолОператора", + "visibility": false, + "roles": { "Role.Оператор": true, "Role.ПолныеПрава": false } + } + ], + "right": [ + { "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 } + ] + } + } +] +``` + +## DefinitionFile (JSON) + +```json +[ + { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, + { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, + { "operation": "add-defaultRole", "value": "ПолныеПрава" } +] +``` + diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1 index 045fa1fc..1e876400 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.ps1 +++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1 @@ -1,989 +1,989 @@ -# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, - [string]$DefinitionFile, - [ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")] - [string]$Operation, - [string]$Value, - [switch]$NoValidate -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Mode validation --- -if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } -if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath -} -if (Test-Path $ConfigPath -PathType Container) { - $candidate = Join-Path $ConfigPath "Configuration.xml" - if (Test-Path $candidate) { $ConfigPath = $candidate } - else { Write-Error "No Configuration.xml in directory"; exit 1 } -} -if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 } -$resolvedPath = (Resolve-Path $ConfigPath).Path -$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath) - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $resolvedPath 'editable' - -# --- Load XML with PreserveWhitespace --- -$script:xmlDoc = New-Object System.Xml.XmlDocument -$script:xmlDoc.PreserveWhitespace = $true -$script:xmlDoc.Load($resolvedPath) - -$script:addCount = 0 -$script:removeCount = 0 -$script:modifyCount = 0 - -function Info([string]$msg) { Write-Host "[INFO] $msg" } -function Warn([string]$msg) { Write-Host "[WARN] $msg" } - -# --- Detect structure --- -$root = $script:xmlDoc.DocumentElement -$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" -$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" -$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" -$script:v8Ns = "http://v8.1c.ru/8.1/data/core" - -$script:cfgEl = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { - $script:cfgEl = $child; break - } -} -if (-not $script:cfgEl) { Write-Error "No element found"; exit 1 } - -$script:propsEl = $null -$script:childObjsEl = $null -foreach ($child in $script:cfgEl.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -eq "Properties") { $script:propsEl = $child } - if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } -} - -$script:objName = "" -foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { - $script:objName = $child.InnerText.Trim(); break - } -} -Info "Configuration: $($script:objName)" - -# --- Canonical type order for ChildObjects (44 types) --- -$script:typeOrder = @( - "Language","Subsystem","StyleItem","Style", - "CommonPicture","SessionParameter","Role","CommonTemplate", - "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", - "XDTOPackage","WebService","HTTPService","WSReference", - "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", - "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", - "Constant","CommonForm","Catalog","Document", - "DocumentNumerator","Sequence","DocumentJournal","Enum", - "Report","DataProcessor","InformationRegister","AccumulationRegister", - "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", - "ChartOfCalculationTypes","CalculationRegister", - "BusinessProcess","Task","IntegrationService" -) - -# --- Type → on-disk directory name (plural) --- -$script:typeToDir = @{ - "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" - "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates" - "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans" - "XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" - "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" - "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups" - "Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" - "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums" - "Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" - "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" - "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" - "BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices" -} - -# --- XML manipulation helpers (from subsystem-edit pattern) --- -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - $depth = 0; $current = $container - while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Remove-NodeWithWhitespace($node) { - $parent = $node.ParentNode - $prev = $node.PreviousSibling - $next = $node.NextSibling - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($prev) | Out-Null - } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($next) | Out-Null - } - $parent.RemoveChild($node) | Out-Null -} - -function Expand-SelfClosingElement($container, $parentIndent) { - if (-not $container.HasChildNodes -or $container.IsEmpty) { - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } -} - -function Import-Fragment([string]$xmlString) { - $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString" - $frag = New-Object System.Xml.XmlDocument - $frag.PreserveWhitespace = $true - $frag.LoadXml($wrapper) - $nodes = @() - foreach ($child in $frag.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $script:xmlDoc.ImportNode($child, $true) - } - } - return ,$nodes -} - -# --- Parse batch value (split by ;;) --- -function Parse-BatchValue([string]$val) { - $items = @() - foreach ($part in $val.Split(";;")) { - $trimmed = $part.Trim() - if ($trimmed) { $items += $trimmed } - } - return ,$items -} - -# --- LocalString properties --- -$mlProps = @("Synonym","BriefInformation","DetailedInformation","Copyright","VendorInformationAddress","ConfigurationInformationAddress") -# Scalar properties -$scalarProps = @("Name","Version","Vendor","Comment","NamePrefix","UpdateCatalogAddress") -# Ref properties -$refProps = @("DefaultLanguage") - -# --- Operation: modify-property --- -function Do-ModifyProperty([string]$batchVal) { - $items = Parse-BatchValue $batchVal - foreach ($item in $items) { - $eqIdx = $item.IndexOf("=") - if ($eqIdx -lt 1) { - Write-Error "Invalid property format '$item', expected 'Key=Value'" - exit 1 - } - $propName = $item.Substring(0, $eqIdx).Trim() - $propValue = $item.Substring($eqIdx + 1).Trim() - - # Find property element - $propEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { - $propEl = $child; break - } - } - if (-not $propEl) { - Write-Error "Property '$propName' not found in Properties" - exit 1 - } - - if ($mlProps -contains $propName) { - # LocalString - if (-not $propValue) { - $propEl.InnerXml = "" - } else { - $indent = Get-ChildIndent $script:propsEl - $escaped = [System.Security.SecurityElement]::Escape($propValue) - $mlXml = "`r`n$indent`t`r`n$indent`t`tru`r`n$indent`t`t$escaped`r`n$indent`t`r`n$indent" - $propEl.InnerXml = $mlXml - } - } elseif ($scalarProps -contains $propName -or $refProps -contains $propName) { - # Simple text - if (-not $propValue) { $propEl.InnerXml = "" } - else { $propEl.InnerText = $propValue } - } else { - # Enum or other — just set text - $propEl.InnerText = $propValue - } - - $script:modifyCount++ - Info "Set $propName = `"$propValue`"" - } -} - -# --- Operation: add-childObject --- -function Do-AddChildObject([string]$batchVal) { - if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } - - $items = Parse-BatchValue $batchVal - $cfgIndent = Get-ChildIndent $script:cfgEl - - # Expand self-closing if needed - if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { - Expand-SelfClosingElement $script:childObjsEl $cfgIndent - } - $childIndent = Get-ChildIndent $script:childObjsEl - - foreach ($item in $items) { - $dotIdx = $item.IndexOf(".") - if ($dotIdx -lt 1) { - Write-Error "Invalid format '$item', expected 'Type.Name'" - exit 1 - } - $typeName = $item.Substring(0, $dotIdx) - $objNameVal = $item.Substring($dotIdx + 1) - - # Check type is valid - $typeIdx = $script:typeOrder.IndexOf($typeName) - if ($typeIdx -lt 0) { - Write-Error "Unknown type '$typeName'" - exit 1 - } - - # Check that the referenced object actually exists on disk. - # cf-edit add-childObject is a low-level operation for rare scenarios - # (e.g. restoring a rolled-back Configuration.xml when object files are intact). - # For creating NEW objects, meta-compile/role-compile/subsystem-compile already - # auto-register in Configuration.xml — calling cf-edit add-childObject there is - # unnecessary and error-prone. - $typeDir = $script:typeToDir[$typeName] - $objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml" - if (-not (Test-Path $objFile)) { - $hintSkill = switch ($typeName) { - "Subsystem" { "subsystem-compile" } - "Role" { "role-compile" } - default { "meta-compile" } - } - Write-Error @" -Object file not found: $typeDir/$objNameVal.xml -cf-edit add-childObject only references objects that already exist on disk. -To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml): - /$hintSkill with {"type":"$typeName","name":"$objNameVal"} -"@ - exit 1 - } - - # Dedup check - $existing = $false - foreach ($child in $script:childObjsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { - $existing = $true; break - } - } - if ($existing) { - Warn "Already exists: $typeName.$objNameVal" - continue - } - - # Find insertion point: after last element of same type, or after last element of preceding type - $insertBefore = $null - $lastSameType = $null - $lastPrecedingType = $null - $currentTypeIdx = -1 - - foreach ($child in $script:childObjsEl.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) - if ($childTypeIdx -lt 0) { continue } - - if ($child.LocalName -eq $typeName) { - # Same type — check alphabetical order - if ($child.InnerText -gt $objNameVal -and -not $insertBefore) { - # Insert before this element (alphabetical) - $insertBefore = $child - } - $lastSameType = $child - } elseif ($childTypeIdx -lt $typeIdx) { - $lastPrecedingType = $child - } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { - # First element of a later type — insert before it - $insertBefore = $child - } - } - - # Create element - $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) - $newEl.InnerText = $objNameVal - - if ($insertBefore) { - Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent - } else { - # Append at end (or after last same/preceding type) - Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent - } - - $script:addCount++ - Info "Added: $typeName.$objNameVal" - } -} - -# --- Operation: remove-childObject --- -function Do-RemoveChildObject([string]$batchVal) { - if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } - - $items = Parse-BatchValue $batchVal - foreach ($item in $items) { - $dotIdx = $item.IndexOf(".") - if ($dotIdx -lt 1) { - Write-Error "Invalid format '$item', expected 'Type.Name'" - exit 1 - } - $typeName = $item.Substring(0, $dotIdx) - $objNameVal = $item.Substring($dotIdx + 1) - - $found = $false - foreach ($child in @($script:childObjsEl.ChildNodes)) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { - Remove-NodeWithWhitespace $child - $script:removeCount++ - Info "Removed: $typeName.$objNameVal" - $found = $true - break - } - } - if (-not $found) { Warn "Not found: $typeName.$objNameVal" } - } -} - -# --- Operation: add-defaultRole --- -function Do-AddDefaultRole([string]$batchVal) { - $items = Parse-BatchValue $batchVal - - # Find DefaultRoles element - $rolesEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { - $rolesEl = $child; break - } - } - if (-not $rolesEl) { Write-Error "No element found in Properties"; exit 1 } - - $propsIndent = Get-ChildIndent $script:propsEl - if (-not $rolesEl.HasChildNodes -or $rolesEl.IsEmpty) { - Expand-SelfClosingElement $rolesEl $propsIndent - } - $roleIndent = Get-ChildIndent $rolesEl - - foreach ($item in $items) { - $roleName = $item - if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } - - # Dedup - $existing = $false - foreach ($child in $rolesEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { - $existing = $true; break - } - } - if ($existing) { - Warn "DefaultRole already exists: $roleName" - continue - } - - $fragXml = "$roleName" - $nodes = Import-Fragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent - $script:addCount++ - Info "Added DefaultRole: $roleName" - } - } -} - -# --- Operation: remove-defaultRole --- -function Do-RemoveDefaultRole([string]$batchVal) { - $items = Parse-BatchValue $batchVal - - $rolesEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { - $rolesEl = $child; break - } - } - if (-not $rolesEl) { Write-Error "No element found"; exit 1 } - - foreach ($item in $items) { - $roleName = $item - if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } - - $found = $false - foreach ($child in @($rolesEl.ChildNodes)) { - if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { - Remove-NodeWithWhitespace $child - $script:removeCount++ - Info "Removed DefaultRole: $roleName" - $found = $true - break - } - } - if (-not $found) { Warn "DefaultRole not found: $roleName" } - } -} - -# --- Operation: set-panels --- -# Canonical English aliases — preferred form, used in docs and error messages. -$script:panelUuids = @{ - "sections" = "b553047f-c9aa-4157-978d-448ecad24248" - "open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" - "favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3" - "history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99" - "functions" = "b2735bd3-d822-4430-ba59-c9e869693b24" -} -# Russian synonyms — silently accepted (cf-info displays Russian names; users -# may copy them straight into cf-edit value). -$script:panelSynonyms = @{ - "разделов" = "sections"; "разделы" = "sections" - "открытых" = "open"; "открытые" = "open" - "избранного" = "favorites";"избранное" = "favorites" - "истории" = "history"; "история" = "history" - "функций" = "functions";"функции" = "functions" -} - -function Build-PanelEntryXml($entry, [string]$indent) { - # String alias -> ... - if ($entry -is [string]) { - $key = $entry.ToLowerInvariant() - if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] } - if (-not $script:panelUuids.ContainsKey($key)) { - Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')" - exit 1 - } - $u = $script:panelUuids[$key] - $instId = [guid]::NewGuid().ToString() - return "$indent`r`n$indent`t$u`r`n$indent" - } - # Object {group: [...]} -> ... (stack) - if ($entry.PSObject.Properties['group']) { - $children = $entry.group - if (-not $children -or $children.Count -eq 0) { - Write-Error "group must contain at least one entry" - exit 1 - } - $gid = [guid]::NewGuid().ToString() - $inner = "" - foreach ($child in $children) { - $childXml = Build-PanelEntryXml $child "$indent`t`t" - $inner += "$indent`t`r`n$childXml`r`n$indent`t`r`n" - } - return "$indent`r`n$inner$indent" - } - Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)" - exit 1 -} - -function Do-SetPanels($valArg) { - # Accept string (JSON), PSCustomObject, or hashtable - $layout = $valArg - if ($layout -is [string]) { - try { $layout = $layout | ConvertFrom-Json } catch { - Write-Error "set-panels value must be valid JSON object, got: $valArg" - exit 1 - } - } - if (-not $layout) { - Write-Error "set-panels value is empty" - exit 1 - } - - $sides = @("top","left","right","bottom") - $bodyParts = @() - foreach ($side in $sides) { - $entries = $null - if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side } - if ($null -eq $entries) { continue } - # Normalize to array - if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) { - $entries = @($entries) - } - foreach ($entry in $entries) { - $entryXml = Build-PanelEntryXml $entry "`t`t" - $bodyParts += "`t<$side>`r`n$entryXml`r`n`t" - } - } - - # Reject unknown side keys (catches typos like "Top" vs "top") - foreach ($prop in $layout.PSObject.Properties) { - if ($sides -notcontains $prop.Name) { - Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')" - exit 1 - } - } - - $body = $bodyParts -join "`r`n" - $declarations = @" - - - - - -"@ - $bodyBlock = if ($body) { "$body`r`n" } else { "" } - $caiXml = @" - - -$bodyBlock$declarations - -"@ - - $extDir = Join-Path $script:configDir "Ext" - if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } - $caiPath = Join-Path $extDir "ClientApplicationInterface.xml" - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom) - $script:modifyCount++ - Info "Wrote panel layout: $caiPath" -} - -# --- Operation: set-home-page --- -# Russian → English type aliases for form-ref normalization -$script:ruTypeMap = @{ - "справочник" = "Catalog" - "документ" = "Document" - "перечисление" = "Enum" - "отчёт" = "Report" - "отчет" = "Report" - "обработка" = "DataProcessor" - "общаяформа" = "CommonForm" - "журналдокументов" = "DocumentJournal" - "планвидовхарактеристик" = "ChartOfCharacteristicTypes" - "плансчетов" = "ChartOfAccounts" - "планвидоврасчета" = "ChartOfCalculationTypes" - "планвидоврасчёта" = "ChartOfCalculationTypes" - "регистрсведений" = "InformationRegister" - "регистрнакопления" = "AccumulationRegister" - "регистрбухгалтерии" = "AccountingRegister" - "регистррасчета" = "CalculationRegister" - "регистррасчёта" = "CalculationRegister" - "бизнеспроцесс" = "BusinessProcess" - "задача" = "Task" - "планобмена" = "ExchangePlan" - "хранилищенастроек" = "SettingsStorage" -} -# plural folder → singular type -$script:dirToType = @{} -foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k } - -function Normalize-FormRef([string]$s) { - $s = $s.Trim() - if (-not $s) { return $s } - # UUID — leave as-is - if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { return $s } - # Path form? - if ($s.Contains("/") -or $s.Contains("\")) { - $parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" } - # Strip trailing Form.xml - if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") { - $parts = @($parts[0..($parts.Count - 2)]) - } - if ($parts.Count -ge 2) { - $typeDir = $parts[0] - $typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()] - if ($typeSingular) { - if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) { - return "CommonForm.$($parts[1])" - } - if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") { - return "$typeSingular.$($parts[1]).Form.$($parts[3])" - } - } - } - return $s - } - # Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form' - $segs = $s.Split(".") - if ($segs.Count -ge 1) { - $head = $segs[0].ToLowerInvariant() - if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] } - for ($i = 1; $i -lt $segs.Count; $i++) { - if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" } - } - # Auto-insert Form: for object types with 3 segments (Type.Object.FormName) - if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") { - $segs = @($segs[0], $segs[1], "Form", $segs[2]) - } - } - return ($segs -join ".") -} - -# Accept short DSL or canonical XML keys (silently) -function Get-FieldValue($obj, [string[]]$keys) { - foreach ($k in $keys) { - if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value } - } - return $null -} - -function Build-HomePageItemXml($entry, [string]$indent) { - # Resolve fields - if ($entry -is [string]) { - $formRef = Normalize-FormRef $entry - $height = 10 - $common = $true - $roles = $null - } else { - $formRaw = Get-FieldValue $entry @("form","Form") - if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 } - $formRef = Normalize-FormRef ([string]$formRaw) - $h = Get-FieldValue $entry @("height","Height") - $height = if ($null -ne $h) { [int]$h } else { 10 } - $vis = Get-FieldValue $entry @("visibility","Visibility") - $common = if ($null -ne $vis) { [bool]$vis } else { $true } - $roles = Get-FieldValue $entry @("roles") - } - - $visParts = @() - $visParts += "$indent`t`t$($common.ToString().ToLower())" - if ($roles) { - # roles is PSCustomObject {Role.X: bool, ...} - foreach ($prop in $roles.PSObject.Properties) { - $rname = $prop.Name - if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" } - $rval = ([bool]$prop.Value).ToString().ToLower() - $escName = [System.Security.SecurityElement]::Escape($rname) - $visParts += "$indent`t`t$rval" - } - } - $visBlock = $visParts -join "`r`n" - $escForm = [System.Security.SecurityElement]::Escape($formRef) - return @" -$indent -$indent`t$escForm -$indent`t$height -$indent`t -$visBlock -$indent`t -$indent -"@ -} - -function Do-SetHomePage($valArg) { - $layout = $valArg - if ($layout -is [string]) { - try { $layout = $layout | ConvertFrom-Json } catch { - Write-Error "set-home-page value must be valid JSON object"; exit 1 - } - } - if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 } - - $allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth") - $tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate") - if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" } - if ($allowedTemplates -notcontains $tmpl) { - Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1 - } - - $leftItems = Get-FieldValue $layout @("left","LeftColumn") - $rightItems = Get-FieldValue $layout @("right","RightColumn") - - # Reject unknown keys - $known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn") - foreach ($prop in $layout.PSObject.Properties) { - if ($known -notcontains $prop.Name) { - Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1 - } - } - - if ($tmpl -eq "OneColumn" -and $rightItems) { - Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1 - } - - function Build-Column([string]$tag, $items) { - if (-not $items) { return "`t<$tag/>" } - if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) { - $items = @($items) - } - if ($items.Count -eq 0) { return "`t<$tag/>" } - $itemBlocks = @() - foreach ($it in $items) { - $itemBlocks += Build-HomePageItemXml $it "`t`t" - } - $body = $itemBlocks -join "`r`n" - return "`t<$tag>`r`n$body`r`n`t" - } - - $leftXml = Build-Column "LeftColumn" $leftItems - $rightXml = Build-Column "RightColumn" $rightItems - - $hpXml = @" - - - $tmpl -$leftXml -$rightXml - -"@ - - $extDir = Join-Path $script:configDir "Ext" - if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } - $hpPath = Join-Path $extDir "HomePageWorkArea.xml" - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom) - $script:modifyCount++ - Info "Wrote home page layout: $hpPath" -} - -# --- Operation: set-defaultRoles --- -function Do-SetDefaultRoles([string]$batchVal) { - $items = Parse-BatchValue $batchVal - - $rolesEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { - $rolesEl = $child; break - } - } - if (-not $rolesEl) { Write-Error "No element found"; exit 1 } - - # Clear all existing children - while ($rolesEl.HasChildNodes) { - $rolesEl.RemoveChild($rolesEl.FirstChild) | Out-Null - } - - if ($items.Count -eq 0) { - $script:modifyCount++ - Info "Cleared DefaultRoles" - return - } - - $propsIndent = Get-ChildIndent $script:propsEl - $roleIndent = "$propsIndent`t" - - # Add closing whitespace - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$propsIndent") - $rolesEl.AppendChild($closeWs) | Out-Null - - foreach ($item in $items) { - $roleName = $item - if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } - - $fragXml = "$roleName" - $nodes = Import-Fragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent - } - } - - $script:modifyCount++ - Info "Set DefaultRoles: $($items.Count) roles" -} - -# --- Execute operations --- -$operations = @() -if ($DefinitionFile) { - if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { - $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile - } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile - $ops = $jsonText | ConvertFrom-Json - if ($ops -is [System.Array]) { - foreach ($op in $ops) { $operations += $op } - } else { - $operations += $ops - } -} else { - $operations += @{ operation = $Operation; value = $Value } -} - -foreach ($op in $operations) { - $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } - # Pass value through as-is (object or string); set-panels needs object form - $opValue = if ($null -ne $op.value) { $op.value } else { $Value } - $opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" } - - switch ($opName) { - "modify-property" { Do-ModifyProperty $opValueStr } - "add-childObject" { Do-AddChildObject $opValueStr } - "remove-childObject" { Do-RemoveChildObject $opValueStr } - "add-defaultRole" { Do-AddDefaultRole $opValueStr } - "remove-defaultRole" { Do-RemoveDefaultRole $opValueStr } - "set-defaultRoles" { Do-SetDefaultRoles $opValueStr } - "set-panels" { Do-SetPanels $opValue } - "set-home-page" { Do-SetHomePage $opValue } - default { Write-Error "Unknown operation: $opName"; exit 1 } - } -} - -# --- Save --- -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = New-Object System.Text.UTF8Encoding($true) -$settings.Indent = $false -$settings.NewLineHandling = [System.Xml.NewLineHandling]::None - -$memStream = New-Object System.IO.MemoryStream -$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) -$script:xmlDoc.Save($writer) -$writer.Flush(); $writer.Close() - -$bytes = $memStream.ToArray() -$memStream.Close() -$text = [System.Text.Encoding]::UTF8.GetString($bytes) -if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } -$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') - -$utf8Bom = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) -Info "Saved: $resolvedPath" - -# --- Auto-validate --- -if (-not $NoValidate) { - $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\cf-validate") "scripts\cf-validate.ps1" - $validateScript = [System.IO.Path]::GetFullPath($validateScript) - if (Test-Path $validateScript) { - Write-Host "" - Write-Host "--- Running cf-validate ---" - & powershell.exe -NoProfile -File $validateScript -ConfigPath $resolvedPath - } -} - -# --- Summary --- -Write-Host "" -Write-Host "=== cf-edit summary ===" -Write-Host " Configuration: $($script:objName)" -Write-Host " Added: $($script:addCount)" -Write-Host " Removed: $($script:removeCount)" -Write-Host " Modified: $($script:modifyCount)" -exit 0 +# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, + [string]$DefinitionFile, + [ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")] + [string]$Operation, + [string]$Value, + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } +if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in directory"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 } +$resolvedPath = (Resolve-Path $ConfigPath).Path +$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath) + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $resolvedPath 'editable' + +# --- Load XML with PreserveWhitespace --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- Detect structure --- +$root = $script:xmlDoc.DocumentElement +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +$script:objName = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { + $script:objName = $child.InnerText.Trim(); break + } +} +Info "Configuration: $($script:objName)" + +# --- Canonical type order for ChildObjects (44 types) --- +$script:typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# --- Type → on-disk directory name (plural) --- +$script:typeToDir = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates" + "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans" + "XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups" + "Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums" + "Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices" +} + +# --- XML manipulation helpers (from subsystem-edit pattern) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +function Import-Fragment([string]$xmlString) { + $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString" + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +# --- Parse batch value (split by ;;) --- +function Parse-BatchValue([string]$val) { + $items = @() + foreach ($part in $val.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } + } + return ,$items +} + +# --- LocalString properties --- +$mlProps = @("Synonym","BriefInformation","DetailedInformation","Copyright","VendorInformationAddress","ConfigurationInformationAddress") +# Scalar properties +$scalarProps = @("Name","Version","Vendor","Comment","NamePrefix","UpdateCatalogAddress") +# Ref properties +$refProps = @("DefaultLanguage") + +# --- Operation: modify-property --- +function Do-ModifyProperty([string]$batchVal) { + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $eqIdx = $item.IndexOf("=") + if ($eqIdx -lt 1) { + Write-Error "Invalid property format '$item', expected 'Key=Value'" + exit 1 + } + $propName = $item.Substring(0, $eqIdx).Trim() + $propValue = $item.Substring($eqIdx + 1).Trim() + + # Find property element + $propEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + $propEl = $child; break + } + } + if (-not $propEl) { + Write-Error "Property '$propName' not found in Properties" + exit 1 + } + + if ($mlProps -contains $propName) { + # LocalString + if (-not $propValue) { + $propEl.InnerXml = "" + } else { + $indent = Get-ChildIndent $script:propsEl + $escaped = [System.Security.SecurityElement]::Escape($propValue) + $mlXml = "`r`n$indent`t`r`n$indent`t`tru`r`n$indent`t`t$escaped`r`n$indent`t`r`n$indent" + $propEl.InnerXml = $mlXml + } + } elseif ($scalarProps -contains $propName -or $refProps -contains $propName) { + # Simple text + if (-not $propValue) { $propEl.InnerXml = "" } + else { $propEl.InnerText = $propValue } + } else { + # Enum or other — just set text + $propEl.InnerText = $propValue + } + + $script:modifyCount++ + Info "Set $propName = `"$propValue`"" + } +} + +# --- Operation: add-childObject --- +function Do-AddChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + # Check type is valid + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + # Check that the referenced object actually exists on disk. + # cf-edit add-childObject is a low-level operation for rare scenarios + # (e.g. restoring a rolled-back Configuration.xml when object files are intact). + # For creating NEW objects, meta-compile/role-compile/subsystem-compile already + # auto-register in Configuration.xml — calling cf-edit add-childObject there is + # unnecessary and error-prone. + $typeDir = $script:typeToDir[$typeName] + $objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml" + if (-not (Test-Path $objFile)) { + $hintSkill = switch ($typeName) { + "Subsystem" { "subsystem-compile" } + "Role" { "role-compile" } + default { "meta-compile" } + } + Write-Error @" +Object file not found: $typeDir/$objNameVal.xml +cf-edit add-childObject only references objects that already exist on disk. +To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml): + /$hintSkill with {"type":"$typeName","name":"$objNameVal"} +"@ + exit 1 + } + + # Dedup check + $existing = $false + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + $existing = $true; break + } + } + if ($existing) { + Warn "Already exists: $typeName.$objNameVal" + continue + } + + # Find insertion point: after last element of same type, or after last element of preceding type + $insertBefore = $null + $lastSameType = $null + $lastPrecedingType = $null + $currentTypeIdx = -1 + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type — check alphabetical order + if ($child.InnerText -gt $objNameVal -and -not $insertBefore) { + # Insert before this element (alphabetical) + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -lt $typeIdx) { + $lastPrecedingType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type — insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objNameVal + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + # Append at end (or after last same/preceding type) + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + $script:addCount++ + Info "Added: $typeName.$objNameVal" + } +} + +# --- Operation: remove-childObject --- +function Do-RemoveChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + $found = $false + foreach ($child in @($script:childObjsEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed: $typeName.$objNameVal" + $found = $true + break + } + } + if (-not $found) { Warn "Not found: $typeName.$objNameVal" } + } +} + +# --- Operation: add-defaultRole --- +function Do-AddDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + # Find DefaultRoles element + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found in Properties"; exit 1 } + + $propsIndent = Get-ChildIndent $script:propsEl + if (-not $rolesEl.HasChildNodes -or $rolesEl.IsEmpty) { + Expand-SelfClosingElement $rolesEl $propsIndent + } + $roleIndent = Get-ChildIndent $rolesEl + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + # Dedup + $existing = $false + foreach ($child in $rolesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + $existing = $true; break + } + } + if ($existing) { + Warn "DefaultRole already exists: $roleName" + continue + } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + $script:addCount++ + Info "Added DefaultRole: $roleName" + } + } +} + +# --- Operation: remove-defaultRole --- +function Do-RemoveDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $found = $false + foreach ($child in @($rolesEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed DefaultRole: $roleName" + $found = $true + break + } + } + if (-not $found) { Warn "DefaultRole not found: $roleName" } + } +} + +# --- Operation: set-panels --- +# Canonical English aliases — preferred form, used in docs and error messages. +$script:panelUuids = @{ + "sections" = "b553047f-c9aa-4157-978d-448ecad24248" + "open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" + "favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3" + "history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99" + "functions" = "b2735bd3-d822-4430-ba59-c9e869693b24" +} +# Russian synonyms — silently accepted (cf-info displays Russian names; users +# may copy them straight into cf-edit value). +$script:panelSynonyms = @{ + "разделов" = "sections"; "разделы" = "sections" + "открытых" = "open"; "открытые" = "open" + "избранного" = "favorites";"избранное" = "favorites" + "истории" = "history"; "история" = "history" + "функций" = "functions";"функции" = "functions" +} + +function Build-PanelEntryXml($entry, [string]$indent) { + # String alias -> ... + if ($entry -is [string]) { + $key = $entry.ToLowerInvariant() + if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] } + if (-not $script:panelUuids.ContainsKey($key)) { + Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')" + exit 1 + } + $u = $script:panelUuids[$key] + $instId = [guid]::NewGuid().ToString() + return "$indent`r`n$indent`t$u`r`n$indent" + } + # Object {group: [...]} -> ... (stack) + if ($entry.PSObject.Properties['group']) { + $children = $entry.group + if (-not $children -or $children.Count -eq 0) { + Write-Error "group must contain at least one entry" + exit 1 + } + $gid = [guid]::NewGuid().ToString() + $inner = "" + foreach ($child in $children) { + $childXml = Build-PanelEntryXml $child "$indent`t`t" + $inner += "$indent`t`r`n$childXml`r`n$indent`t`r`n" + } + return "$indent`r`n$inner$indent" + } + Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)" + exit 1 +} + +function Do-SetPanels($valArg) { + # Accept string (JSON), PSCustomObject, or hashtable + $layout = $valArg + if ($layout -is [string]) { + try { $layout = $layout | ConvertFrom-Json } catch { + Write-Error "set-panels value must be valid JSON object, got: $valArg" + exit 1 + } + } + if (-not $layout) { + Write-Error "set-panels value is empty" + exit 1 + } + + $sides = @("top","left","right","bottom") + $bodyParts = @() + foreach ($side in $sides) { + $entries = $null + if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side } + if ($null -eq $entries) { continue } + # Normalize to array + if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) { + $entries = @($entries) + } + foreach ($entry in $entries) { + $entryXml = Build-PanelEntryXml $entry "`t`t" + $bodyParts += "`t<$side>`r`n$entryXml`r`n`t" + } + } + + # Reject unknown side keys (catches typos like "Top" vs "top") + foreach ($prop in $layout.PSObject.Properties) { + if ($sides -notcontains $prop.Name) { + Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')" + exit 1 + } + } + + $body = $bodyParts -join "`r`n" + $declarations = @" + + + + + +"@ + $bodyBlock = if ($body) { "$body`r`n" } else { "" } + $caiXml = @" + + +$bodyBlock$declarations + +"@ + + $extDir = Join-Path $script:configDir "Ext" + if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } + $caiPath = Join-Path $extDir "ClientApplicationInterface.xml" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom) + $script:modifyCount++ + Info "Wrote panel layout: $caiPath" +} + +# --- Operation: set-home-page --- +# Russian → English type aliases for form-ref normalization +$script:ruTypeMap = @{ + "справочник" = "Catalog" + "документ" = "Document" + "перечисление" = "Enum" + "отчёт" = "Report" + "отчет" = "Report" + "обработка" = "DataProcessor" + "общаяформа" = "CommonForm" + "журналдокументов" = "DocumentJournal" + "планвидовхарактеристик" = "ChartOfCharacteristicTypes" + "плансчетов" = "ChartOfAccounts" + "планвидоврасчета" = "ChartOfCalculationTypes" + "планвидоврасчёта" = "ChartOfCalculationTypes" + "регистрсведений" = "InformationRegister" + "регистрнакопления" = "AccumulationRegister" + "регистрбухгалтерии" = "AccountingRegister" + "регистррасчета" = "CalculationRegister" + "регистррасчёта" = "CalculationRegister" + "бизнеспроцесс" = "BusinessProcess" + "задача" = "Task" + "планобмена" = "ExchangePlan" + "хранилищенастроек" = "SettingsStorage" +} +# plural folder → singular type +$script:dirToType = @{} +foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k } + +function Normalize-FormRef([string]$s) { + $s = $s.Trim() + if (-not $s) { return $s } + # UUID — leave as-is + if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { return $s } + # Path form? + if ($s.Contains("/") -or $s.Contains("\")) { + $parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" } + # Strip trailing Form.xml + if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") { + $parts = @($parts[0..($parts.Count - 2)]) + } + if ($parts.Count -ge 2) { + $typeDir = $parts[0] + $typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()] + if ($typeSingular) { + if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) { + return "CommonForm.$($parts[1])" + } + if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") { + return "$typeSingular.$($parts[1]).Form.$($parts[3])" + } + } + } + return $s + } + # Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form' + $segs = $s.Split(".") + if ($segs.Count -ge 1) { + $head = $segs[0].ToLowerInvariant() + if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] } + for ($i = 1; $i -lt $segs.Count; $i++) { + if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" } + } + # Auto-insert Form: for object types with 3 segments (Type.Object.FormName) + if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") { + $segs = @($segs[0], $segs[1], "Form", $segs[2]) + } + } + return ($segs -join ".") +} + +# Accept short DSL or canonical XML keys (silently) +function Get-FieldValue($obj, [string[]]$keys) { + foreach ($k in $keys) { + if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value } + } + return $null +} + +function Build-HomePageItemXml($entry, [string]$indent) { + # Resolve fields + if ($entry -is [string]) { + $formRef = Normalize-FormRef $entry + $height = 10 + $common = $true + $roles = $null + } else { + $formRaw = Get-FieldValue $entry @("form","Form") + if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 } + $formRef = Normalize-FormRef ([string]$formRaw) + $h = Get-FieldValue $entry @("height","Height") + $height = if ($null -ne $h) { [int]$h } else { 10 } + $vis = Get-FieldValue $entry @("visibility","Visibility") + $common = if ($null -ne $vis) { [bool]$vis } else { $true } + $roles = Get-FieldValue $entry @("roles") + } + + $visParts = @() + $visParts += "$indent`t`t$($common.ToString().ToLower())" + if ($roles) { + # roles is PSCustomObject {Role.X: bool, ...} + foreach ($prop in $roles.PSObject.Properties) { + $rname = $prop.Name + if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" } + $rval = ([bool]$prop.Value).ToString().ToLower() + $escName = [System.Security.SecurityElement]::Escape($rname) + $visParts += "$indent`t`t$rval" + } + } + $visBlock = $visParts -join "`r`n" + $escForm = [System.Security.SecurityElement]::Escape($formRef) + return @" +$indent +$indent`t
$escForm
+$indent`t$height +$indent`t +$visBlock +$indent`t +$indent
+"@ +} + +function Do-SetHomePage($valArg) { + $layout = $valArg + if ($layout -is [string]) { + try { $layout = $layout | ConvertFrom-Json } catch { + Write-Error "set-home-page value must be valid JSON object"; exit 1 + } + } + if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 } + + $allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth") + $tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate") + if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" } + if ($allowedTemplates -notcontains $tmpl) { + Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1 + } + + $leftItems = Get-FieldValue $layout @("left","LeftColumn") + $rightItems = Get-FieldValue $layout @("right","RightColumn") + + # Reject unknown keys + $known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn") + foreach ($prop in $layout.PSObject.Properties) { + if ($known -notcontains $prop.Name) { + Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1 + } + } + + if ($tmpl -eq "OneColumn" -and $rightItems) { + Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1 + } + + function Build-Column([string]$tag, $items) { + if (-not $items) { return "`t<$tag/>" } + if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) { + $items = @($items) + } + if ($items.Count -eq 0) { return "`t<$tag/>" } + $itemBlocks = @() + foreach ($it in $items) { + $itemBlocks += Build-HomePageItemXml $it "`t`t" + } + $body = $itemBlocks -join "`r`n" + return "`t<$tag>`r`n$body`r`n`t" + } + + $leftXml = Build-Column "LeftColumn" $leftItems + $rightXml = Build-Column "RightColumn" $rightItems + + $hpXml = @" + + + $tmpl +$leftXml +$rightXml + +"@ + + $extDir = Join-Path $script:configDir "Ext" + if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } + $hpPath = Join-Path $extDir "HomePageWorkArea.xml" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom) + $script:modifyCount++ + Info "Wrote home page layout: $hpPath" +} + +# --- Operation: set-defaultRoles --- +function Do-SetDefaultRoles([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + # Clear all existing children + while ($rolesEl.HasChildNodes) { + $rolesEl.RemoveChild($rolesEl.FirstChild) | Out-Null + } + + if ($items.Count -eq 0) { + $script:modifyCount++ + Info "Cleared DefaultRoles" + return + } + + $propsIndent = Get-ChildIndent $script:propsEl + $roleIndent = "$propsIndent`t" + + # Add closing whitespace + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$propsIndent") + $rolesEl.AppendChild($closeWs) | Out-Null + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + } + } + + $script:modifyCount++ + Info "Set DefaultRoles: $($items.Count) roles" +} + +# --- Execute operations --- +$operations = @() +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $ops = $jsonText | ConvertFrom-Json + if ($ops -is [System.Array]) { + foreach ($op in $ops) { $operations += $op } + } else { + $operations += $ops + } +} else { + $operations += @{ operation = $Operation; value = $Value } +} + +foreach ($op in $operations) { + $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } + # Pass value through as-is (object or string); set-panels needs object form + $opValue = if ($null -ne $op.value) { $op.value } else { $Value } + $opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" } + + switch ($opName) { + "modify-property" { Do-ModifyProperty $opValueStr } + "add-childObject" { Do-AddChildObject $opValueStr } + "remove-childObject" { Do-RemoveChildObject $opValueStr } + "add-defaultRole" { Do-AddDefaultRole $opValueStr } + "remove-defaultRole" { Do-RemoveDefaultRole $opValueStr } + "set-defaultRoles" { Do-SetDefaultRoles $opValueStr } + "set-panels" { Do-SetPanels $opValue } + "set-home-page" { Do-SetHomePage $opValue } + default { Write-Error "Unknown operation: $opName"; exit 1 } + } +} + +# --- Save --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) +Info "Saved: $resolvedPath" + +# --- Auto-validate --- +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\cf-validate") "scripts\cf-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running cf-validate ---" + & powershell.exe -NoProfile -File $validateScript -ConfigPath $resolvedPath + } +} + +# --- Summary --- +Write-Host "" +Write-Host "=== cf-edit summary ===" +Write-Host " Configuration: $($script:objName)" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +exit 0 diff --git a/.claude/skills/cf-info/SKILL.md b/.claude/skills/cf-info/SKILL.md index dedc0382..32cc9166 100644 --- a/.claude/skills/cf-info/SKILL.md +++ b/.claude/skills/cf-info/SKILL.md @@ -1,54 +1,54 @@ ---- -name: cf-info -description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки -argument-hint: [-Mode overview|brief|full] [-Section home-page] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cf-info — Структура конфигурации 1С - -Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | -| `Mode` | Режим: `overview` (default), `brief`, `full` | -| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` | -| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | -| `OutFile` | Записать результат в файл (UTF-8 BOM) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" -``` - -## Три режима - -| Режим | Что показывает | -|---|---| -| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | -| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | -| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | - -## Примеры - -```powershell -# Обзор пустой конфигурации -... -ConfigPath src - -# Краткая сводка реальной конфигурации -... -ConfigPath src -Mode brief - -# Полная информация -... -ConfigPath src -Mode full - -# С пагинацией -... -ConfigPath src -Mode full -Limit 50 -Offset 100 - -# Drill-down: только начальная страница (раскладка форм с ролями) -... -ConfigPath src -Section home-page -``` +--- +name: cf-info +description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки +argument-hint: [-Mode overview|brief|full] [-Section home-page] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-info — Структура конфигурации 1С + +Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Mode` | Режим: `overview` (default), `brief`, `full` | +| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" +``` + +## Три режима + +| Режим | Что показывает | +|---|---| +| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | +| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | +| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | + +## Примеры + +```powershell +# Обзор пустой конфигурации +... -ConfigPath src + +# Краткая сводка реальной конфигурации +... -ConfigPath src -Mode brief + +# Полная информация +... -ConfigPath src -Mode full + +# С пагинацией +... -ConfigPath src -Mode full -Limit 50 -Offset 100 + +# Drill-down: только начальная страница (раскладка форм с ролями) +... -ConfigPath src -Section home-page +``` diff --git a/.claude/skills/cf-info/scripts/cf-info.ps1 b/.claude/skills/cf-info/scripts/cf-info.ps1 index c9ce3072..be050f68 100644 --- a/.claude/skills/cf-info/scripts/cf-info.ps1 +++ b/.claude/skills/cf-info/scripts/cf-info.ps1 @@ -1,654 +1,654 @@ -# cf-info v1.3 — Compact summary of 1C configuration root -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, - [ValidateSet("overview","brief","full")] - [string]$Mode = "overview", - [Alias('Name')] - [ValidateSet("home-page")] - [string]$Section, - [int]$Limit = 150, - [int]$Offset = 0, - [string]$OutFile -) - -$ErrorActionPreference = 'Stop' -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Output helper (always collect, paginate at the end) --- -$script:lines = @() -function Out([string]$text) { $script:lines += $text } - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath -} - -# Directory -> find Configuration.xml -if (Test-Path $ConfigPath -PathType Container) { - $candidate = Join-Path $ConfigPath "Configuration.xml" - if (Test-Path $candidate) { - $ConfigPath = $candidate - } else { - Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" - exit 1 - } -} - -if (-not (Test-Path $ConfigPath)) { - Write-Host "[ERROR] File not found: $ConfigPath" - exit 1 -} - -# --- Load XML --- -[xml]$xmlDoc = Get-Content -Path $ConfigPath -Encoding UTF8 -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") - -$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) -if (-not $mdRoot) { - Write-Host "[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)" - exit 1 -} - -$cfgNode = $mdRoot.SelectSingleNode("md:Configuration", $ns) -if (-not $cfgNode) { - Write-Host "[ERROR] No element found" - exit 1 -} - -$version = $mdRoot.GetAttribute("version") -$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) -$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) - -# --- Helpers --- -function Get-MLText($node) { - if (-not $node) { return "" } - $item = $node.SelectSingleNode("v8:item/v8:content", $ns) - if ($item -and $item.InnerText) { return $item.InnerText } - return "" -} - -function Get-PropText([string]$propName) { - $n = $propsNode.SelectSingleNode("md:$propName", $ns) - if ($n -and $n.InnerText) { return $n.InnerText } - return "" -} - -function Get-PropML([string]$propName) { - $n = $propsNode.SelectSingleNode("md:$propName", $ns) - return (Get-MLText $n) -} - -# --- Type name maps (canonical order, 44 types) --- -$typeOrder = @( - "Language","Subsystem","StyleItem","Style", - "CommonPicture","SessionParameter","Role","CommonTemplate", - "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", - "XDTOPackage","WebService","HTTPService","WSReference", - "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", - "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", - "Constant","CommonForm","Catalog","Document", - "DocumentNumerator","Sequence","DocumentJournal","Enum", - "Report","DataProcessor","InformationRegister","AccumulationRegister", - "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", - "ChartOfCalculationTypes","CalculationRegister", - "BusinessProcess","Task","IntegrationService" -) - -$typeRuNames = @{ - "Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили" - "CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли" - "CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули" - "CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты" - "WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки" - "EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания" - "SettingsStorage"="Хранилища настроек"; "FunctionalOption"="Функциональные опции" - "FunctionalOptionsParameter"="Параметры ФО"; "DefinedType"="Определяемые типы" - "CommonCommand"="Общие команды"; "CommandGroup"="Группы команд"; "Constant"="Константы" - "CommonForm"="Общие формы"; "Catalog"="Справочники"; "Document"="Документы" - "DocumentNumerator"="Нумераторы"; "Sequence"="Последовательности"; "DocumentJournal"="Журналы документов" - "Enum"="Перечисления"; "Report"="Отчёты"; "DataProcessor"="Обработки" - "InformationRegister"="Регистры сведений"; "AccumulationRegister"="Регистры накопления" - "ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов" - "AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР" - "CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы" - "Task"="Задачи"; "IntegrationService"="Сервисы интеграции" -} - -# --- Read panel layout (Ext/ClientApplicationInterface.xml) --- -$script:panelNames = @{ - "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых" - "b553047f-c9aa-4157-978d-448ecad24248" = "Разделов" - "13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного" - "c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История" - "b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций" -} - -function Get-PanelsLayout { - $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) - $caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml" - if (-not (Test-Path $caiPath)) { return $null } - try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null } - if (-not $caiDoc.DocumentElement) { return $null } - $caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable) - $caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core") - $layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() } - foreach ($side in @("top","left","right","bottom")) { - foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) { - $slot = @() - foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) { - $key = $u.InnerText.Trim() - $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } - $slot += $nm - } - if ($slot.Count -gt 0) { $layout[$side] += ,$slot } - } - } - foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) { - $key = $pd.GetAttribute("id") - $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } - $layout.declared += $nm - } - return $layout -} - -function Format-LayoutSlots($slots) { - # slots is array of arrays (each inner array = one side-tag's panels, may be 1+) - # Single inner array, single panel -> just name - # Single inner array, multiple panels -> "Стек(a, b)" - # Multiple inner arrays -> separate entries joined by " | " - if (-not $slots -or $slots.Count -eq 0) { return "" } - $parts = @() - foreach ($slot in $slots) { - if ($slot.Count -eq 1) { $parts += $slot[0] } - else { $parts += ("Стек(" + ($slot -join ", ") + ")") } - } - return ($parts -join " | ") -} - -$script:panelLayout = Get-PanelsLayout - -# --- Read home page layout (Ext/HomePageWorkArea.xml) --- -function Get-HomePageLayout { - $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) - $hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" - if (-not (Test-Path $hpPath)) { return $null } - try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null } - if (-not $hpDoc.DocumentElement) { return $null } - $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) - $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") - $hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $result = [ordered]@{ template = ""; left = @(); right = @() } - $tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs) - if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() } - foreach ($colName in @("LeftColumn","RightColumn")) { - $colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs) - if (-not $colNode) { continue } - $items = @() - foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) { - $f = $item.SelectSingleNode("hp:Form", $hpNs) - $h = $item.SelectSingleNode("hp:Height", $hpNs) - $visNode = $item.SelectSingleNode("hp:Visibility", $hpNs) - $common = $true - $roles = @() - if ($visNode) { - $cn = $visNode.SelectSingleNode("xr:Common", $hpNs) - if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") } - foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) { - $roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") } - } - } - $items += [ordered]@{ - form = if ($f) { $f.InnerText.Trim() } else { "" } - height = if ($h) { [int]$h.InnerText.Trim() } else { 10 } - common = $common - roles = $roles - } - } - if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items } - } - return $result -} - -$script:homePage = Get-HomePageLayout - -# --- Support state (Ext/ParentConfigurations.bin) --- -# Decodes the 1C support-state file. See docs/1c-support-state-spec.md. -# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed', -# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}), -# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each -# vendor block separately); only computed when G=0. -function Read-SupportState([string]$binPath) { - try { - if (-not (Test-Path $binPath)) { return @{ State = 'absent' } } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return @{ State = 'removed' } } - $startIdx = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return $null } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return @{ State = 'removed' } } - # Vendor descriptors: ...,"ver","vendor","name",count, - $vendors = @() - $vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,' - foreach ($m in $vRe.Matches($text)) { - $vendors += @{ - Version = ($m.Groups[1].Value -replace '""','"') - Vendor = ($m.Groups[2].Value -replace '""','"') - Name = ($m.Groups[3].Value -replace '""','"') - } - } - # Per-object counts only matter when editing is enabled (G=0); when G=1 the - # whole config is read-only and stored f1 values are the inactive default. - $counts = $null - if ($G -eq 0) { - $counts = @(0, 0, 0) - # Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid. - $rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' - foreach ($m in $rRe.Matches($text)) { - $counts[[int]$m.Groups[1].Value]++ - } - } - return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts } - } catch { return $null } -} - -function Get-SupportLines { - $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) - $binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin" - $st = Read-SupportState $binPath - $out = @() - if (-not $st -or $st.State -eq 'absent') { - if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" } - else { $out += "Поддержка: не на поддержке (своя конфигурация)" } - return $out - } - if ($st.State -eq 'removed') { - $out += "Поддержка: снята с поддержки полностью" - return $out - } - $out += "Поддержка: на поддержке" - if ($st.G -eq 0) { - $out += " Возможность изменения: включена" - $out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])" - } else { - $out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)" - } - $out += " Конфигураций поставщика: $($st.K)" - if ($st.K -gt 1) { - foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" } - } - return $out -} - -function Format-HomePageItem($it, [bool]$detailed) { - $badges = @() - $badges += "h=$($it.height)" - if (-not $it.common) { $badges += "скрыта" } - if ($it.roles.Count -gt 0) { - if ($detailed) { $badges += "роли: $($it.roles.Count)" } - else { $badges += "+$($it.roles.Count) ролей" } - } - $tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" } - return " $($it.form)$tail" -} - -# --- Count objects in ChildObjects --- -$objectCounts = [ordered]@{} -$totalObjects = 0 - -if ($childObjNode) { - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - if (-not $objectCounts.Contains($typeName)) { - $objectCounts[$typeName] = 0 - } - $objectCounts[$typeName] = $objectCounts[$typeName] + 1 - $totalObjects++ - } -} - -# --- Read key properties --- -$cfgName = Get-PropText "Name" -$cfgSynonym = Get-PropML "Synonym" -$cfgVersion = Get-PropText "Version" -$cfgVendor = Get-PropText "Vendor" -$cfgCompat = Get-PropText "CompatibilityMode" -$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode" -$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose" -$cfgDefaultRun = Get-PropText "DefaultRunMode" -$cfgScript = Get-PropText "ScriptVariant" -$cfgDefaultLang = Get-PropText "DefaultLanguage" -$cfgDataLock = Get-PropText "DataLockControlMode" -$dash = [char]0x2014 -$cfgModality = Get-PropText "ModalityUseMode" -$cfgIntfCompat = Get-PropText "InterfaceCompatibilityMode" -$cfgAutoNum = Get-PropText "ObjectAutonumerationMode" -$cfgSyncCalls = Get-PropText "SynchronousPlatformExtensionAndAddInCallUseMode" -$cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode" -$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode" - -# --- BRIEF mode --- -if ($Mode -eq "brief" -and -not $Section) { - $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } - $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } - $compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" } - Out "Конфигурация: ${cfgName}${synPart}${verPart} | $totalObjects объектов${compatPart}" -} - -# --- OVERVIEW mode --- -if ($Mode -eq "overview" -and -not $Section) { - $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } - $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } - Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" - Out "" - - # Key properties - Out "Формат: $version" - if ($cfgVendor) { Out "Поставщик: $cfgVendor" } - if ($cfgVersion) { Out "Версия: $cfgVersion" } - foreach ($l in (Get-SupportLines)) { Out $l } - Out "Совместимость: $cfgCompat" - Out "Режим запуска: $cfgDefaultRun" - Out "Язык скриптов: $cfgScript" - Out "Язык: $cfgDefaultLang" - Out "Блокировки: $cfgDataLock" - Out "Модальность: $cfgModality" - Out "Интерфейс: $cfgIntfCompat" - Out "" - - # Panel layout (if file exists) - if ($script:panelLayout) { - $hasPlaced = $false - foreach ($s in @("top","left","right","bottom")) { - if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break } - } - if ($hasPlaced) { - Out "--- Раскладка панелей ---" - foreach ($s in @("top","left","right","bottom")) { - if ($script:panelLayout[$s].Count -gt 0) { - Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])" - } - } - Out "" - } - } - - # Home page layout (brief summary) - if ($script:homePage) { - $ln = $script:homePage.left.Count - $rn = $script:homePage.right.Count - Out "--- Начальная страница ---" - Out " Шаблон: $($script:homePage.template)" - Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" - Out "" - } - - # Object counts table - Out "--- Состав ($totalObjects объектов) ---" - Out "" - $maxTypeLen = 0 - foreach ($typeName in $typeOrder) { - if ($objectCounts.Contains($typeName)) { - $ruName = $typeRuNames[$typeName] - if ($ruName.Length -gt $maxTypeLen) { $maxTypeLen = $ruName.Length } - } - } - if ($maxTypeLen -lt 10) { $maxTypeLen = 10 } - - foreach ($typeName in $typeOrder) { - if ($objectCounts.Contains($typeName)) { - $count = $objectCounts[$typeName] - $ruName = $typeRuNames[$typeName] - $padded = $ruName.PadRight($maxTypeLen) - Out " $padded $count" - } - } -} - -# --- Drill-down: -Section home-page --- -if ($Section -eq "home-page") { - if (-not $script:homePage) { - Out "Файл Ext/HomePageWorkArea.xml не найден" - } else { - Out "=== Начальная страница: $cfgName ===" - Out "" - Out "Шаблон: $($script:homePage.template)" - Out "" - foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) { - $items = $script:homePage[$side[1]] - $lbl = $side[0] - if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue } - Out "${lbl} ($($items.Count)):" - foreach ($it in $items) { - Out (Format-HomePageItem $it $true) - foreach ($r in $it.roles) { - $rval = if ($r.value) { "true" } else { "false" } - Out " $($r.name): $rval" - } - } - Out "" - } - } -} - -# --- FULL mode --- -if ($Mode -eq "full" -and -not $Section) { - $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } - $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } - Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" - Out "" - - # --- Section: Identification --- - Out "--- Идентификация ---" - Out "UUID: $($cfgNode.GetAttribute('uuid'))" - Out "Имя: $cfgName" - if ($cfgSynonym) { Out "Синоним: $cfgSynonym" } - $cfgComment = Get-PropText "Comment" - if ($cfgComment) { Out "Комментарий: $cfgComment" } - $cfgPrefix = Get-PropText "NamePrefix" - if ($cfgPrefix) { Out "Префикс: $cfgPrefix" } - if ($cfgVendor) { Out "Поставщик: $cfgVendor" } - if ($cfgVersion) { Out "Версия: $cfgVersion" } - foreach ($l in (Get-SupportLines)) { Out $l } - $cfgUpdateAddr = Get-PropText "UpdateCatalogAddress" - if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" } - Out "" - - # --- Section: Modes --- - Out "--- Режимы работы ---" - Out "Формат: $version" - Out "Совместимость: $cfgCompat" - Out "Совм. расширений: $cfgExtCompat" - Out "Режим запуска: $cfgDefaultRun" - Out "Язык скриптов: $cfgScript" - Out "Блокировки: $cfgDataLock" - Out "Автонумерация: $cfgAutoNum" - Out "Модальность: $cfgModality" - Out "Синхр. вызовы: $cfgSyncCalls" - Out "Интерфейс: $cfgIntfCompat" - Out "Табл. пространства: $cfgDbSpaces" - Out "Режим окна: $cfgWindowMode" - Out "" - - # --- Section: Language, roles, purposes --- - Out "--- Назначение ---" - Out "Язык по умолч.: $cfgDefaultLang" - - # UsePurposes - $purposeNode = $propsNode.SelectSingleNode("md:UsePurposes", $ns) - if ($purposeNode) { - $purposes = @() - foreach ($val in $purposeNode.SelectNodes("v8:Value", $ns)) { - $purposes += $val.InnerText - } - if ($purposes.Count -gt 0) { Out "Назначения: $($purposes -join ', ')" } - } - - # DefaultRoles - $rolesNode = $propsNode.SelectSingleNode("md:DefaultRoles", $ns) - if ($rolesNode) { - $roles = @() - foreach ($item in $rolesNode.SelectNodes("xr:Item", $ns)) { - $roles += $item.InnerText - } - if ($roles.Count -gt 0) { - Out "Роли по умолч.: $($roles.Count)" - foreach ($r in $roles) { Out " - $r" } - } - } - - # Booleans - $useMF = Get-PropText "UseManagedFormInOrdinaryApplication" - $useOF = Get-PropText "UseOrdinaryFormInManagedApplication" - Out "Управл.формы в обычн.: $useMF" - Out "Обычн.формы в управл.: $useOF" - Out "" - - # --- Section: Panel layout --- - if ($script:panelLayout) { - Out "--- Раскладка панелей ---" - foreach ($s in @("top","left","right","bottom")) { - $slots = $script:panelLayout[$s] - if ($slots.Count -gt 0) { - Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)" - } else { - Out " $($s.PadRight(7)) —" - } - } - if ($script:panelLayout.declared.Count -gt 0) { - Out " объявлено: $($script:panelLayout.declared -join ', ')" - } - Out "" - } - - # --- Section: Home page (brief summary) --- - if ($script:homePage) { - $ln = $script:homePage.left.Count - $rn = $script:homePage.right.Count - Out "--- Начальная страница ---" - Out " Шаблон: $($script:homePage.template)" - Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" - Out "" - } - - # --- Section: Storages & default forms --- - Out "--- Хранилища и формы по умолчанию ---" - $storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage") - foreach ($sp in $storageProps) { - $val = Get-PropText $sp - if ($val) { Out " ${sp}: $val" } - } - $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultReportAppearanceTemplate","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm","DefaultInterface","DefaultStyle") - foreach ($fp in $formProps) { - $val = Get-PropText $fp - if ($val) { Out " ${fp}: $val" } - } - Out "" - - # --- Section: Info --- - $cfgBrief = Get-PropML "BriefInformation" - $cfgDetail = Get-PropML "DetailedInformation" - $cfgCopyright = Get-PropML "Copyright" - $cfgVendorAddr = Get-PropML "VendorInformationAddress" - $cfgInfoAddr = Get-PropML "ConfigurationInformationAddress" - if ($cfgBrief -or $cfgDetail -or $cfgCopyright -or $cfgVendorAddr -or $cfgInfoAddr) { - Out "--- Информация ---" - if ($cfgBrief) { Out "Краткая: $cfgBrief" } - if ($cfgDetail) { Out "Подробная: $cfgDetail" } - if ($cfgCopyright) { Out "Copyright: $cfgCopyright" } - if ($cfgVendorAddr) { Out "Сайт поставщика: $cfgVendorAddr" } - if ($cfgInfoAddr) { Out "Адрес информ.: $cfgInfoAddr" } - Out "" - } - - # --- Section: Mobile functionalities --- - $mobileFunc = $propsNode.SelectSingleNode("md:UsedMobileApplicationFunctionalities", $ns) - if ($mobileFunc) { - $enabledFuncs = @() - $disabledFuncs = @() - foreach ($func in $mobileFunc.SelectNodes("app:functionality", $ns)) { - $fName = $func.SelectSingleNode("app:functionality", $ns) - $fUse = $func.SelectSingleNode("app:use", $ns) - if ($fName -and $fUse) { - if ($fUse.InnerText -eq "true") { - $enabledFuncs += $fName.InnerText - } else { - $disabledFuncs += $fName.InnerText - } - } - } - $totalFunc = $enabledFuncs.Count + $disabledFuncs.Count - Out "--- Мобильные функциональности ($totalFunc, включено: $($enabledFuncs.Count)) ---" - if ($enabledFuncs.Count -gt 0) { - foreach ($f in $enabledFuncs) { Out " [+] $f" } - } - foreach ($f in $disabledFuncs) { Out " [-] $f" } - Out "" - } - - # --- Section: InternalInfo --- - $internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) - if ($internalInfo) { - $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) - Out "--- InternalInfo ($($contained.Count) ContainedObject) ---" - foreach ($co in $contained) { - $classId = $co.SelectSingleNode("xr:ClassId", $ns).InnerText - $objectId = $co.SelectSingleNode("xr:ObjectId", $ns).InnerText - Out " $classId -> $objectId" - } - Out "" - } - - # --- Section: ChildObjects (full list) --- - Out "--- Состав ($totalObjects объектов) ---" - Out "" - - foreach ($typeName in $typeOrder) { - if (-not $objectCounts.Contains($typeName)) { continue } - $count = $objectCounts[$typeName] - $ruName = $typeRuNames[$typeName] - Out " $ruName ($typeName): $count" - - # Collect names for this type - $names = @() - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName) { - $names += $child.InnerText - } - } - foreach ($n in $names) { Out " $n" } - } -} - -# --- Pagination and output --- -$total = $script:lines.Count -if ($Offset -gt 0 -or $Limit -lt $total) { - $start = [Math]::Min($Offset, $total) - $end = [Math]::Min($start + $Limit, $total) - $page = $script:lines[$start..($end - 1)] - $result = ($page -join "`n") - if ($end -lt $total) { - $result += "`n`n... ($end of $total lines, use -Offset $end to continue)" - } -} else { - $result = ($script:lines -join "`n") -} - -Write-Host $result - -if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "`nWritten to: $OutFile" -} +# cf-info v1.3 — Compact summary of 1C configuration root +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, + [ValidateSet("overview","brief","full")] + [string]$Mode = "overview", + [Alias('Name')] + [ValidateSet("home-page")] + [string]$Section, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper (always collect, paginate at the end) --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +# Directory -> find Configuration.xml +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +# --- Load XML --- +[xml]$xmlDoc = Get-Content -Path $ConfigPath -Encoding UTF8 +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) +if (-not $mdRoot) { + Write-Host "[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)" + exit 1 +} + +$cfgNode = $mdRoot.SelectSingleNode("md:Configuration", $ns) +if (-not $cfgNode) { + Write-Host "[ERROR] No element found" + exit 1 +} + +$version = $mdRoot.GetAttribute("version") +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +# --- Helpers --- +function Get-MLText($node) { + if (-not $node) { return "" } + $item = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($item -and $item.InnerText) { return $item.InnerText } + return "" +} + +function Get-PropText([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($n -and $n.InnerText) { return $n.InnerText } + return "" +} + +function Get-PropML([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + return (Get-MLText $n) +} + +# --- Type name maps (canonical order, 44 types) --- +$typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +$typeRuNames = @{ + "Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили" + "CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли" + "CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули" + "CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты" + "WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки" + "EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания" + "SettingsStorage"="Хранилища настроек"; "FunctionalOption"="Функциональные опции" + "FunctionalOptionsParameter"="Параметры ФО"; "DefinedType"="Определяемые типы" + "CommonCommand"="Общие команды"; "CommandGroup"="Группы команд"; "Constant"="Константы" + "CommonForm"="Общие формы"; "Catalog"="Справочники"; "Document"="Документы" + "DocumentNumerator"="Нумераторы"; "Sequence"="Последовательности"; "DocumentJournal"="Журналы документов" + "Enum"="Перечисления"; "Report"="Отчёты"; "DataProcessor"="Обработки" + "InformationRegister"="Регистры сведений"; "AccumulationRegister"="Регистры накопления" + "ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов" + "AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР" + "CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы" + "Task"="Задачи"; "IntegrationService"="Сервисы интеграции" +} + +# --- Read panel layout (Ext/ClientApplicationInterface.xml) --- +$script:panelNames = @{ + "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых" + "b553047f-c9aa-4157-978d-448ecad24248" = "Разделов" + "13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного" + "c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История" + "b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций" +} + +function Get-PanelsLayout { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml" + if (-not (Test-Path $caiPath)) { return $null } + try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null } + if (-not $caiDoc.DocumentElement) { return $null } + $caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable) + $caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core") + $layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() } + foreach ($side in @("top","left","right","bottom")) { + foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) { + $slot = @() + foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) { + $key = $u.InnerText.Trim() + $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } + $slot += $nm + } + if ($slot.Count -gt 0) { $layout[$side] += ,$slot } + } + } + foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) { + $key = $pd.GetAttribute("id") + $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } + $layout.declared += $nm + } + return $layout +} + +function Format-LayoutSlots($slots) { + # slots is array of arrays (each inner array = one side-tag's panels, may be 1+) + # Single inner array, single panel -> just name + # Single inner array, multiple panels -> "Стек(a, b)" + # Multiple inner arrays -> separate entries joined by " | " + if (-not $slots -or $slots.Count -eq 0) { return "" } + $parts = @() + foreach ($slot in $slots) { + if ($slot.Count -eq 1) { $parts += $slot[0] } + else { $parts += ("Стек(" + ($slot -join ", ") + ")") } + } + return ($parts -join " | ") +} + +$script:panelLayout = Get-PanelsLayout + +# --- Read home page layout (Ext/HomePageWorkArea.xml) --- +function Get-HomePageLayout { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" + if (-not (Test-Path $hpPath)) { return $null } + try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null } + if (-not $hpDoc.DocumentElement) { return $null } + $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) + $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") + $hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $result = [ordered]@{ template = ""; left = @(); right = @() } + $tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs) + if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() } + foreach ($colName in @("LeftColumn","RightColumn")) { + $colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs) + if (-not $colNode) { continue } + $items = @() + foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) { + $f = $item.SelectSingleNode("hp:Form", $hpNs) + $h = $item.SelectSingleNode("hp:Height", $hpNs) + $visNode = $item.SelectSingleNode("hp:Visibility", $hpNs) + $common = $true + $roles = @() + if ($visNode) { + $cn = $visNode.SelectSingleNode("xr:Common", $hpNs) + if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") } + foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) { + $roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") } + } + } + $items += [ordered]@{ + form = if ($f) { $f.InnerText.Trim() } else { "" } + height = if ($h) { [int]$h.InnerText.Trim() } else { 10 } + common = $common + roles = $roles + } + } + if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items } + } + return $result +} + +$script:homePage = Get-HomePageLayout + +# --- Support state (Ext/ParentConfigurations.bin) --- +# Decodes the 1C support-state file. See docs/1c-support-state-spec.md. +# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed', +# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}), +# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each +# vendor block separately); only computed when G=0. +function Read-SupportState([string]$binPath) { + try { + if (-not (Test-Path $binPath)) { return @{ State = 'absent' } } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return @{ State = 'removed' } } + $startIdx = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return $null } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return @{ State = 'removed' } } + # Vendor descriptors: ...,"ver","vendor","name",count, + $vendors = @() + $vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,' + foreach ($m in $vRe.Matches($text)) { + $vendors += @{ + Version = ($m.Groups[1].Value -replace '""','"') + Vendor = ($m.Groups[2].Value -replace '""','"') + Name = ($m.Groups[3].Value -replace '""','"') + } + } + # Per-object counts only matter when editing is enabled (G=0); when G=1 the + # whole config is read-only and stored f1 values are the inactive default. + $counts = $null + if ($G -eq 0) { + $counts = @(0, 0, 0) + # Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid. + $rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + foreach ($m in $rRe.Matches($text)) { + $counts[[int]$m.Groups[1].Value]++ + } + } + return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts } + } catch { return $null } +} + +function Get-SupportLines { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin" + $st = Read-SupportState $binPath + $out = @() + if (-not $st -or $st.State -eq 'absent') { + if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" } + else { $out += "Поддержка: не на поддержке (своя конфигурация)" } + return $out + } + if ($st.State -eq 'removed') { + $out += "Поддержка: снята с поддержки полностью" + return $out + } + $out += "Поддержка: на поддержке" + if ($st.G -eq 0) { + $out += " Возможность изменения: включена" + $out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])" + } else { + $out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)" + } + $out += " Конфигураций поставщика: $($st.K)" + if ($st.K -gt 1) { + foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" } + } + return $out +} + +function Format-HomePageItem($it, [bool]$detailed) { + $badges = @() + $badges += "h=$($it.height)" + if (-not $it.common) { $badges += "скрыта" } + if ($it.roles.Count -gt 0) { + if ($detailed) { $badges += "роли: $($it.roles.Count)" } + else { $badges += "+$($it.roles.Count) ролей" } + } + $tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" } + return " $($it.form)$tail" +} + +# --- Count objects in ChildObjects --- +$objectCounts = [ordered]@{} +$totalObjects = 0 + +if ($childObjNode) { + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if (-not $objectCounts.Contains($typeName)) { + $objectCounts[$typeName] = 0 + } + $objectCounts[$typeName] = $objectCounts[$typeName] + 1 + $totalObjects++ + } +} + +# --- Read key properties --- +$cfgName = Get-PropText "Name" +$cfgSynonym = Get-PropML "Synonym" +$cfgVersion = Get-PropText "Version" +$cfgVendor = Get-PropText "Vendor" +$cfgCompat = Get-PropText "CompatibilityMode" +$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode" +$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose" +$cfgDefaultRun = Get-PropText "DefaultRunMode" +$cfgScript = Get-PropText "ScriptVariant" +$cfgDefaultLang = Get-PropText "DefaultLanguage" +$cfgDataLock = Get-PropText "DataLockControlMode" +$dash = [char]0x2014 +$cfgModality = Get-PropText "ModalityUseMode" +$cfgIntfCompat = Get-PropText "InterfaceCompatibilityMode" +$cfgAutoNum = Get-PropText "ObjectAutonumerationMode" +$cfgSyncCalls = Get-PropText "SynchronousPlatformExtensionAndAddInCallUseMode" +$cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode" +$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode" + +# --- BRIEF mode --- +if ($Mode -eq "brief" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + $compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" } + Out "Конфигурация: ${cfgName}${synPart}${verPart} | $totalObjects объектов${compatPart}" +} + +# --- OVERVIEW mode --- +if ($Mode -eq "overview" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # Key properties + Out "Формат: $version" + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + foreach ($l in (Get-SupportLines)) { Out $l } + Out "Совместимость: $cfgCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Язык: $cfgDefaultLang" + Out "Блокировки: $cfgDataLock" + Out "Модальность: $cfgModality" + Out "Интерфейс: $cfgIntfCompat" + Out "" + + # Panel layout (if file exists) + if ($script:panelLayout) { + $hasPlaced = $false + foreach ($s in @("top","left","right","bottom")) { + if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break } + } + if ($hasPlaced) { + Out "--- Раскладка панелей ---" + foreach ($s in @("top","left","right","bottom")) { + if ($script:panelLayout[$s].Count -gt 0) { + Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])" + } + } + Out "" + } + } + + # Home page layout (brief summary) + if ($script:homePage) { + $ln = $script:homePage.left.Count + $rn = $script:homePage.right.Count + Out "--- Начальная страница ---" + Out " Шаблон: $($script:homePage.template)" + Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" + Out "" + } + + # Object counts table + Out "--- Состав ($totalObjects объектов) ---" + Out "" + $maxTypeLen = 0 + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $ruName = $typeRuNames[$typeName] + if ($ruName.Length -gt $maxTypeLen) { $maxTypeLen = $ruName.Length } + } + } + if ($maxTypeLen -lt 10) { $maxTypeLen = 10 } + + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + $padded = $ruName.PadRight($maxTypeLen) + Out " $padded $count" + } + } +} + +# --- Drill-down: -Section home-page --- +if ($Section -eq "home-page") { + if (-not $script:homePage) { + Out "Файл Ext/HomePageWorkArea.xml не найден" + } else { + Out "=== Начальная страница: $cfgName ===" + Out "" + Out "Шаблон: $($script:homePage.template)" + Out "" + foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) { + $items = $script:homePage[$side[1]] + $lbl = $side[0] + if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue } + Out "${lbl} ($($items.Count)):" + foreach ($it in $items) { + Out (Format-HomePageItem $it $true) + foreach ($r in $it.roles) { + $rval = if ($r.value) { "true" } else { "false" } + Out " $($r.name): $rval" + } + } + Out "" + } + } +} + +# --- FULL mode --- +if ($Mode -eq "full" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # --- Section: Identification --- + Out "--- Идентификация ---" + Out "UUID: $($cfgNode.GetAttribute('uuid'))" + Out "Имя: $cfgName" + if ($cfgSynonym) { Out "Синоним: $cfgSynonym" } + $cfgComment = Get-PropText "Comment" + if ($cfgComment) { Out "Комментарий: $cfgComment" } + $cfgPrefix = Get-PropText "NamePrefix" + if ($cfgPrefix) { Out "Префикс: $cfgPrefix" } + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + foreach ($l in (Get-SupportLines)) { Out $l } + $cfgUpdateAddr = Get-PropText "UpdateCatalogAddress" + if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" } + Out "" + + # --- Section: Modes --- + Out "--- Режимы работы ---" + Out "Формат: $version" + Out "Совместимость: $cfgCompat" + Out "Совм. расширений: $cfgExtCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Блокировки: $cfgDataLock" + Out "Автонумерация: $cfgAutoNum" + Out "Модальность: $cfgModality" + Out "Синхр. вызовы: $cfgSyncCalls" + Out "Интерфейс: $cfgIntfCompat" + Out "Табл. пространства: $cfgDbSpaces" + Out "Режим окна: $cfgWindowMode" + Out "" + + # --- Section: Language, roles, purposes --- + Out "--- Назначение ---" + Out "Язык по умолч.: $cfgDefaultLang" + + # UsePurposes + $purposeNode = $propsNode.SelectSingleNode("md:UsePurposes", $ns) + if ($purposeNode) { + $purposes = @() + foreach ($val in $purposeNode.SelectNodes("v8:Value", $ns)) { + $purposes += $val.InnerText + } + if ($purposes.Count -gt 0) { Out "Назначения: $($purposes -join ', ')" } + } + + # DefaultRoles + $rolesNode = $propsNode.SelectSingleNode("md:DefaultRoles", $ns) + if ($rolesNode) { + $roles = @() + foreach ($item in $rolesNode.SelectNodes("xr:Item", $ns)) { + $roles += $item.InnerText + } + if ($roles.Count -gt 0) { + Out "Роли по умолч.: $($roles.Count)" + foreach ($r in $roles) { Out " - $r" } + } + } + + # Booleans + $useMF = Get-PropText "UseManagedFormInOrdinaryApplication" + $useOF = Get-PropText "UseOrdinaryFormInManagedApplication" + Out "Управл.формы в обычн.: $useMF" + Out "Обычн.формы в управл.: $useOF" + Out "" + + # --- Section: Panel layout --- + if ($script:panelLayout) { + Out "--- Раскладка панелей ---" + foreach ($s in @("top","left","right","bottom")) { + $slots = $script:panelLayout[$s] + if ($slots.Count -gt 0) { + Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)" + } else { + Out " $($s.PadRight(7)) —" + } + } + if ($script:panelLayout.declared.Count -gt 0) { + Out " объявлено: $($script:panelLayout.declared -join ', ')" + } + Out "" + } + + # --- Section: Home page (brief summary) --- + if ($script:homePage) { + $ln = $script:homePage.left.Count + $rn = $script:homePage.right.Count + Out "--- Начальная страница ---" + Out " Шаблон: $($script:homePage.template)" + Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" + Out "" + } + + # --- Section: Storages & default forms --- + Out "--- Хранилища и формы по умолчанию ---" + $storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage") + foreach ($sp in $storageProps) { + $val = Get-PropText $sp + if ($val) { Out " ${sp}: $val" } + } + $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultReportAppearanceTemplate","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm","DefaultInterface","DefaultStyle") + foreach ($fp in $formProps) { + $val = Get-PropText $fp + if ($val) { Out " ${fp}: $val" } + } + Out "" + + # --- Section: Info --- + $cfgBrief = Get-PropML "BriefInformation" + $cfgDetail = Get-PropML "DetailedInformation" + $cfgCopyright = Get-PropML "Copyright" + $cfgVendorAddr = Get-PropML "VendorInformationAddress" + $cfgInfoAddr = Get-PropML "ConfigurationInformationAddress" + if ($cfgBrief -or $cfgDetail -or $cfgCopyright -or $cfgVendorAddr -or $cfgInfoAddr) { + Out "--- Информация ---" + if ($cfgBrief) { Out "Краткая: $cfgBrief" } + if ($cfgDetail) { Out "Подробная: $cfgDetail" } + if ($cfgCopyright) { Out "Copyright: $cfgCopyright" } + if ($cfgVendorAddr) { Out "Сайт поставщика: $cfgVendorAddr" } + if ($cfgInfoAddr) { Out "Адрес информ.: $cfgInfoAddr" } + Out "" + } + + # --- Section: Mobile functionalities --- + $mobileFunc = $propsNode.SelectSingleNode("md:UsedMobileApplicationFunctionalities", $ns) + if ($mobileFunc) { + $enabledFuncs = @() + $disabledFuncs = @() + foreach ($func in $mobileFunc.SelectNodes("app:functionality", $ns)) { + $fName = $func.SelectSingleNode("app:functionality", $ns) + $fUse = $func.SelectSingleNode("app:use", $ns) + if ($fName -and $fUse) { + if ($fUse.InnerText -eq "true") { + $enabledFuncs += $fName.InnerText + } else { + $disabledFuncs += $fName.InnerText + } + } + } + $totalFunc = $enabledFuncs.Count + $disabledFuncs.Count + Out "--- Мобильные функциональности ($totalFunc, включено: $($enabledFuncs.Count)) ---" + if ($enabledFuncs.Count -gt 0) { + foreach ($f in $enabledFuncs) { Out " [+] $f" } + } + foreach ($f in $disabledFuncs) { Out " [-] $f" } + Out "" + } + + # --- Section: InternalInfo --- + $internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) + if ($internalInfo) { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + Out "--- InternalInfo ($($contained.Count) ContainedObject) ---" + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns).InnerText + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns).InnerText + Out " $classId -> $objectId" + } + Out "" + } + + # --- Section: ChildObjects (full list) --- + Out "--- Состав ($totalObjects объектов) ---" + Out "" + + foreach ($typeName in $typeOrder) { + if (-not $objectCounts.Contains($typeName)) { continue } + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + Out " $ruName ($typeName): $count" + + # Collect names for this type + $names = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName) { + $names += $child.InnerText + } + } + foreach ($n in $names) { Out " $n" } + } +} + +# --- Pagination and output --- +$total = $script:lines.Count +if ($Offset -gt 0 -or $Limit -lt $total) { + $start = [Math]::Min($Offset, $total) + $end = [Math]::Min($start + $Limit, $total) + $page = $script:lines[$start..($end - 1)] + $result = ($page -join "`n") + if ($end -lt $total) { + $result += "`n`n... ($end of $total lines, use -Offset $end to continue)" + } +} else { + $result = ($script:lines -join "`n") +} + +Write-Host $result + +if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "`nWritten to: $OutFile" +} diff --git a/.claude/skills/cf-init/SKILL.md b/.claude/skills/cf-init/SKILL.md index 3ac0cc39..6a2b3477 100644 --- a/.claude/skills/cf-init/SKILL.md +++ b/.claude/skills/cf-init/SKILL.md @@ -1,49 +1,49 @@ ---- -name: cf-init -description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля -argument-hint: [-Synonym ] [-OutputDir src] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cf-init — Создание пустой конфигурации 1С - -Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `Name` | Имя конфигурации (обязат.) | -| `Synonym` | Синоним (= Name если не указан) | -| `OutputDir` | Каталог для создания (default: `src`) | -| `Version` | Версия конфигурации | -| `Vendor` | Поставщик | -| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" -``` - -## Примеры - -```powershell -# Базовая конфигурация -... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf - -# С версией и поставщиком -... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 - -# Другой режим совместимости -... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 -``` - -## Верификация - -``` -/cf-init TestConfig -OutputDir test-tmp/cf -/cf-info test-tmp/cf — проверить созданное -/cf-validate test-tmp/cf — валидировать -``` +--- +name: cf-init +description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля +argument-hint: [-Synonym ] [-OutputDir src] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-init — Создание пустой конфигурации 1С + +Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `Name` | Имя конфигурации (обязат.) | +| `Synonym` | Синоним (= Name если не указан) | +| `OutputDir` | Каталог для создания (default: `src`) | +| `Version` | Версия конфигурации | +| `Vendor` | Поставщик | +| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" +``` + +## Примеры + +```powershell +# Базовая конфигурация +... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf + +# С версией и поставщиком +... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 + +# Другой режим совместимости +... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 +``` + +## Верификация + +``` +/cf-init TestConfig -OutputDir test-tmp/cf +/cf-info test-tmp/cf — проверить созданное +/cf-validate test-tmp/cf — валидировать +``` diff --git a/.claude/skills/cf-init/scripts/cf-init.ps1 b/.claude/skills/cf-init/scripts/cf-init.ps1 index 69603dca..54b20dee 100644 --- a/.claude/skills/cf-init/scripts/cf-init.ps1 +++ b/.claude/skills/cf-init/scripts/cf-init.ps1 @@ -1,249 +1,249 @@ -# cf-init v1.2 — Create empty 1C configuration scaffold -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$Name, - [string]$Synonym = $Name, - [string]$OutputDir = "src", - [string]$Version, - [string]$Vendor, - [string]$CompatibilityMode = "Version8_3_24" -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve output dir --- -if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { - $OutputDir = Join-Path (Get-Location).Path $OutputDir -} - -# --- Check existing --- -$cfgFile = Join-Path $OutputDir "Configuration.xml" -if (Test-Path $cfgFile) { - Write-Error "Configuration.xml already exists: $cfgFile" - exit 1 -} - -# --- Generate UUIDs --- -$uuidCfg = [guid]::NewGuid().ToString() -$uuidLang = [guid]::NewGuid().ToString() -# 7 ContainedObject ObjectIds -$co1 = [guid]::NewGuid().ToString() -$co2 = [guid]::NewGuid().ToString() -$co3 = [guid]::NewGuid().ToString() -$co4 = [guid]::NewGuid().ToString() -$co5 = [guid]::NewGuid().ToString() -$co6 = [guid]::NewGuid().ToString() -$co7 = [guid]::NewGuid().ToString() - -# --- Mobile functionalities --- -$mobileFuncs = @( - @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), - @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), - @("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"), - @("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"), - @("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"), - @("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"), - @("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"), - @("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"), - @("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"), - @("OSBackup","true"), @("ApplicationUsageStatistics","false"), - @("BarcodeScanning","false"), @("BackgroundAudioRecording","false"), - @("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"), - @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), - @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") -) - -$mobileXml = "" -foreach ($mf in $mobileFuncs) { - $mobileXml += "`r`n`t`t`t`t`r`n`t`t`t`t`t$($mf[0])`r`n`t`t`t`t`t$($mf[1])`r`n`t`t`t`t" -} - -# --- Synonym XML --- -$synonymXml = "" -if ($Synonym) { - $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" -} - -# --- Optional properties --- -$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } -$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } - -# --- Configuration.xml --- -$cfgXml = @" - - - - - - 9cd510cd-abfc-11d4-9434-004095e12fc7 - $co1 - - - 9fcd25a0-4822-11d4-9414-008048da11f9 - $co2 - - - e3687481-0a87-462c-a166-9f34594f9bba - $co3 - - - 9de14907-ec23-4a07-96f0-85521cb6b53b - $co4 - - - 51f2d5d8-ea4d-4064-8892-82951750031e - $co5 - - - e68182ea-4237-4383-967f-90c1e3370bc7 - $co6 - - - fb282519-d103-4dd3-bc12-cb271d631dfc - $co7 - - - - $([System.Security.SecurityElement]::Escape($Name)) - $synonymXml - - - $CompatibilityMode - ManagedApplication - - PlatformApplication - - Russian - - $vendorXml - $versionXml - - false - false - false - - - - - - - - - - - - - - - - - - - - $mobileXml - - - - - Normal - - - Language.Русский - - - - - - Managed - NotAutoFree - DontUse - DontUse - TaxiEnableVersion8_2 - DontUse - $CompatibilityMode - - - - Русский - - - -"@ - -# --- Languages/Русский.xml --- -$langXml = @" - - - - - Русский - - - ru - Русский - - - - ru - - - -"@ - -# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- -# Open panel on top, Sections panel on left; Functions/Favorites/History declared -# via panelDef but not placed by default. Without this file the web client renders -# section icons without labels (icon-only mode). -$openPanelInst = [guid]::NewGuid().ToString() -$sectionsPanelInst = [guid]::NewGuid().ToString() -$caiXml = @" - - - - - cbab57f2-a0f3-4f0a-89ea-4cb19570ab75 - - - - - b553047f-c9aa-4157-978d-448ecad24248 - - - - - - - - -"@ - -# --- Create directories --- -if (-not (Test-Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null -} -$langDir = Join-Path $OutputDir "Languages" -if (-not (Test-Path $langDir)) { - New-Item -ItemType Directory -Path $langDir -Force | Out-Null -} -$extDir = Join-Path $OutputDir "Ext" -if (-not (Test-Path $extDir)) { - New-Item -ItemType Directory -Path $extDir -Force | Out-Null -} - -# --- Write files with UTF-8 BOM --- -$enc = New-Object System.Text.UTF8Encoding($true) - -[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) -$langFile = Join-Path $langDir "Русский.xml" -[System.IO.File]::WriteAllText($langFile, $langXml, $enc) -$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" -[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) - -# --- Output --- -Write-Host "[OK] Создана конфигурация: $Name" -Write-Host " Каталог: $OutputDir" -Write-Host " Configuration.xml: $cfgFile" -Write-Host " Languages: $langFile" -Write-Host " Ext/CAI: $caiFile" +# cf-init v1.2 — Create empty 1C configuration scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$OutputDir = "src", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Mobile functionalities --- +$mobileFuncs = @( + @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), + @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), + @("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"), + @("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"), + @("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"), + @("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"), + @("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"), + @("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"), + @("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"), + @("OSBackup","true"), @("ApplicationUsageStatistics","false"), + @("BarcodeScanning","false"), @("BackgroundAudioRecording","false"), + @("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"), + @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), + @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") +) + +$mobileXml = "" +foreach ($mf in $mobileFuncs) { + $mobileXml += "`r`n`t`t`t`t`r`n`t`t`t`t`t$($mf[0])`r`n`t`t`t`t`t$($mf[1])`r`n`t`t`t`t" +} + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + + $vendorXml + $versionXml + + false + false + false + + + + + + + + + + + + + + + + + + + + $mobileXml + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + $CompatibilityMode + + + + Русский + + + +"@ + +# --- Languages/Русский.xml --- +$langXml = @" + + + + + Русский + + + ru + Русский + + + + ru + + + +"@ + +# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- +# Open panel on top, Sections panel on left; Functions/Favorites/History declared +# via panelDef but not placed by default. Without this file the web client renders +# section icons without labels (icon-only mode). +$openPanelInst = [guid]::NewGuid().ToString() +$sectionsPanelInst = [guid]::NewGuid().ToString() +$caiXml = @" + + + + + cbab57f2-a0f3-4f0a-89ea-4cb19570ab75 + + + + + b553047f-c9aa-4157-978d-448ecad24248 + + + + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} +$extDir = Join-Path $OutputDir "Ext" +if (-not (Test-Path $extDir)) { + New-Item -ItemType Directory -Path $extDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) +$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" +[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) + +# --- Output --- +Write-Host "[OK] Создана конфигурация: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +Write-Host " Ext/CAI: $caiFile" diff --git a/.claude/skills/cf-validate/SKILL.md b/.claude/skills/cf-validate/SKILL.md index c4ec95ac..d62e5bd5 100644 --- a/.claude/skills/cf-validate/SKILL.md +++ b/.claude/skills/cf-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: cf-validate -description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cf-validate — валидация конфигурации 1С - -Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|------------|:-----:|---------|-------------------------------------------------| -| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" -``` +--- +name: cf-validate +description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-validate — валидация конфигурации 1С + +Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" +``` diff --git a/.claude/skills/cf-validate/scripts/cf-validate.ps1 b/.claude/skills/cf-validate/scripts/cf-validate.ps1 index f7c071a4..365853b9 100644 --- a/.claude/skills/cf-validate/scripts/cf-validate.ps1 +++ b/.claude/skills/cf-validate/scripts/cf-validate.ps1 @@ -1,611 +1,611 @@ -# cf-validate v1.3 — Validate 1C configuration root structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$ConfigPath, - - [switch]$Detailed, - - [int]$MaxErrors = 30, - - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath -} - -if (Test-Path $ConfigPath -PathType Container) { - $candidate = Join-Path $ConfigPath "Configuration.xml" - if (Test-Path $candidate) { - $ConfigPath = $candidate - } else { - Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" - exit 1 - } -} - -if (-not (Test-Path $ConfigPath)) { - Write-Host "[ERROR] File not found: $ConfigPath" - exit 1 -} - -$resolvedPath = (Resolve-Path $ConfigPath).Path -$configDir = Split-Path $resolvedPath -Parent - -# --- Output infrastructure --- -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$finalize = { - $checks = $script:okCount + $script:errors + $script:warnings - if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: Configuration.$objName ($checks checks) ===" - } else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() - } - Write-Host $result - - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" - } -} - -# --- Reference tables --- -$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' - -# 7 fixed ClassIds for Configuration -$validClassIds = @( - "9cd510cd-abfc-11d4-9434-004095e12fc7", # managed application module - "9fcd25a0-4822-11d4-9414-008048da11f9", # ordinary application module - "e3687481-0a87-462c-a166-9f34594f9bba", # session module - "9de14907-ec23-4a07-96f0-85521cb6b53b", # external connection module - "51f2d5d8-ea4d-4064-8892-82951750031e", # command interface - "e68182ea-4237-4383-967f-90c1e3370bc7", # main section command interface - "fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface -) - -# 44 types in canonical order -$childObjectTypes = @( - "Language","Subsystem","StyleItem","Style", - "CommonPicture","SessionParameter","Role","CommonTemplate", - "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", - "XDTOPackage","WebService","HTTPService","WSReference", - "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", - "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", - "Constant","CommonForm","Catalog","Document", - "DocumentNumerator","Sequence","DocumentJournal","Enum", - "Report","DataProcessor","InformationRegister","AccumulationRegister", - "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", - "ChartOfCalculationTypes","CalculationRegister", - "BusinessProcess","Task","IntegrationService" -) - -# Type -> directory mapping -$childTypeDirMap = @{ - "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" - "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" - "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" - "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" - "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" - "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" - "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" - "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" - "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" - "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" - "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" - "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" - "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" - "AccumulationRegister"="AccumulationRegisters" - "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" - "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" - "ChartOfCalculationTypes"="ChartsOfCalculationTypes" - "CalculationRegister"="CalculationRegisters" - "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" - "IntegrationService"="IntegrationServices" -} - -# Valid enum values for Configuration properties -$validEnumValues = @{ - "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") - "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") - "ScriptVariant" = @("Russian","English") - "DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged") - "ObjectAutonumerationMode" = @("NotAutoFree","AutoFree") - "ModalityUseMode" = @("DontUse","Use","UseWithWarnings") - "SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings") - "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") - "DatabaseTablespacesUseMode" = @("DontUse","Use") - "MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk") - "CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") -} - -# --- 1. Parse XML --- -Out-Line "" - -$xmlDoc = $null -try { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $false - $xmlDoc.Load($resolvedPath) -} catch { - Out-Line "=== Validation: Configuration (parse failed) ===" - Out-Line "" - Report-Error "1. XML parse failed: $($_.Exception.Message)" - & $finalize - exit 1 -} - -# --- Register namespaces --- -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") - -$root = $xmlDoc.DocumentElement - -# --- Check 1: Root structure --- -$check1Ok = $true -$expectedNs = "http://v8.1c.ru/8.3/MDClasses" - -if ($root.LocalName -ne "MetaDataObject") { - Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" - & $finalize - exit 1 -} - -if ($root.NamespaceURI -ne $expectedNs) { - Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" - $check1Ok = $false -} - -$version = $root.GetAttribute("version") -if (-not $version) { - Report-Warn "1. Missing version attribute on MetaDataObject" -} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { - Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" -} - -# Must have Configuration child -$cfgNode = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { - $cfgNode = $child; break - } -} - -if (-not $cfgNode) { - Report-Error "1. No element found inside MetaDataObject" - & $finalize - exit 1 -} - -# UUID -$cfgUuid = $cfgNode.GetAttribute("uuid") -if (-not $cfgUuid) { - Report-Error "1. Missing uuid on " - $check1Ok = $false -} elseif ($cfgUuid -notmatch $guidPattern) { - Report-Error "1. Invalid uuid '$cfgUuid' on " - $check1Ok = $false -} - -# Get name early for header -$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) -$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } -$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } - -$script:output.Insert(0, "=== Validation: Configuration.$objName ===$([Environment]::NewLine)") | Out-Null - -if ($check1Ok) { - Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 2: InternalInfo --- -$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) -$check2Ok = $true - -if (-not $internalInfo) { - Report-Error "2. InternalInfo: missing" -} else { - $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) - if ($contained.Count -ne 7) { - Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" - } - - $foundClassIds = @{} - foreach ($co in $contained) { - $classId = $co.SelectSingleNode("xr:ClassId", $ns) - $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) - - if (-not $classId -or -not $classId.InnerText) { - Report-Error "2. ContainedObject missing ClassId" - $check2Ok = $false - continue - } - - $cid = $classId.InnerText - if ($validClassIds -notcontains $cid) { - Report-Error "2. Unknown ClassId: $cid" - $check2Ok = $false - } - - if ($foundClassIds.ContainsKey($cid)) { - Report-Error "2. Duplicate ClassId: $cid" - $check2Ok = $false - } - $foundClassIds[$cid] = $true - - if (-not $objectId -or -not $objectId.InnerText) { - Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" - $check2Ok = $false - } elseif ($objectId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" - $check2Ok = $false - } - } - - # Check missing ClassIds - $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) - if ($missingIds.Count -gt 0) { - Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" - } - - if ($check2Ok) { - Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 3: Properties — Name, Synonym, DefaultLanguage, DefaultRunMode --- -if (-not $propsNode) { - Report-Error "3. Properties block missing" -} else { - $check3Ok = $true - - # Name - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "3. Properties: Name is missing or empty" - $check3Ok = $false - } else { - $nameVal = $nameNode.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" - $check3Ok = $false - } - } - - # Synonym - $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) - $synPresent = $false - if ($synNode) { - $synItem = $synNode.SelectSingleNode("v8:item", $ns) - if ($synItem) { - $synContent = $synItem.SelectSingleNode("v8:content", $ns) - if ($synContent -and $synContent.InnerText) { $synPresent = $true } - } - } - - # DefaultLanguage - $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) - $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } - if (-not $defLang) { - Report-Error "3. Properties: DefaultLanguage is missing or empty" - $check3Ok = $false - } - - # DefaultRunMode - $defRunNode = $propsNode.SelectSingleNode("md:DefaultRunMode", $ns) - if (-not $defRunNode -or -not $defRunNode.InnerText) { - Report-Warn "3. Properties: DefaultRunMode is missing or empty" - } - - if ($check3Ok) { - $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } - Report-OK "3. Properties: Name=`"$objName`", $synInfo, DefaultLanguage=$defLang" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 4: Property values — enum properties --- -if ($propsNode) { - $enumChecked = 0 - $check4Ok = $true - - foreach ($propName in $validEnumValues.Keys) { - $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) - if ($propNode -and $propNode.InnerText) { - $val = $propNode.InnerText - $allowed = $validEnumValues[$propName] - if ($allowed -notcontains $val) { - Report-Error "4. Property '$propName' has invalid value '$val'" - $check4Ok = $false - } - $enumChecked++ - } - } - - if ($check4Ok) { - Report-OK "4. Property values: $enumChecked enum properties checked" - } -} else { - Report-Warn "4. No Properties block to check" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 5: ChildObjects — valid types, no duplicates, order --- -$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) - -if (-not $childObjNode) { - Report-Error "5. ChildObjects block missing" -} else { - $check5Ok = $true - $totalCount = 0 - $typeCounts = @{} - $duplicates = @{} - $typeFirstIndex = @{} # type -> first position index - $lastTypeOrder = -1 - $orderOk = $true - $idx = 0 - - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - $objNameVal = $child.InnerText - - # Valid type? - $typeIdx = $childObjectTypes.IndexOf($typeName) - if ($typeIdx -lt 0) { - Report-Error "5. Unknown type '$typeName' in ChildObjects" - $check5Ok = $false - } else { - # Check order - if (-not $typeFirstIndex.ContainsKey($typeName)) { - $typeFirstIndex[$typeName] = $typeIdx - if ($typeIdx -lt $lastTypeOrder) { - Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" - $orderOk = $false - } - $lastTypeOrder = $typeIdx - } - } - - # Count and dedup - if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } - if ($typeCounts[$typeName].ContainsKey($objNameVal)) { - if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { - Report-Error "5. Duplicate: $typeName.$objNameVal" - $duplicates["$typeName.$objNameVal"] = $true - $check5Ok = $false - } - } else { - $typeCounts[$typeName][$objNameVal] = $true - } - - $totalCount++ - $idx++ - } - - $typeCount = $typeCounts.Count - if ($check5Ok) { - $orderInfo = if ($orderOk) { ", order correct" } else { "" } - Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- -if ($defLang -and $childObjNode) { - # DefaultLanguage is like "Language.Русский" - $langName = $defLang - if ($langName.StartsWith("Language.")) { - $langName = $langName.Substring(9) - } - - $found = $false - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { - $found = $true; break - } - } - - if ($found) { - Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" - } else { - Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" - } -} else { - if (-not $defLang) { - Report-Warn "6. Cannot check DefaultLanguage (empty)" - } else { - Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 7: Language files exist --- -if ($childObjNode) { - $langNames = @() - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { - $langNames += $child.InnerText - } - } - - if ($langNames.Count -gt 0) { - $existCount = 0 - foreach ($ln in $langNames) { - $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" - if (Test-Path $langFile) { - $existCount++ - } else { - Report-Warn "7. Language file missing: Languages/$ln.xml" - } - } - if ($existCount -eq $langNames.Count) { - Report-OK "7. Language files: $existCount/$($langNames.Count) exist" - } - } else { - Report-Warn "7. No Language entries in ChildObjects" - } -} else { - Report-Warn "7. Cannot check language files (no ChildObjects)" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 8: Object directories exist (spot-check) --- -if ($childObjNode) { - $dirsToCheck = @{} - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - if ($typeName -eq "Language") { continue } # Already checked - if ($childTypeDirMap.ContainsKey($typeName)) { - $dirName = $childTypeDirMap[$typeName] - if (-not $dirsToCheck.ContainsKey($dirName)) { - $dirsToCheck[$dirName] = 0 - } - $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 - } - } - - $missingDirs = @() - foreach ($dir in $dirsToCheck.Keys) { - $dirPath = Join-Path $configDir $dir - if (-not (Test-Path $dirPath -PathType Container)) { - $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" - } - } - - if ($missingDirs.Count -eq 0) { - Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" - } else { - foreach ($md in $missingDirs) { - Report-Warn "8. Missing directory: $md" - } - } -} - -# --- Check 9: Form references (HomePageWorkArea + Properties) --- -function Test-FormRef([string]$ref) { - if (-not $ref) { return $true } - # UUID — cannot verify without scanning all forms; skip - if ($ref -match $guidPattern) { return $true } - $parts = $ref.Split(".") - if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") { - $p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml" - $pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml" - return (Test-Path $p) -or (Test-Path $pExt) - } - if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) { - $dir = $childTypeDirMap[$parts[0]] - $p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml" - $pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml" - return (Test-Path $p) -or (Test-Path $pExt) - } - return $false -} - -$formRefsChecked = 0 -$formRefErrors = @() - -# HomePageWorkArea -$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" -if (Test-Path $hpPath) { - try { - [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 - $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) - $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") - foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) { - $ref = $f.InnerText.Trim() - if (-not $ref) { continue } - $formRefsChecked++ - if (-not (Test-FormRef $ref)) { - $formRefErrors += "HomePageWorkArea.Form '$ref' — file not found" - } - } - } catch { - $formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)" - } -} - -# Properties: DefaultXxxForm refs -if ($propsNode) { - $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm") - foreach ($pn in $formProps) { - $node = $propsNode.SelectSingleNode("md:$pn", $ns) - if ($node -and $node.InnerText.Trim()) { - $ref = $node.InnerText.Trim() - $formRefsChecked++ - if (-not (Test-FormRef $ref)) { - $formRefErrors += "Properties.$pn '$ref' — form not found" - } - } - } -} - -if ($formRefsChecked -eq 0) { - Report-OK "9. Form references: none to check" -} elseif ($formRefErrors.Count -eq 0) { - Report-OK "9. Form references: $formRefsChecked verified" -} else { - foreach ($err in $formRefErrors) { Report-Error "9. $err" } -} - -# --- Final output --- -& $finalize - -if ($script:errors -gt 0) { - exit 1 -} -exit 0 +# cf-validate v1.3 — Validate 1C configuration root structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ConfigPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ConfigPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Configuration.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "9cd510cd-abfc-11d4-9434-004095e12fc7", # managed application module + "9fcd25a0-4822-11d4-9414-008048da11f9", # ordinary application module + "e3687481-0a87-462c-a166-9f34594f9bba", # session module + "9de14907-ec23-4a07-96f0-85521cb6b53b", # external connection module + "51f2d5d8-ea4d-4064-8892-82951750031e", # command interface + "e68182ea-4237-4383-967f-90c1e3370bc7", # main section command interface + "fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface +) + +# 44 types in canonical order +$childObjectTypes = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# Type -> directory mapping +$childTypeDirMap = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" + "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" + "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" + "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" + "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" + "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" + "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" + "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" + "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "IntegrationService"="IntegrationServices" +} + +# Valid enum values for Configuration properties +$validEnumValues = @{ + "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged") + "ObjectAutonumerationMode" = @("NotAutoFree","AutoFree") + "ModalityUseMode" = @("DontUse","Use","UseWithWarnings") + "SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings") + "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") + "DatabaseTablespacesUseMode" = @("DontUse","Use") + "MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk") + "CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Configuration (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Configuration.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + # Check missing ClassIds + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties — Name, Synonym, DefaultLanguage, DefaultRunMode --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { $synPresent = $true } + } + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + if (-not $defLang) { + Report-Error "3. Properties: DefaultLanguage is missing or empty" + $check3Ok = $false + } + + # DefaultRunMode + $defRunNode = $propsNode.SelectSingleNode("md:DefaultRunMode", $ns) + if (-not $defRunNode -or -not $defRunNode.InnerText) { + Report-Warn "3. Properties: DefaultRunMode is missing or empty" + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo, DefaultLanguage=$defLang" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Property values — enum properties --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $typeCounts = @{} + $duplicates = @{} + $typeFirstIndex = @{} # type -> first position index + $lastTypeOrder = -1 + $orderOk = $true + $idx = 0 + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + # Valid type? + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + # Check order + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + # Count and dedup + if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } + if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $typeCounts[$typeName][$objNameVal] = $true + } + + $totalCount++ + $idx++ + } + + $typeCount = $typeCounts.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + # DefaultLanguage is like "Language.Русский" + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist (spot-check) --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } # Already checked + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} + +# --- Check 9: Form references (HomePageWorkArea + Properties) --- +function Test-FormRef([string]$ref) { + if (-not $ref) { return $true } + # UUID — cannot verify without scanning all forms; skip + if ($ref -match $guidPattern) { return $true } + $parts = $ref.Split(".") + if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") { + $p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml" + $pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml" + return (Test-Path $p) -or (Test-Path $pExt) + } + if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) { + $dir = $childTypeDirMap[$parts[0]] + $p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml" + $pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml" + return (Test-Path $p) -or (Test-Path $pExt) + } + return $false +} + +$formRefsChecked = 0 +$formRefErrors = @() + +# HomePageWorkArea +$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" +if (Test-Path $hpPath) { + try { + [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 + $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) + $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") + foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) { + $ref = $f.InnerText.Trim() + if (-not $ref) { continue } + $formRefsChecked++ + if (-not (Test-FormRef $ref)) { + $formRefErrors += "HomePageWorkArea.Form '$ref' — file not found" + } + } + } catch { + $formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)" + } +} + +# Properties: DefaultXxxForm refs +if ($propsNode) { + $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm") + foreach ($pn in $formProps) { + $node = $propsNode.SelectSingleNode("md:$pn", $ns) + if ($node -and $node.InnerText.Trim()) { + $ref = $node.InnerText.Trim() + $formRefsChecked++ + if (-not (Test-FormRef $ref)) { + $formRefErrors += "Properties.$pn '$ref' — form not found" + } + } + } +} + +if ($formRefsChecked -eq 0) { + Report-OK "9. Form references: none to check" +} elseif ($formRefErrors.Count -eq 0) { + Report-OK "9. Form references: $formRefsChecked verified" +} else { + foreach ($err in $formRefErrors) { Report-Error "9. $err" } +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md index 024701a7..032b8406 100644 --- a/.claude/skills/cfe-borrow/SKILL.md +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -1,101 +1,101 @@ ---- -name: cfe-borrow -description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации -argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cfe-borrow — Заимствование объектов из конфигурации - -Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. - -## Предусловие - -Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. - -### Авто-определение ConfigPath - -Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: -1. Прочитай `.v8-project.json` из корня проекта -2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) -3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` -4. Если `configSrc` нет — спроси у пользователя - -## Параметры - -| Параметр | Описание | -|----------|----------| -| `ExtensionPath` | Путь к каталогу расширения (обязат.) | -| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | -| `Object` | Что заимствовать (обязат.), batch через `;;` | -| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object | - -## Формат -Object - -- `Catalog.Контрагенты` — справочник -- `CommonModule.РаботаСФайлами` — общий модуль -- `Document.РеализацияТоваров` — документ -- `Enum.ВидыОплат` — перечисление -- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) -- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов -Поддерживаются все 44 типа объектов конфигурации. - -### Заимствование форм - -Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. - -Создаётся: -1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` -2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `` (начальное состояние) -3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` -4. **Регистрация** — `
` в ChildObjects родительского объекта - -### Заимствование основного реквизита формы (-BorrowMainAttribute) - -**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. - -**Два режима**: -- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев -- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет - -**Типовой сценарий** (добавление реквизита + вывод на форму): -1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами -2. `/meta-edit` — добавить новый реквизит в объект расширения -3. `/form-edit` — вывести реквизит на заимствованную форму - -**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" -``` - -## Примеры - -```powershell -# Заимствовать один объект -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" - -# Заимствовать форму (автоматически заимствует родительский объект) -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" - -# Несколько объектов за раз -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" - -# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute - -# Заимствовать форму с ВСЕМИ реквизитами объекта -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All -``` - -## Верификация - -``` -/cfe-validate -``` - +--- +name: cfe-borrow +description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации +argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-borrow — Заимствование объектов из конфигурации + +Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. + +## Предусловие + +Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. + +### Авто-определение ConfigPath + +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + +## Параметры + +| Параметр | Описание | +|----------|----------| +| `ExtensionPath` | Путь к каталогу расширения (обязат.) | +| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | +| `Object` | Что заимствовать (обязат.), batch через `;;` | +| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object | + +## Формат -Object + +- `Catalog.Контрагенты` — справочник +- `CommonModule.РаботаСФайлами` — общий модуль +- `Document.РеализацияТоваров` — документ +- `Enum.ВидыОплат` — перечисление +- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) +- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов +Поддерживаются все 44 типа объектов конфигурации. + +### Заимствование форм + +Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. + +Создаётся: +1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` +2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `` (начальное состояние) +3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` +4. **Регистрация** — `` в ChildObjects родительского объекта + +### Заимствование основного реквизита формы (-BorrowMainAttribute) + +**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. + +**Два режима**: +- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев +- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет + +**Типовой сценарий** (добавление реквизита + вывод на форму): +1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами +2. `/meta-edit` — добавить новый реквизит в объект расширения +3. `/form-edit` — вывести реквизит на заимствованную форму + +**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +``` + +## Примеры + +```powershell +# Заимствовать один объект +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" + +# Заимствовать форму (автоматически заимствует родительский объект) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" + +# Несколько объектов за раз +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" + +# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute + +# Заимствовать форму с ВСЕМИ реквизитами объекта +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 92af562b..6002842c 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -1,1866 +1,1866 @@ -# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][string]$ExtensionPath, - [Parameter(Mandatory)][string]$ConfigPath, - [Parameter(Mandatory)][string]$Object, - [string]$BorrowMainAttribute -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -function Info([string]$msg) { Write-Host "[INFO] $msg" } -function Warn([string]$msg) { Write-Host "[WARN] $msg" } - -# Form data-binding tags (value = attribute path). A binding survives only if its root -# attribute is borrowed into the form's ; otherwise it must be stripped or the -# platform rejects the form with "Неверный путь к данным" on load. -$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath') -# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. -$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath') - -# Strip data-binding tags whose root attribute isn't borrowed. -# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. -# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped. -function Strip-FormBindings { - param([string]$xml, [bool]$keepObjekt) - foreach ($tag in $script:formBindingDataTags) { - if ($keepObjekt) { - $xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*", '') - } else { - $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*", '') - } - } - foreach ($tag in $script:formBindingPictureTags) { - $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*", '') - } - return $xml -} - -# --- 1. Resolve paths --- -if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { - $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath -} -if (Test-Path $ExtensionPath -PathType Container) { - $candidate = Join-Path $ExtensionPath "Configuration.xml" - if (Test-Path $candidate) { $ExtensionPath = $candidate } - else { Write-Error "No Configuration.xml in extension directory: $ExtensionPath"; exit 1 } -} -if (-not (Test-Path $ExtensionPath)) { Write-Error "Extension file not found: $ExtensionPath"; exit 1 } -$extResolvedPath = (Resolve-Path $ExtensionPath).Path -$extDir = Split-Path $extResolvedPath -Parent - -if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath -} -if (Test-Path $ConfigPath -PathType Container) { - $candidate = Join-Path $ConfigPath "Configuration.xml" - if (Test-Path $candidate) { $ConfigPath = $candidate } - else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } -} -if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } -$cfgResolvedPath = (Resolve-Path $ConfigPath).Path -$cfgDir = Split-Path $cfgResolvedPath -Parent - -# --- 2. Load extension Configuration.xml --- -$script:xmlDoc = New-Object System.Xml.XmlDocument -$script:xmlDoc.PreserveWhitespace = $true -$script:xmlDoc.Load($extResolvedPath) - -$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" -$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" -$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" -$script:v8Ns = "http://v8.1c.ru/8.1/data/core" - -$root = $script:xmlDoc.DocumentElement - -$script:cfgEl = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { - $script:cfgEl = $child; break - } -} -if (-not $script:cfgEl) { Write-Error "No element found in extension"; exit 1 } - -$script:propsEl = $null -$script:childObjsEl = $null -foreach ($child in $script:cfgEl.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -eq "Properties") { $script:propsEl = $child } - if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } -} - -if (-not $script:propsEl) { Write-Error "No element found in extension"; exit 1 } -if (-not $script:childObjsEl) { Write-Error "No element found in extension"; exit 1 } - -# --- 3. Extract NamePrefix --- -$script:namePrefix = "" -foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "NamePrefix") { - $script:namePrefix = $child.InnerText.Trim(); break - } -} -Info "Extension NamePrefix: $($script:namePrefix)" - -# --- 4. Type mappings --- -$childTypeDirMap = @{ - "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" - "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" - "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" - "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" - "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" - "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" - "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" - "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" - "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" - "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" - "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" - "FunctionalOptionsParameter"="FunctionalOptionsParameters" - "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" - "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" - "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" - "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" - "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" - "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" - "XDTOPackage"="XDTOPackages"; "WebService"="WebServices" - "HTTPService"="HTTPServices"; "WSReference"="WSReferences" - "CommonAttribute"="CommonAttributes"; "Style"="Styles" -} - -# --- 4b. Russian synonym → English type --- -$synonymMap = @{ - "Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum" - "ОбщийМодуль"="CommonModule"; "ОбщаяКартинка"="CommonPicture" - "ОбщаяКоманда"="CommonCommand"; "ОбщийМакет"="CommonTemplate" - "ПланОбмена"="ExchangePlan"; "Отчет"="Report"; "Отчёт"="Report" - "Обработка"="DataProcessor"; "РегистрСведений"="InformationRegister" - "РегистрНакопления"="AccumulationRegister" - "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" - "ПланСчетов"="ChartOfAccounts"; "РегистрБухгалтерии"="AccountingRegister" - "ПланВидовРасчета"="ChartOfCalculationTypes"; "РегистрРасчета"="CalculationRegister" - "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" - "Подсистема"="Subsystem"; "Роль"="Role"; "Константа"="Constant" - "ФункциональнаяОпция"="FunctionalOption"; "ОпределяемыйТип"="DefinedType" - "ОбщаяФорма"="CommonForm"; "ЖурналДокументов"="DocumentJournal" - "ПараметрСеанса"="SessionParameter"; "ГруппаКоманд"="CommandGroup" - "ПодпискаНаСобытие"="EventSubscription"; "РегламентноеЗадание"="ScheduledJob" - "ОбщийРеквизит"="CommonAttribute"; "ПакетXDTO"="XDTOPackage" - "HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService" -} - -# --- 5. Canonical type order (44 types) --- -$script:typeOrder = @( - "Language","Subsystem","StyleItem","Style", - "CommonPicture","SessionParameter","Role","CommonTemplate", - "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", - "XDTOPackage","WebService","HTTPService","WSReference", - "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", - "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", - "Constant","CommonForm","Catalog","Document", - "DocumentNumerator","Sequence","DocumentJournal","Enum", - "Report","DataProcessor","InformationRegister","AccumulationRegister", - "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", - "ChartOfCalculationTypes","CalculationRegister", - "BusinessProcess","Task","IntegrationService" -) - -# --- 6. GeneratedType patterns per type --- -$script:generatedTypes = @{ - "Catalog" = @( - @{ prefix = "CatalogObject"; category = "Object" } - @{ prefix = "CatalogRef"; category = "Ref" } - @{ prefix = "CatalogSelection"; category = "Selection" } - @{ prefix = "CatalogList"; category = "List" } - @{ prefix = "CatalogManager"; category = "Manager" } - ) - "Document" = @( - @{ prefix = "DocumentObject"; category = "Object" } - @{ prefix = "DocumentRef"; category = "Ref" } - @{ prefix = "DocumentSelection"; category = "Selection" } - @{ prefix = "DocumentList"; category = "List" } - @{ prefix = "DocumentManager"; category = "Manager" } - ) - "Enum" = @( - @{ prefix = "EnumRef"; category = "Ref" } - @{ prefix = "EnumManager"; category = "Manager" } - @{ prefix = "EnumList"; category = "List" } - ) - "Constant" = @( - @{ prefix = "ConstantManager"; category = "Manager" } - @{ prefix = "ConstantValueManager"; category = "ValueManager" } - @{ prefix = "ConstantValueKey"; category = "ValueKey" } - ) - "InformationRegister" = @( - @{ prefix = "InformationRegisterRecord"; category = "Record" } - @{ prefix = "InformationRegisterManager"; category = "Manager" } - @{ prefix = "InformationRegisterSelection"; category = "Selection" } - @{ prefix = "InformationRegisterList"; category = "List" } - @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } - @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } - ) - "AccumulationRegister" = @( - @{ prefix = "AccumulationRegisterRecord"; category = "Record" } - @{ prefix = "AccumulationRegisterManager"; category = "Manager" } - @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } - @{ prefix = "AccumulationRegisterList"; category = "List" } - @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } - ) - "AccountingRegister" = @( - @{ prefix = "AccountingRegisterRecord"; category = "Record" } - @{ prefix = "AccountingRegisterManager"; category = "Manager" } - @{ prefix = "AccountingRegisterSelection"; category = "Selection" } - @{ prefix = "AccountingRegisterList"; category = "List" } - @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } - ) - "CalculationRegister" = @( - @{ prefix = "CalculationRegisterRecord"; category = "Record" } - @{ prefix = "CalculationRegisterManager"; category = "Manager" } - @{ prefix = "CalculationRegisterSelection"; category = "Selection" } - @{ prefix = "CalculationRegisterList"; category = "List" } - @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } - ) - "ChartOfAccounts" = @( - @{ prefix = "ChartOfAccountsObject"; category = "Object" } - @{ prefix = "ChartOfAccountsRef"; category = "Ref" } - @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } - @{ prefix = "ChartOfAccountsList"; category = "List" } - @{ prefix = "ChartOfAccountsManager"; category = "Manager" } - ) - "ChartOfCharacteristicTypes" = @( - @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } - @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } - @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } - @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } - @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } - ) - "ChartOfCalculationTypes" = @( - @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } - @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } - @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } - @{ prefix = "ChartOfCalculationTypesList"; category = "List" } - @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } - @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } - @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } - @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } - ) - "BusinessProcess" = @( - @{ prefix = "BusinessProcessObject"; category = "Object" } - @{ prefix = "BusinessProcessRef"; category = "Ref" } - @{ prefix = "BusinessProcessSelection"; category = "Selection" } - @{ prefix = "BusinessProcessList"; category = "List" } - @{ prefix = "BusinessProcessManager"; category = "Manager" } - ) - "Task" = @( - @{ prefix = "TaskObject"; category = "Object" } - @{ prefix = "TaskRef"; category = "Ref" } - @{ prefix = "TaskSelection"; category = "Selection" } - @{ prefix = "TaskList"; category = "List" } - @{ prefix = "TaskManager"; category = "Manager" } - ) - "ExchangePlan" = @( - @{ prefix = "ExchangePlanObject"; category = "Object" } - @{ prefix = "ExchangePlanRef"; category = "Ref" } - @{ prefix = "ExchangePlanSelection"; category = "Selection" } - @{ prefix = "ExchangePlanList"; category = "List" } - @{ prefix = "ExchangePlanManager"; category = "Manager" } - ) - "DocumentJournal" = @( - @{ prefix = "DocumentJournalSelection"; category = "Selection" } - @{ prefix = "DocumentJournalList"; category = "List" } - @{ prefix = "DocumentJournalManager"; category = "Manager" } - ) - "Report" = @( - @{ prefix = "ReportObject"; category = "Object" } - @{ prefix = "ReportManager"; category = "Manager" } - ) - "DataProcessor" = @( - @{ prefix = "DataProcessorObject"; category = "Object" } - @{ prefix = "DataProcessorManager"; category = "Manager" } - ) - "DefinedType" = @( - @{ prefix = "DefinedType"; category = "DefinedType" } - ) -} - -# Types that need ChildObjects element -$typesWithChildObjects = @( - "Catalog","Document","ExchangePlan","ChartOfAccounts", - "ChartOfCharacteristicTypes","ChartOfCalculationTypes", - "BusinessProcess","Task","Enum", - "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister" -) - -# CommonModule properties to copy from source -$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") - -# Standard system fields to skip when collecting DataPath references -$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") - -# --- 7. XML manipulation helpers (from cf-edit) --- -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - $depth = 0; $current = $container - while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Expand-SelfClosingElement($container, $parentIndent) { - if (-not $container.HasChildNodes -or $container.IsEmpty) { - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } -} - -# --- 7b. Detect format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$script:formatVersion = Detect-FormatVersion $extDir - -# --- 8. Namespaces declaration for object XML --- -$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' - -# --- 9. Parse -Object into items --- -$items = @() -foreach ($part in $Object.Split(";;")) { - $trimmed = $part.Trim() - if ($trimmed) { $items += $trimmed } -} - -if ($items.Count -eq 0) { - Write-Error "No objects specified in -Object" - exit 1 -} - -# --- 9b. Validate -BorrowMainAttribute --- -if ($BorrowMainAttribute) { - # PS treats -BorrowMainAttribute without value as "True" - if ($BorrowMainAttribute -eq "True") { $BorrowMainAttribute = "Form" } - if ($BorrowMainAttribute -notin @("Form","All")) { - Write-Error "-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)" - exit 1 - } - # Validate: only with .Form. pattern - $hasForm = $false - foreach ($item in $items) { if ($item -match '\.Form\.') { $hasForm = $true; break } } - if (-not $hasForm) { - Write-Error "-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')" - exit 1 - } -} - -# --- 10. Helper: read source object XML --- -function Read-SourceObject { - param([string]$typeName, [string]$objName) - - $dirName = $childTypeDirMap[$typeName] - if (-not $dirName) { - Write-Error "Unknown type '$typeName'" - exit 1 - } - - $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" - if (-not (Test-Path $srcFile)) { - Write-Error "Source object not found: $srcFile" - exit 1 - } - - $srcDoc = New-Object System.Xml.XmlDocument - $srcDoc.PreserveWhitespace = $false - $srcDoc.Load($srcFile) - - $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) - $srcNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $srcNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - - # Find the type element (e.g. ) - $srcRoot = $srcDoc.DocumentElement - $srcEl = $null - foreach ($c in $srcRoot.ChildNodes) { - if ($c.NodeType -eq 'Element') { $srcEl = $c; break } - } - if (-not $srcEl) { - Write-Error "No metadata element found in ${dirName}/${objName}.xml" - exit 1 - } - - # Extract uuid - $srcUuid = $srcEl.GetAttribute("uuid") - if (-not $srcUuid) { - Write-Error "No uuid attribute on source element in ${dirName}/${objName}.xml" - exit 1 - } - - # Extract properties for CommonModule - $srcProps = @{} - $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) - if ($propsNode) { - foreach ($propName in $commonModuleProps) { - $propNode = $propsNode.SelectSingleNode("md:${propName}", $srcNs) - if ($propNode) { - $srcProps[$propName] = $propNode.InnerText.Trim() - } - } - # DefinedType: carry the definition. A type alias is meaningless as a bare shell — - # the platform needs its underlying type (e.g. to know a column is a summable Number for totals). - if ($typeName -eq "DefinedType") { - $typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs) - if ($typeNode) { - $srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '') - } - } - } - - return @{ - Uuid = $srcUuid - Properties = $srcProps - Element = $srcEl - NsManager = $srcNs - } -} - -# --- 10b. Helper: read source form UUID --- -function Read-SourceFormUuid { - param([string]$typeName, [string]$objName, [string]$formName) - - $dirName = $childTypeDirMap[$typeName] - $srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml" - if (-not (Test-Path $srcFile)) { - Write-Error "Source form not found: $srcFile" - exit 1 - } - - $srcDoc = New-Object System.Xml.XmlDocument - $srcDoc.PreserveWhitespace = $false - $srcDoc.Load($srcFile) - - $srcEl = $null - foreach ($c in $srcDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $srcEl = $c; break } - } - if (-not $srcEl) { - Write-Error "No metadata element found in source form: $srcFile" - exit 1 - } - - $srcUuid = $srcEl.GetAttribute("uuid") - if (-not $srcUuid) { - Write-Error "No uuid attribute on source form element: $srcFile" - exit 1 - } - - return $srcUuid -} - -# --- 10c. Helper: borrow a form --- -function Borrow-Form { - param([string]$typeName, [string]$objName, [string]$formName, [switch]$BorrowMainAttr) - - $dirName = $childTypeDirMap[$typeName] - $enc = New-Object System.Text.UTF8Encoding($true) - - # 1. Read source form UUID - $formUuid = Read-SourceFormUuid $typeName $objName $formName - Info " Source form UUID: $formUuid" - - # 2. Read source Form.xml content - $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" - if (-not (Test-Path $srcFormXmlPath)) { - Write-Error "Source Form.xml not found: $srcFormXmlPath" - exit 1 - } - $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) - - # 3. Generate form metadata XML (ФормаЭлемента.xml). - # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent - # (regenerating it would churn the form's identity on every rerun). - $formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml" - $newFormUuid = "" - if (Test-Path $formMetaFileExisting) { - try { - $existingDoc = New-Object System.Xml.XmlDocument - $existingDoc.Load($formMetaFileExisting) - $existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']") - if ($existingFormNode) { - $existingUuid = $existingFormNode.GetAttribute("uuid") - if ($existingUuid) { $newFormUuid = $existingUuid } - } - } catch { } - } - if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() } - $formMetaSb = New-Object System.Text.StringBuilder - $formMetaSb.AppendLine("") | Out-Null - $formMetaSb.AppendLine("") | Out-Null - $formMetaSb.AppendLine("`t") | Out-Null - $formMetaSb.AppendLine("`t`t") | Out-Null - $formMetaSb.AppendLine("`t`t") | Out-Null - $formMetaSb.AppendLine("`t`t`tAdopted") | Out-Null - $formMetaSb.AppendLine("`t`t`t${formName}") | Out-Null - $formMetaSb.AppendLine("`t`t`t") | Out-Null - $formMetaSb.AppendLine("`t`t`t${formUuid}") | Out-Null - $formMetaSb.AppendLine("`t`t`tManaged") | Out-Null - $formMetaSb.AppendLine("`t`t") | Out-Null - $formMetaSb.AppendLine("`t") | Out-Null - $formMetaSb.Append("") | Out-Null - - # 4. Create directories - $formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms" - if (-not (Test-Path $formMetaDir)) { - New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null - } - - # Write form metadata - $formMetaFile = Join-Path $formMetaDir "${formName}.xml" - [System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc) - Info " Created: $formMetaFile" - - # 5. Generate Form.xml with BaseForm (visual elements only) - # Parse source Form.xml as XmlDocument - $srcFormDoc = New-Object System.Xml.XmlDocument - $srcFormDoc.PreserveWhitespace = $true - $srcFormDoc.Load($srcFormXmlPath) - $srcFormEl = $srcFormDoc.DocumentElement - - # Borrowed form must use the extension's format version (not the source form's), so the whole - # extension stays uniform — otherwise the platform rejects the import on a version mismatch - # (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version. - $formVersion = $script:formatVersion - - # Find direct children: form properties, AutoCommandBar, ChildItems - $srcAutoCmd = $null - $srcChildItems = $null - $formProps = @() - $reachedVisual = $false - foreach ($fc in $srcFormEl.ChildNodes) { - if ($fc.NodeType -ne 'Element') { continue } - if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { - $reachedVisual = $true; $srcAutoCmd = $fc; continue - } - if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { - $reachedVisual = $true; $srcChildItems = $fc; continue - } - if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') { - $reachedVisual = $true; continue - } - if (-not $reachedVisual) { - $formProps += $fc.OuterXml - } - } - - # Get OuterXml and strip redundant namespace redeclarations (they're on root
) - $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' - - # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false - $autoCmdXml = "" - if ($srcAutoCmd) { - $autoCmdXml = $srcAutoCmd.OuterXml - $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') - $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') - $autoCmdXml = $autoCmdXml -replace 'true', 'false' - # Strip ExcludedCommand (references to standard commands invalid in extension) - $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') - # Strip data-binding tags whose root attribute isn't borrowed - $autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr) - } - - # ChildItems: copy full tree, clean up base-config references - $childItemsXml = "" - if ($srcChildItems) { - $childItemsXml = $srcChildItems.OuterXml - $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') - # Replace all CommandName values with 0 - $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') - # Strip data-binding tags whose root attribute isn't borrowed - # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*) - $childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr) - # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) - $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') - # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) - $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*Items\.[^<]*.*?', '') - # Strip element-level Events (base form handlers not in extension) - $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') - - # Collect CommonPicture references from ChildItems and AutoCommandBar - $referencedPictures = @{} - $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') - foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } - if ($autoCmdXml) { - $picRefs2 = [regex]::Matches($autoCmdXml, 'CommonPicture\.(\w+)') - foreach ($m in $picRefs2) { $referencedPictures[$m.Groups[1].Value] = $true } - } - - # Auto-borrow referenced CommonPictures (if not already borrowed) - $autoBorrowedPics = @() - foreach ($picName in $referencedPictures.Keys) { - if (-not (Test-ObjectBorrowed "CommonPicture" $picName)) { - $picSrcFile = Join-Path (Join-Path $cfgDir "CommonPictures") "${picName}.xml" - if (Test-Path $picSrcFile) { - $src = Read-SourceObject "CommonPicture" $picName - $borrowedXml = Build-BorrowedObjectXml "CommonPicture" $picName $src.Uuid $src.Properties - $targetDir = Join-Path $extDir "CommonPictures" - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - $targetFile = Join-Path $targetDir "${picName}.xml" - $encBom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) - Add-ToChildObjects "CommonPicture" $picName - $autoBorrowedPics += $picName - $script:borrowedFiles += $targetFile - Info " Auto-borrowed: CommonPicture.${picName}" - } else { - Warn " CommonPicture.${picName} not found in source config — will strip from form" - } - } - } - - # Collect all borrowed CommonPictures (including previously borrowed) - $borrowedPicSet = @{} - $nsMgr2 = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable) - $nsMgr2.AddNamespace("md", $script:mdNs) - $picNodes = $script:xmlDoc.SelectNodes("//md:ChildObjects/md:CommonPicture", $nsMgr2) - foreach ($pn in $picNodes) { $borrowedPicSet[$pn.InnerText] = $true } - - # Strip blocks referencing non-borrowed CommonPictures - $picBlockPattern = '(?s)\s*\s*CommonPicture\.(\w+).*?' - $picMatches = [regex]::Matches($childItemsXml, $picBlockPattern) - # Process in reverse order to preserve positions - for ($mi = $picMatches.Count - 1; $mi -ge 0; $mi--) { - $pm = $picMatches[$mi] - $cpName = $pm.Groups[1].Value - if (-not $borrowedPicSet.ContainsKey($cpName)) { - $childItemsXml = $childItemsXml.Remove($pm.Index, $pm.Length) - } - } - # Strip StdPicture blocks (except Print) - $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') - - # Same Picture strip for AutoCommandBar - if ($autoCmdXml) { - $acPicMatches = [regex]::Matches($autoCmdXml, $picBlockPattern) - for ($mi = $acPicMatches.Count - 1; $mi -ge 0; $mi--) { - $pm = $acPicMatches[$mi] - $cpName = $pm.Groups[1].Value - if (-not $borrowedPicSet.ContainsKey($cpName)) { - $autoCmdXml = $autoCmdXml.Remove($pm.Index, $pm.Length) - } - } - $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') - } - - # Auto-borrow StyleItems referenced in ChildItems - # Pattern 1: , - # Pattern 2: style:XXX, style:XXX, etc. - $referencedStyles = @{} - $styleRefs1 = [regex]::Matches($childItemsXml, 'ref="style:(\w+)"[^>]*kind="StyleItem"') - foreach ($m in $styleRefs1) { $referencedStyles[$m.Groups[1].Value] = $true } - $styleRefs2 = [regex]::Matches($childItemsXml, '>style:(\w+)') - foreach ($m in $styleRefs2) { $referencedStyles[$m.Groups[1].Value] = $true } - - foreach ($styleName in $referencedStyles.Keys) { - if (-not (Test-ObjectBorrowed "StyleItem" $styleName)) { - $styleSrcFile = Join-Path (Join-Path $cfgDir "StyleItems") "${styleName}.xml" - if (Test-Path $styleSrcFile) { - $src = Read-SourceObject "StyleItem" $styleName - $borrowedXml = Build-BorrowedObjectXml "StyleItem" $styleName $src.Uuid $src.Properties - $targetDir = Join-Path $extDir "StyleItems" - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - $targetFile = Join-Path $targetDir "${styleName}.xml" - $encBom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) - Add-ToChildObjects "StyleItem" $styleName - $script:borrowedFiles += $targetFile - Info " Auto-borrowed: StyleItem.${styleName}" - } else { - Warn " StyleItem.${styleName} not found in source config" - } - } - } - # Auto-borrow Enums + EnumValues referenced via DesignTimeRef in ChoiceParameters - # Collect Enum -> [EnumValue names] map - $dtRefs = [regex]::Matches($childItemsXml, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)') - $referencedEnumValues = @{} - foreach ($m in $dtRefs) { - $eName = $m.Groups[1].Value - $evName = $m.Groups[2].Value - if (-not $referencedEnumValues.ContainsKey($eName)) { $referencedEnumValues[$eName] = @{} } - $referencedEnumValues[$eName][$evName] = $true - } - - foreach ($enumName in $referencedEnumValues.Keys) { - if (-not (Test-ObjectBorrowed "Enum" $enumName)) { - $enumSrcFile = Join-Path (Join-Path $cfgDir "Enums") "${enumName}.xml" - if (Test-Path $enumSrcFile) { - # Read source Enum to get UUID and EnumValue UUIDs - $srcParser = New-Object System.Xml.XmlDocument - $srcParser.PreserveWhitespace = $true - $srcParser.Load($enumSrcFile) - $srcEnumEl = $null - foreach ($cn in $srcParser.DocumentElement.ChildNodes) { - if ($cn.NodeType -eq 'Element') { $srcEnumEl = $cn; break } - } - $srcEnumUuid = $srcEnumEl.GetAttribute("uuid") - - # Find source EnumValues by name - $enumValueXmls = @() - $neededValues = $referencedEnumValues[$enumName] - $srcNsMgr = New-Object System.Xml.XmlNamespaceManager($srcParser.NameTable) - $srcNsMgr.AddNamespace("md", $script:mdNs) - $srcEvNodes = $srcEnumEl.SelectNodes("md:ChildObjects/md:EnumValue", $srcNsMgr) - foreach ($evNode in $srcEvNodes) { - $evUuid = $evNode.GetAttribute("uuid") - $evNameNode = $evNode.SelectSingleNode("md:Properties/md:Name", $srcNsMgr) - if ($evNameNode -and $neededValues.ContainsKey($evNameNode.InnerText)) { - $newEvUuid = [guid]::NewGuid().ToString() - $enumValueXmls += @" - - - - Adopted - $($evNameNode.InnerText) - - ${evUuid} - - -"@ - } - } - - # Build borrowed Enum with EnumValues in ChildObjects - $src = Read-SourceObject "Enum" $enumName - $borrowedXml = Build-BorrowedObjectXml "Enum" $enumName $src.Uuid $src.Properties - if ($enumValueXmls.Count -gt 0) { - $evBlock = ($enumValueXmls -join "`r`n") - $borrowedXml = $borrowedXml -replace '', "`r`n${evBlock}`r`n`t`t" - } - - $targetDir = Join-Path $extDir "Enums" - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - $targetFile = Join-Path $targetDir "${enumName}.xml" - $encBom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) - Add-ToChildObjects "Enum" $enumName - $script:borrowedFiles += $targetFile - Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" - } else { - Warn " Enum.${enumName} not found in source config" - } - } - } - } - - # Extract the opening tag from source text (preserves namespace declarations) - $xmlDecl = '' - $formTag = "" - if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] } - if ($srcFormContent -match '(]*>)') { $formTag = $Matches[1] } - - # Build output Form.xml - $formXmlSb = New-Object System.Text.StringBuilder - $formXmlSb.Append($xmlDecl) | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - $formXmlSb.Append($formTag) | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - - # Part 1: form properties + AutoCommandBar + ChildItems - foreach ($propXml in $formProps) { - $propXml = [regex]::Replace($propXml, $nsStripPattern, '') - $formXmlSb.Append("`t$propXml`r`n") | Out-Null - } - if ($autoCmdXml) { - $formXmlSb.Append("`t$autoCmdXml") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - } - if ($childItemsXml) { - $formXmlSb.Append("`t$childItemsXml") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - } - # Attributes: empty or with MainAttribute when BorrowMainAttr - if ($BorrowMainAttr) { - $objTypePrefix = "" - $gtList = $script:generatedTypes[$typeName] - if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } } - $mainAttrType = "cfg:${objTypePrefix}.${objName}" - $formXmlSb.Append("`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t${mainAttrType}`r`n") | Out-Null - $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`r`n") | Out-Null - $formXmlSb.Append("`t") | Out-Null - } else { - $formXmlSb.Append("`t") | Out-Null - } - $formXmlSb.Append("`r`n") | Out-Null - - # BaseForm: same content, indented one more level - $formXmlSb.Append("`t") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - - foreach ($propXml in $formProps) { - $propXml = [regex]::Replace($propXml, $nsStripPattern, '') - $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null - } - if ($autoCmdXml) { - $acLines = $autoCmdXml -split "`r?`n" - for ($li = 0; $li -lt $acLines.Count; $li++) { - if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } - else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null } - $formXmlSb.Append("`r`n") | Out-Null - } - } - if ($childItemsXml) { - # Reindent ChildItems for BaseForm (+1 tab level) - $ciLines = $childItemsXml -split "`r?`n" - for ($li = 0; $li -lt $ciLines.Count; $li++) { - if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } - else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } - $formXmlSb.Append("`r`n") | Out-Null - } - } - - # BaseForm Attributes: same as main section - if ($BorrowMainAttr) { - $formXmlSb.Append("`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`t${mainAttrType}`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t") | Out-Null - } else { - $formXmlSb.Append("`t`t") | Out-Null - } - $formXmlSb.Append("`r`n") | Out-Null - $formXmlSb.Append("`t") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null - $formXmlSb.Append("") | Out-Null - - # Write Form.xml - $formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext" - if (-not (Test-Path $formXmlDir)) { - New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null - } - $formXmlFile = Join-Path $formXmlDir "Form.xml" - [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) - Info " Created: $formXmlFile" - - # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must - # not clobber user code added to the form module). - $moduleDir = Join-Path $formXmlDir "Form" - if (-not (Test-Path $moduleDir)) { - New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null - } - $moduleBslFile = Join-Path $moduleDir "Module.bsl" - if (Test-Path $moduleBslFile) { - Info " Preserved existing Module.bsl" - } else { - [System.IO.File]::WriteAllText($moduleBslFile, "", $enc) - Info " Created: $moduleBslFile" - } - - # 7. Register form in parent object ChildObjects - Register-FormInObject $typeName $objName $formName - - return @($formMetaFile, $formXmlFile, $moduleBslFile) -} - -# --- 10d. Helper: register form in parent object's ChildObjects --- -function Register-FormInObject { - param([string]$typeName, [string]$objName, [string]$formName) - - $dirName = $childTypeDirMap[$typeName] - $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" - - if (-not (Test-Path $objFile)) { - Warn "Parent object file not found: $objFile — form not registered in ChildObjects" - return - } - - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $true - $objDoc.Load($objFile) - - $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - # Find the type element - $objEl = $null - foreach ($c in $objDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $objEl = $c; break } - } - if (-not $objEl) { - Warn "No type element in $objFile — form not registered" - return - } - - # Find or create ChildObjects - $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) - if (-not $childObjs) { - # Create ChildObjects element - $childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses") - $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null - $objEl.AppendChild($childObjs) | Out-Null - $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null - } - - # Check dedup - foreach ($c in $childObjs.ChildNodes) { - if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) { - Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}" - return - } - } - - # Expand self-closing if needed - if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { - $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") - $childObjs.AppendChild($closeWs) | Out-Null - } - - # Add
formName
- $formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") - $formEl.InnerText = $formName - - $trailing = $childObjs.LastChild - $ws = $objDoc.CreateWhitespace("`r`n`t`t`t") - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $childObjs.InsertBefore($ws, $trailing) | Out-Null - $childObjs.InsertBefore($formEl, $trailing) | Out-Null - } else { - $childObjs.AppendChild($ws) | Out-Null - $childObjs.AppendChild($formEl) | Out-Null - } - - # Save object XML - $settings2 = New-Object System.Xml.XmlWriterSettings - $settings2.Encoding = New-Object System.Text.UTF8Encoding($true) - $settings2.Indent = $false - $settings2.NewLineHandling = [System.Xml.NewLineHandling]::None - - $memStream2 = New-Object System.IO.MemoryStream - $writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2) - $objDoc.Save($writer2) - $writer2.Flush(); $writer2.Close() - - $bytes2 = $memStream2.ToArray() - $memStream2.Close() - $text2 = [System.Text.Encoding]::UTF8.GetString($bytes2) - if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) } - $text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"') - - $utf8Bom2 = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2) - Info " Registered form in: $objFile" -} - -# --- 10e. Helper: check if object is already borrowed in extension --- -function Test-ObjectBorrowed { - param([string]$typeName, [string]$objName) - - $dirName = $childTypeDirMap[$typeName] - $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" - return (Test-Path $objFile) -} - -# --- 11. Helper: generate InternalInfo XML --- -function Build-InternalInfoXml { - param([string]$typeName, [string]$objName, [string]$indent) - - $types = $script:generatedTypes[$typeName] - if (-not $types -or $types.Count -eq 0) { - return "${indent}" - } - - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("${indent}") | Out-Null - - # ExchangePlan: ThisNode UUID before GeneratedTypes - if ($typeName -eq "ExchangePlan") { - $thisNodeUuid = [guid]::NewGuid().ToString() - $sb.AppendLine("${indent}`t${thisNodeUuid}") | Out-Null - } - - foreach ($gt in $types) { - $fullName = "$($gt.prefix).${objName}" - $typeId = [guid]::NewGuid().ToString() - $valueId = [guid]::NewGuid().ToString() - $sb.AppendLine("${indent}`t") | Out-Null - $sb.AppendLine("${indent}`t`t${typeId}") | Out-Null - $sb.AppendLine("${indent}`t`t${valueId}") | Out-Null - $sb.AppendLine("${indent}`t") | Out-Null - } - - $sb.Append("${indent}") | Out-Null - return $sb.ToString() -} - -# --- 11b. Collect DataPath references from source Form.xml --- -function Collect-FormDataPaths { - param([string]$formXmlPath) - - $enc = New-Object System.Text.UTF8Encoding($true) - $content = [System.IO.File]::ReadAllText($formXmlPath, $enc) - - $firstLevel = @{} - $deepPaths = @() - - # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) - # for Объект.* references — picture-path tags carry picture indices, not data attributes. - foreach ($tag in $script:formBindingDataTags) { - $bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)") - foreach ($m in $bms) { - $path = $m.Groups[1].Value - $segments = $path.Split(".") - $seg0 = $segments[0] - if ($script:standardFields -contains $seg0) { continue } - $firstLevel[$seg0] = $true - if ($segments.Count -ge 2) { - $seg1 = $segments[1] - if ($script:standardFields -contains $seg1) { continue } - $seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null } - $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 } - } - } - } - - # Also scan Объект.X — object attributes referenced by filter/conditional-appearance - # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. - $fieldMatches = [regex]::Matches($content, "[^<]*\bОбъект\.(\w+(?:\.\w+)*)") - foreach ($m in $fieldMatches) { - $path = $m.Groups[1].Value - $segments = $path.Split(".") - $seg0 = $segments[0] - if ($script:standardFields -contains $seg0) { continue } - $firstLevel[$seg0] = $true - if ($segments.Count -ge 2) { - $seg1 = $segments[1] - if ($script:standardFields -contains $seg1) { continue } - $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1 } - } - } - - # Deduplicate deep paths - $seen = @{} - $uniqueDeep = @() - foreach ($dp in $deepPaths) { - $key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)" - if (-not $seen.ContainsKey($key)) { - $seen[$key] = $true - $uniqueDeep += $dp - } - } - - return @{ FirstLevel = $firstLevel; DeepPaths = $uniqueDeep } -} - -# --- 11c. Resolve source attributes and tabular sections --- -function Resolve-SourceAttributes { - param([string]$typeName, [string]$objName, $firstLevelNames) - # $firstLevelNames: hashtable of names, or $null for "all" - - $dirName = $childTypeDirMap[$typeName] - $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" - if (-not (Test-Path $srcFile)) { - Write-Error "Source object not found: $srcFile" - exit 1 - } - - $srcDoc = New-Object System.Xml.XmlDocument - $srcDoc.PreserveWhitespace = $false - $srcDoc.Load($srcFile) - - $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) - $srcNs.AddNamespace("md", $script:mdNs) - $srcNs.AddNamespace("xr", $script:xrNs) - $srcNs.AddNamespace("v8", $script:v8Ns) - - $srcEl = $null - foreach ($c in $srcDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $srcEl = $c; break } - } - if (-not $srcEl) { Write-Error "No metadata element in source: $srcFile"; exit 1 } - - $childObjs = $srcEl.SelectSingleNode("md:ChildObjects", $srcNs) - if (-not $childObjs) { return @{ Attributes = @(); TabularSections = @(); ExtraProps = @{} } } - - $attrs = @() - $tabSections = @() - - foreach ($child in $childObjs.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - - if ($child.LocalName -eq 'Attribute') { - $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) - if (-not $nameNode) { continue } - $attrName = $nameNode.InnerText - if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($attrName)) { continue } - - $uuid = $child.GetAttribute("uuid") - $typeNode = $child.SelectSingleNode("md:Properties/md:Type", $srcNs) - $typeXml = if ($typeNode) { $typeNode.OuterXml } else { "" } - # Strip namespace declarations from Type - $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') - - $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } - } - elseif ($child.LocalName -eq 'TabularSection') { - $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) - if (-not $nameNode) { continue } - $tsName = $nameNode.InnerText - if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($tsName)) { continue } - - $tsUuid = $child.GetAttribute("uuid") - - # Extract GeneratedTypes from InternalInfo - $tsGenTypes = @() - $iiNode = $child.SelectSingleNode("md:InternalInfo", $srcNs) - if ($iiNode) { - $gtNodes = $iiNode.SelectNodes("xr:GeneratedType", $srcNs) - foreach ($gt in $gtNodes) { - $tsGenTypes += @{ - Name = $gt.GetAttribute("name") - Category = $gt.GetAttribute("category") - TypeId = $gt.SelectSingleNode("xr:TypeId", $srcNs).InnerText - ValueId = $gt.SelectSingleNode("xr:ValueId", $srcNs).InnerText - } - } - } - - # Extract ALL child attributes of TabularSection - $tsAttrs = @() - $tsChildObjs = $child.SelectSingleNode("md:ChildObjects", $srcNs) - if ($tsChildObjs) { - foreach ($tsChild in $tsChildObjs.ChildNodes) { - if ($tsChild.NodeType -ne 'Element' -or $tsChild.LocalName -ne 'Attribute') { continue } - $tsAttrName = $tsChild.SelectSingleNode("md:Properties/md:Name", $srcNs) - if (-not $tsAttrName) { continue } - $tsAttrUuid = $tsChild.GetAttribute("uuid") - $tsTypeNode = $tsChild.SelectSingleNode("md:Properties/md:Type", $srcNs) - $tsTypeXml = if ($tsTypeNode) { $tsTypeNode.OuterXml } else { "" } - $tsTypeXml = [regex]::Replace($tsTypeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') - $tsAttrs += @{ Name = $tsAttrName.InnerText; Uuid = $tsAttrUuid; TypeXml = $tsTypeXml } - } - } - - $tabSections += @{ Name = $tsName; Uuid = $tsUuid; GeneratedTypes = $tsGenTypes; Attributes = $tsAttrs } - } - } - - # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) - # Ordered so PS emits the same property order as the Python port (dict preserves insertion order). - $extraProps = [ordered]@{} - $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) - if ($propsNode) { - $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", - "NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") - foreach ($pName in $propsToExtract) { - $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) - if ($pNode) { $extraProps[$pName] = $pNode.InnerText } - } - } - - return @{ Attributes = $attrs; TabularSections = $tabSections; ExtraProps = $extraProps } -} - -# --- 11d. Build adopted attribute XML --- -function Build-AdoptedAttributeXml { - param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) - - $newUuid = [guid]::NewGuid().ToString() - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("${indent}") | Out-Null - $sb.AppendLine("${indent}`t") | Out-Null - $sb.AppendLine("${indent}`t") | Out-Null - $sb.AppendLine("${indent}`t`tAdopted") | Out-Null - $sb.AppendLine("${indent}`t`t${name}") | Out-Null - $sb.AppendLine("${indent}`t`t") | Out-Null - $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null - $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null - $sb.AppendLine("${indent}`t") | Out-Null - $sb.Append("${indent}") | Out-Null - return $sb.ToString() -} - -# --- 11e. Build adopted tabular section XML --- -function Build-AdoptedTabularSectionXml { - param([string]$tsName, [string]$sourceUuid, $generatedTypes, $childAttrs, [string]$indent) - - $newUuid = [guid]::NewGuid().ToString() - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("${indent}") | Out-Null - - # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) - if ($generatedTypes -and $generatedTypes.Count -gt 0) { - $sb.AppendLine("${indent}`t") | Out-Null - foreach ($gt in $generatedTypes) { - $newTid = [guid]::NewGuid().ToString() - $newVid = [guid]::NewGuid().ToString() - $sb.AppendLine("${indent}`t`t") | Out-Null - $sb.AppendLine("${indent}`t`t`t${newTid}") | Out-Null - $sb.AppendLine("${indent}`t`t`t${newVid}") | Out-Null - $sb.AppendLine("${indent}`t`t") | Out-Null - } - $sb.AppendLine("${indent}`t") | Out-Null - } else { - $sb.AppendLine("${indent}`t") | Out-Null - } - - $sb.AppendLine("${indent}`t") | Out-Null - $sb.AppendLine("${indent}`t`tAdopted") | Out-Null - $sb.AppendLine("${indent}`t`t${tsName}") | Out-Null - $sb.AppendLine("${indent}`t`t") | Out-Null - $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null - $sb.AppendLine("${indent}`t") | Out-Null - - # ChildObjects with all attributes - if ($childAttrs -and $childAttrs.Count -gt 0) { - $sb.AppendLine("${indent}`t") | Out-Null - foreach ($ca in $childAttrs) { - $caXml = Build-AdoptedAttributeXml $ca.Name $ca.Uuid $ca.TypeXml "${indent}`t`t" - $sb.AppendLine($caXml) | Out-Null - } - $sb.AppendLine("${indent}`t") | Out-Null - } else { - $sb.AppendLine("${indent}`t") | Out-Null - } - - $sb.Append("${indent}") | Out-Null - return $sb.ToString() -} - -# --- 11f. Collect reference types from attribute Type XML strings --- -function Collect-ReferenceTypes { - param([string[]]$typeXmls) - - $result = @{} - foreach ($typeXml in $typeXmls) { - # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. - $refMatches = [regex]::Matches($typeXml, 'cfg:(\w+)Ref\.(\w+)') - foreach ($m in $refMatches) { - $refPrefix = $m.Groups[1].Value # e.g. "Catalog", "Enum", "Document" - $objName = $m.Groups[2].Value - $key = "${refPrefix}.${objName}" - if (-not $result.ContainsKey($key)) { - $result[$key] = @{ TypeName = $refPrefix; ObjName = $objName } - } - } - # cfg:DefinedType.XXX (via v8:TypeSet or v8:Type) - $dtMatches = [regex]::Matches($typeXml, 'cfg:DefinedType\.(\w+)') - foreach ($m in $dtMatches) { - $dtName = $m.Groups[1].Value - $key = "DefinedType.${dtName}" - if (-not $result.ContainsKey($key)) { - $result[$key] = @{ TypeName = "DefinedType"; ObjName = $dtName } - } - } - } - return @($result.Values) -} - -# --- 11g. Merge adopted attributes into existing extension object XML --- -function Merge-AttributesIntoObject { - param([string]$typeName, [string]$objName, $attrsToAdd) - - $dirName = $childTypeDirMap[$typeName] - $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" - if (-not (Test-Path $objFile)) { - Warn "Cannot merge attributes: $objFile not found" - return - } - - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $true - $objDoc.Load($objFile) - - $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $objNs.AddNamespace("md", $script:mdNs) - - $objEl = $null - foreach ($c in $objDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $objEl = $c; break } - } - if (-not $objEl) { Warn "No type element in $objFile"; return } - - $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) - if (-not $childObjs) { - $childObjs = $objDoc.CreateElement("ChildObjects", $script:mdNs) - $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null - $objEl.AppendChild($childObjs) | Out-Null - $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null - } - - # Collect existing attribute names for dedup - $existingNames = @{} - foreach ($c in $childObjs.ChildNodes) { - if ($c.NodeType -ne 'Element' -or $c.LocalName -ne 'Attribute') { continue } - $nameNode = $c.SelectSingleNode("md:Properties/md:Name", $objNs) - if ($nameNode) { $existingNames[$nameNode.InnerText] = $true } - } - - $added = 0 - foreach ($attr in $attrsToAdd) { - if ($existingNames.ContainsKey($attr.Name)) { continue } - $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" - - # Expand self-closing ChildObjects if needed - if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { - $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") - $childObjs.AppendChild($closeWs) | Out-Null - } - - $added++ - } - - if ($added -gt 0) { - # Build all adopted attributes as text and do string-level insertion - $allAttrXml = "" - foreach ($attr in $attrsToAdd) { - if ($existingNames.ContainsKey($attr.Name)) { continue } - $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") - } - - # Save via text manipulation to avoid namespace issues with InnerXml - $settings3 = New-Object System.Xml.XmlWriterSettings - $settings3.Encoding = New-Object System.Text.UTF8Encoding($true) - $settings3.Indent = $false - $settings3.NewLineHandling = [System.Xml.NewLineHandling]::None - $memStream3 = New-Object System.IO.MemoryStream - $writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3) - $objDoc.Save($writer3) - $writer3.Flush(); $writer3.Close() - $bytes3 = $memStream3.ToArray() - $memStream3.Close() - $text3 = [System.Text.Encoding]::UTF8.GetString($bytes3) - if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } - $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') - - # Insert attributes before
- $text3 = $text3 -replace '
', "${allAttrXml}`r`n`t`t
" - - $utf8Bom3 = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3) - Info " Merged $added attribute(s) into: $objFile" - } -} - -# --- 11h. Borrow-MainAttribute orchestrator --- -function Borrow-MainAttribute { - param([string]$typeName, [string]$objName, [string]$formName, [string]$mode) - - $dirName = $childTypeDirMap[$typeName] - Info "Borrowing main attribute for ${typeName}.${objName} (mode: $mode)..." - - # Step 1: Collect DataPaths (Form mode) or take all (All mode) - $firstLevelNames = $null - $deepPaths = @() - if ($mode -eq "Form") { - $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" - if (-not (Test-Path $srcFormXmlPath)) { - Write-Error "Source Form.xml not found: $srcFormXmlPath" - exit 1 - } - $dp = Collect-FormDataPaths $srcFormXmlPath - $firstLevelNames = $dp.FirstLevel - $deepPaths = $dp.DeepPaths - Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" - } else { - Info " Mode All: borrowing all attributes and tabular sections" - } - - # Step 2: Resolve source attributes - $resolved = Resolve-SourceAttributes $typeName $objName $firstLevelNames - $srcAttrs = $resolved.Attributes - $srcTS = $resolved.TabularSections - $extraProps = $resolved.ExtraProps - Info " Resolved: $($srcAttrs.Count) attributes, $($srcTS.Count) tabular section(s)" - - # Identify which FirstLevel names are TabularSections (for deep path filtering) - $tsNames = @{} - foreach ($ts in $srcTS) { $tsNames[$ts.Name] = $true } - - # Step 3: Build the adopted content and insert into main object XML - $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" - - # Read existing object XML (needed for dedup + enrichment) - $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) - - # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow) - $existingChildNames = @{} - if ($objContent -match '(?s)(.*?)') { - foreach ($nm in [regex]::Matches($Matches[1], '(\w+)')) { - $existingChildNames[$nm.Groups[1].Value] = $true - } - } - $insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) - $insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) - - # Generate full object XML with attributes and TS - $contentSb = New-Object System.Text.StringBuilder - foreach ($attr in $insertAttrs) { - $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" - $contentSb.AppendLine($attrXml) | Out-Null - } - foreach ($ts in $insertTS) { - $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" - $contentSb.AppendLine($tsXml) | Out-Null - } - $adoptedContent = $contentSb.ToString().TrimEnd() - - # Inject extra properties into the object's OWN Properties only — idempotent and anchored to the - # first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their - # own ExtendedConfigurationObject; a global replace would push object props inside every . - if ($extraProps.Count -gt 0) { - $objPropsBlock = "" - if ($objContent -match '(?s)(.*?)') { $objPropsBlock = $Matches[1] } - $propsSb = New-Object System.Text.StringBuilder - foreach ($pName in $extraProps.Keys) { - if ($objPropsBlock -match "<$pName>") { continue } - $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])") | Out-Null - } - if ($propsSb.Length -gt 0) { - $objContent = ([regex]'').Replace($objContent, "$($propsSb.ToString())", 1) - } - } - - # Replace empty ChildObjects with adopted content - if ($adoptedContent) { - # Handle (self-closing) - if ($objContent -match '') { - $objContent = $objContent -replace '', "`r`n${adoptedContent}`r`n`t`t" - } - # Handle ... (may already have Form entry) - elseif ($objContent -match '(?s)(.*?)') { - $existingInner = $Matches[1] - $objContent = $objContent -replace '(?s)(.*?)', "${existingInner}`r`n${adoptedContent}`r`n`t`t" - } - } - - $encBom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($objFile, $objContent, $encBom) - Info " Enriched object: $objFile" - - # Step 4: Collect all reference types and borrow as shells - $allTypeXmls = @() - foreach ($a in $srcAttrs) { $allTypeXmls += $a.TypeXml } - foreach ($ts in $srcTS) { - foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } - } - $refTypes = Collect-ReferenceTypes $allTypeXmls - Info " Reference types to borrow: $($refTypes.Count)" - - foreach ($rt in $refTypes) { - if (-not $childTypeDirMap.ContainsKey($rt.TypeName)) { - Warn " Unknown reference type: $($rt.TypeName).$($rt.ObjName)" - continue - } - if (Test-ObjectBorrowed $rt.TypeName $rt.ObjName) { - Info " Already borrowed: $($rt.TypeName).$($rt.ObjName)" - continue - } - $rtSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$rt.TypeName]) "$($rt.ObjName).xml" - if (-not (Test-Path $rtSrcFile)) { - Warn " Source not found: $($rt.TypeName).$($rt.ObjName)" - continue - } - $src = Read-SourceObject $rt.TypeName $rt.ObjName - $borrowedXml = Build-BorrowedObjectXml $rt.TypeName $rt.ObjName $src.Uuid $src.Properties - $targetDir = Join-Path $extDir $childTypeDirMap[$rt.TypeName] - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - $targetFile = Join-Path $targetDir "$($rt.ObjName).xml" - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) - Add-ToChildObjects $rt.TypeName $rt.ObjName - $script:borrowedFiles += $targetFile - Info " Auto-borrowed: $($rt.TypeName).$($rt.ObjName)" - } - - # Step 5: Handle deep paths (Form mode only) - if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { - # Top-level ref deep paths: Объект.. — borrow the ref attribute's catalog with the sub-attribute - $deepByAttr = @{} - foreach ($dp in $deepPaths) { - if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue } - if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() } - if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr } - } - if ($deepByAttr.Count -gt 0) { - Info " Processing $($deepByAttr.Count) deep path attribute(s)..." - foreach ($attrName in $deepByAttr.Keys) { - $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 - if (-not $attrInfo) { continue } - $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') - if (-not $catMatch.Success) { continue } - Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName] - } - } - - # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>. — borrow the column's catalog with the sub-attribute - $tsDeepByCol = @{} - foreach ($dp in $deepPaths) { - if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue } - if (-not $dp.SubSubAttr) { continue } - if ($script:standardFields -contains $dp.SubSubAttr) { continue } - $k = "$($dp.ObjectAttr)|$($dp.SubAttr)" - if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() } - if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr } - } - if ($tsDeepByCol.Count -gt 0) { - Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..." - foreach ($k in $tsDeepByCol.Keys) { - $parts = $k.Split("|") - $tsName = $parts[0]; $colName = $parts[1] - $tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1 - if (-not $tsInfo) { continue } - $colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1 - if (-not $colInfo) { continue } - $catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') - if (-not $catMatch.Success) { continue } - Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k] - } - } - } - - Info " Main attribute borrowing complete" -} - -# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes --- -# Used for both Объект.. (top-level ref attr) and Объект.<ТЧ>.<Колонка>. (tabular-section -# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows, -# otherwise the platform rejects the deep DataPath ("Неверный путь к данным"). -function Borrow-DeepTargetAttrs { - param([string]$targetTypeName, [string]$targetObjName, $subAttrNames) - - $encBomLocal = New-Object System.Text.UTF8Encoding($true) - - # Ensure target is borrowed (shell) - if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { - $tSrc = Read-SourceObject $targetTypeName $targetObjName - $tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties - $tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] - if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null } - $tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" - [System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal) - Add-ToChildObjects $targetTypeName $targetObjName - $script:borrowedFiles += $tTargetFile - Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" - } - - # Resolve sub-attributes in target catalog and merge them in - $subNames = @{} - foreach ($sn in $subAttrNames) { $subNames[$sn] = $true } - $subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames - if ($subResolved.Attributes.Count -gt 0) { - Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes - - # Borrow ref types referenced by the sub-attributes - $subTypeXmls = @() - foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml } - $subRefTypes = Collect-ReferenceTypes $subTypeXmls - foreach ($srt in $subRefTypes) { - if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue } - if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue } - $sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml" - if (-not (Test-Path $sSrcFile)) { continue } - $sSrc = Read-SourceObject $srt.TypeName $srt.ObjName - $sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties - $sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName] - if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null } - $sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml" - [System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal) - Add-ToChildObjects $srt.TypeName $srt.ObjName - $script:borrowedFiles += $sTargetFile - Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)" - } - } -} - -# --- 12. Helper: build borrowed object XML --- -function Build-BorrowedObjectXml { - param( - [string]$typeName, - [string]$objName, - [string]$sourceUuid, - [hashtable]$sourceProps - ) - - $newUuid = [guid]::NewGuid().ToString() - $internalInfoXml = Build-InternalInfoXml $typeName $objName "`t`t" - - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("") | Out-Null - $sb.AppendLine("") | Out-Null - $sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null - - # InternalInfo - $sb.AppendLine($internalInfoXml) | Out-Null - - # Properties - $sb.AppendLine("`t`t") | Out-Null - $sb.AppendLine("`t`t`tAdopted") | Out-Null - $sb.AppendLine("`t`t`t${objName}") | Out-Null - $sb.AppendLine("`t`t`t") | Out-Null - $sb.AppendLine("`t`t`t${sourceUuid}") | Out-Null - - # CommonModule: extra properties from source - if ($typeName -eq "CommonModule") { - foreach ($propName in $commonModuleProps) { - $propVal = "false" - if ($sourceProps.ContainsKey($propName)) { - $propVal = $sourceProps[$propName] - } - $sb.AppendLine("`t`t`t<${propName}>${propVal}") | Out-Null - } - } - - # DefinedType: emit the carried definition (needed for the alias to resolve, e.g. totals) - if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) { - $sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null - } - - $sb.AppendLine("`t`t") | Out-Null - - # ChildObjects (for types that need it) - if ($typesWithChildObjects -contains $typeName) { - $sb.AppendLine("`t`t") | Out-Null - } - - $sb.AppendLine("`t") | Out-Null - $sb.Append("") | Out-Null - - return $sb.ToString() -} - -# --- 13. Helper: add object to extension ChildObjects --- -function Add-ToChildObjects { - param([string]$typeName, [string]$objName) - - $cfgIndent = Get-ChildIndent $script:cfgEl - - # Expand self-closing ChildObjects if needed - if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { - Expand-SelfClosingElement $script:childObjsEl $cfgIndent - } - $childIndent = Get-ChildIndent $script:childObjsEl - - $typeIdx = $script:typeOrder.IndexOf($typeName) - if ($typeIdx -lt 0) { - Write-Error "Unknown type '$typeName' for ChildObjects ordering" - exit 1 - } - - # Dedup check - foreach ($child in $script:childObjsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objName) { - Warn "Already in ChildObjects: ${typeName}.${objName}" - return - } - } - - # Find insertion point: after last element of same type, or before first element of later type - $insertBefore = $null - $lastSameType = $null - - foreach ($child in $script:childObjsEl.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) - if ($childTypeIdx -lt 0) { continue } - - if ($child.LocalName -eq $typeName) { - # Same type -- check alphabetical order - if ($child.InnerText -gt $objName -and -not $insertBefore) { - $insertBefore = $child - } - $lastSameType = $child - } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { - # First element of a later type -- insert before it - $insertBefore = $child - } - } - - # Create element - $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) - $newEl.InnerText = $objName - - if ($insertBefore) { - Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent - } else { - Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent - } - - Info "Added to ChildObjects: ${typeName}.${objName}" -} - -# --- 14. Process each item --- -$script:borrowedFiles = @() -$borrowedCount = 0 - -foreach ($item in $items) { - $dotIdx = $item.IndexOf(".") - if ($dotIdx -lt 1) { - Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'" - exit 1 - } - $typeName = $item.Substring(0, $dotIdx) - $remainder = $item.Substring($dotIdx + 1) - - # Resolve Russian synonym to English type name - if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] } - - if (-not $childTypeDirMap.ContainsKey($typeName)) { - Write-Error "Unknown type '${typeName}'" - exit 1 - } - - # Check for .Form. pattern: Type.ObjName.Form.FormName - $formName = $null - $formIdx = $remainder.IndexOf(".Form.") - if ($formIdx -gt 0) { - $objName = $remainder.Substring(0, $formIdx) - $formName = $remainder.Substring($formIdx + 6) # skip ".Form." - } else { - $objName = $remainder - } - - $dirName = $childTypeDirMap[$typeName] - - if ($formName) { - # --- Form borrowing --- - Info "Borrowing form ${typeName}.${objName}.Form.${formName}..." - - # Auto-borrow parent object if not yet borrowed - if (-not (Test-ObjectBorrowed $typeName $objName)) { - Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..." - - $src = Read-SourceObject $typeName $objName - Info " Source UUID: $($src.Uuid)" - $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties - - $targetDir = Join-Path $extDir $dirName - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - $targetFile = Join-Path $targetDir "${objName}.xml" - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) - Info " Created: $targetFile" - - Add-ToChildObjects $typeName $objName - $script:borrowedFiles += $targetFile - } - - # Borrow the form - $hasBMA = [bool]$BorrowMainAttribute - $formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA - $script:borrowedFiles += $formFiles - $borrowedCount++ - - # Borrow main attribute if requested - if ($hasBMA) { - Borrow-MainAttribute $typeName $objName $formName $BorrowMainAttribute - } - } else { - # --- Object borrowing (existing logic) --- - Info "Borrowing ${typeName}.${objName}..." - - $src = Read-SourceObject $typeName $objName - Info " Source UUID: $($src.Uuid)" - - $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties - - $targetDir = Join-Path $extDir $dirName - if (-not (Test-Path $targetDir)) { - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - } - - $targetFile = Join-Path $targetDir "${objName}.xml" - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) - Info " Created: $targetFile" - - Add-ToChildObjects $typeName $objName - - $script:borrowedFiles += $targetFile - $borrowedCount++ - } -} - -# --- 15. Save modified Configuration.xml --- -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = New-Object System.Text.UTF8Encoding($true) -$settings.Indent = $false -$settings.NewLineHandling = [System.Xml.NewLineHandling]::None - -$memStream = New-Object System.IO.MemoryStream -$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) -$script:xmlDoc.Save($writer) -$writer.Flush(); $writer.Close() - -$bytes = $memStream.ToArray() -$memStream.Close() -$text = [System.Text.Encoding]::UTF8.GetString($bytes) -if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } -$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') - -$utf8Bom = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) -Info "Saved: $extResolvedPath" - -# --- 16. Summary --- -Write-Host "" -Write-Host "=== cfe-borrow summary ===" -Write-Host " Extension: $extDir" -Write-Host " Config: $cfgDir" -Write-Host " Borrowed: $borrowedCount object(s)" -foreach ($f in $script:borrowedFiles) { - Write-Host " - $f" -} -exit 0 +# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][string]$ExtensionPath, + [Parameter(Mandatory)][string]$ConfigPath, + [Parameter(Mandatory)][string]$Object, + [string]$BorrowMainAttribute +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# Form data-binding tags (value = attribute path). A binding survives only if its root +# attribute is borrowed into the form's ; otherwise it must be stripped or the +# platform rejects the form with "Неверный путь к данным" on load. +$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath') +# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. +$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath') + +# Strip data-binding tags whose root attribute isn't borrowed. +# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. +# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped. +function Strip-FormBindings { + param([string]$xml, [bool]$keepObjekt) + foreach ($tag in $script:formBindingDataTags) { + if ($keepObjekt) { + $xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*", '') + } else { + $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*", '') + } + } + foreach ($tag in $script:formBindingPictureTags) { + $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*", '') + } + return $xml +} + +# --- 1. Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { $ExtensionPath = $candidate } + else { Write-Error "No Configuration.xml in extension directory: $ExtensionPath"; exit 1 } +} +if (-not (Test-Path $ExtensionPath)) { Write-Error "Extension file not found: $ExtensionPath"; exit 1 } +$extResolvedPath = (Resolve-Path $ExtensionPath).Path +$extDir = Split-Path $extResolvedPath -Parent + +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } +$cfgResolvedPath = (Resolve-Path $ConfigPath).Path +$cfgDir = Split-Path $cfgResolvedPath -Parent + +# --- 2. Load extension Configuration.xml --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($extResolvedPath) + +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$root = $script:xmlDoc.DocumentElement + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found in extension"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +if (-not $script:propsEl) { Write-Error "No element found in extension"; exit 1 } +if (-not $script:childObjsEl) { Write-Error "No element found in extension"; exit 1 } + +# --- 3. Extract NamePrefix --- +$script:namePrefix = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "NamePrefix") { + $script:namePrefix = $child.InnerText.Trim(); break + } +} +Info "Extension NamePrefix: $($script:namePrefix)" + +# --- 4. Type mappings --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "XDTOPackage"="XDTOPackages"; "WebService"="WebServices" + "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "CommonAttribute"="CommonAttributes"; "Style"="Styles" +} + +# --- 4b. Russian synonym → English type --- +$synonymMap = @{ + "Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum" + "ОбщийМодуль"="CommonModule"; "ОбщаяКартинка"="CommonPicture" + "ОбщаяКоманда"="CommonCommand"; "ОбщийМакет"="CommonTemplate" + "ПланОбмена"="ExchangePlan"; "Отчет"="Report"; "Отчёт"="Report" + "Обработка"="DataProcessor"; "РегистрСведений"="InformationRegister" + "РегистрНакопления"="AccumulationRegister" + "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" + "ПланСчетов"="ChartOfAccounts"; "РегистрБухгалтерии"="AccountingRegister" + "ПланВидовРасчета"="ChartOfCalculationTypes"; "РегистрРасчета"="CalculationRegister" + "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" + "Подсистема"="Subsystem"; "Роль"="Role"; "Константа"="Constant" + "ФункциональнаяОпция"="FunctionalOption"; "ОпределяемыйТип"="DefinedType" + "ОбщаяФорма"="CommonForm"; "ЖурналДокументов"="DocumentJournal" + "ПараметрСеанса"="SessionParameter"; "ГруппаКоманд"="CommandGroup" + "ПодпискаНаСобытие"="EventSubscription"; "РегламентноеЗадание"="ScheduledJob" + "ОбщийРеквизит"="CommonAttribute"; "ПакетXDTO"="XDTOPackage" + "HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService" +} + +# --- 5. Canonical type order (44 types) --- +$script:typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# --- 6. GeneratedType patterns per type --- +$script:generatedTypes = @{ + "Catalog" = @( + @{ prefix = "CatalogObject"; category = "Object" } + @{ prefix = "CatalogRef"; category = "Ref" } + @{ prefix = "CatalogSelection"; category = "Selection" } + @{ prefix = "CatalogList"; category = "List" } + @{ prefix = "CatalogManager"; category = "Manager" } + ) + "Document" = @( + @{ prefix = "DocumentObject"; category = "Object" } + @{ prefix = "DocumentRef"; category = "Ref" } + @{ prefix = "DocumentSelection"; category = "Selection" } + @{ prefix = "DocumentList"; category = "List" } + @{ prefix = "DocumentManager"; category = "Manager" } + ) + "Enum" = @( + @{ prefix = "EnumRef"; category = "Ref" } + @{ prefix = "EnumManager"; category = "Manager" } + @{ prefix = "EnumList"; category = "List" } + ) + "Constant" = @( + @{ prefix = "ConstantManager"; category = "Manager" } + @{ prefix = "ConstantValueManager"; category = "ValueManager" } + @{ prefix = "ConstantValueKey"; category = "ValueKey" } + ) + "InformationRegister" = @( + @{ prefix = "InformationRegisterRecord"; category = "Record" } + @{ prefix = "InformationRegisterManager"; category = "Manager" } + @{ prefix = "InformationRegisterSelection"; category = "Selection" } + @{ prefix = "InformationRegisterList"; category = "List" } + @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } + ) + "AccumulationRegister" = @( + @{ prefix = "AccumulationRegisterRecord"; category = "Record" } + @{ prefix = "AccumulationRegisterManager"; category = "Manager" } + @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } + @{ prefix = "AccumulationRegisterList"; category = "List" } + @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } + ) + "AccountingRegister" = @( + @{ prefix = "AccountingRegisterRecord"; category = "Record" } + @{ prefix = "AccountingRegisterManager"; category = "Manager" } + @{ prefix = "AccountingRegisterSelection"; category = "Selection" } + @{ prefix = "AccountingRegisterList"; category = "List" } + @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } + ) + "CalculationRegister" = @( + @{ prefix = "CalculationRegisterRecord"; category = "Record" } + @{ prefix = "CalculationRegisterManager"; category = "Manager" } + @{ prefix = "CalculationRegisterSelection"; category = "Selection" } + @{ prefix = "CalculationRegisterList"; category = "List" } + @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } + ) + "ChartOfAccounts" = @( + @{ prefix = "ChartOfAccountsObject"; category = "Object" } + @{ prefix = "ChartOfAccountsRef"; category = "Ref" } + @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } + @{ prefix = "ChartOfAccountsList"; category = "List" } + @{ prefix = "ChartOfAccountsManager"; category = "Manager" } + ) + "ChartOfCharacteristicTypes" = @( + @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } + @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } + @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } + ) + "ChartOfCalculationTypes" = @( + @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } + @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCalculationTypesList"; category = "List" } + @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } + @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } + @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } + @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } + ) + "BusinessProcess" = @( + @{ prefix = "BusinessProcessObject"; category = "Object" } + @{ prefix = "BusinessProcessRef"; category = "Ref" } + @{ prefix = "BusinessProcessSelection"; category = "Selection" } + @{ prefix = "BusinessProcessList"; category = "List" } + @{ prefix = "BusinessProcessManager"; category = "Manager" } + ) + "Task" = @( + @{ prefix = "TaskObject"; category = "Object" } + @{ prefix = "TaskRef"; category = "Ref" } + @{ prefix = "TaskSelection"; category = "Selection" } + @{ prefix = "TaskList"; category = "List" } + @{ prefix = "TaskManager"; category = "Manager" } + ) + "ExchangePlan" = @( + @{ prefix = "ExchangePlanObject"; category = "Object" } + @{ prefix = "ExchangePlanRef"; category = "Ref" } + @{ prefix = "ExchangePlanSelection"; category = "Selection" } + @{ prefix = "ExchangePlanList"; category = "List" } + @{ prefix = "ExchangePlanManager"; category = "Manager" } + ) + "DocumentJournal" = @( + @{ prefix = "DocumentJournalSelection"; category = "Selection" } + @{ prefix = "DocumentJournalList"; category = "List" } + @{ prefix = "DocumentJournalManager"; category = "Manager" } + ) + "Report" = @( + @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportManager"; category = "Manager" } + ) + "DataProcessor" = @( + @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorManager"; category = "Manager" } + ) + "DefinedType" = @( + @{ prefix = "DefinedType"; category = "DefinedType" } + ) +} + +# Types that need ChildObjects element +$typesWithChildObjects = @( + "Catalog","Document","ExchangePlan","ChartOfAccounts", + "ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister" +) + +# CommonModule properties to copy from source +$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") + +# Standard system fields to skip when collecting DataPath references +$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") + +# --- 7. XML manipulation helpers (from cf-edit) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# --- 7b. Detect format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$script:formatVersion = Detect-FormatVersion $extDir + +# --- 8. Namespaces declaration for object XML --- +$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' + +# --- 9. Parse -Object into items --- +$items = @() +foreach ($part in $Object.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } +} + +if ($items.Count -eq 0) { + Write-Error "No objects specified in -Object" + exit 1 +} + +# --- 9b. Validate -BorrowMainAttribute --- +if ($BorrowMainAttribute) { + # PS treats -BorrowMainAttribute without value as "True" + if ($BorrowMainAttribute -eq "True") { $BorrowMainAttribute = "Form" } + if ($BorrowMainAttribute -notin @("Form","All")) { + Write-Error "-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)" + exit 1 + } + # Validate: only with .Form. pattern + $hasForm = $false + foreach ($item in $items) { if ($item -match '\.Form\.') { $hasForm = $true; break } } + if (-not $hasForm) { + Write-Error "-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')" + exit 1 + } +} + +# --- 10. Helper: read source object XML --- +function Read-SourceObject { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + if (-not $dirName) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $srcNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the type element (e.g. ) + $srcRoot = $srcDoc.DocumentElement + $srcEl = $null + foreach ($c in $srcRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract uuid + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source element in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract properties for CommonModule + $srcProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + foreach ($propName in $commonModuleProps) { + $propNode = $propsNode.SelectSingleNode("md:${propName}", $srcNs) + if ($propNode) { + $srcProps[$propName] = $propNode.InnerText.Trim() + } + } + # DefinedType: carry the definition. A type alias is meaningless as a bare shell — + # the platform needs its underlying type (e.g. to know a column is a summable Number for totals). + if ($typeName -eq "DefinedType") { + $typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs) + if ($typeNode) { + $srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + } + } + } + + return @{ + Uuid = $srcUuid + Properties = $srcProps + Element = $srcEl + NsManager = $srcNs + } +} + +# --- 10b. Helper: read source form UUID --- +function Read-SourceFormUuid { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source form not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in source form: $srcFile" + exit 1 + } + + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source form element: $srcFile" + exit 1 + } + + return $srcUuid +} + +# --- 10c. Helper: borrow a form --- +function Borrow-Form { + param([string]$typeName, [string]$objName, [string]$formName, [switch]$BorrowMainAttr) + + $dirName = $childTypeDirMap[$typeName] + $enc = New-Object System.Text.UTF8Encoding($true) + + # 1. Read source form UUID + $formUuid = Read-SourceFormUuid $typeName $objName $formName + Info " Source form UUID: $formUuid" + + # 2. Read source Form.xml content + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) + + # 3. Generate form metadata XML (ФормаЭлемента.xml). + # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent + # (regenerating it would churn the form's identity on every rerun). + $formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml" + $newFormUuid = "" + if (Test-Path $formMetaFileExisting) { + try { + $existingDoc = New-Object System.Xml.XmlDocument + $existingDoc.Load($formMetaFileExisting) + $existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']") + if ($existingFormNode) { + $existingUuid = $existingFormNode.GetAttribute("uuid") + if ($existingUuid) { $newFormUuid = $existingUuid } + } + } catch { } + } + if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() } + $formMetaSb = New-Object System.Text.StringBuilder + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("`t
") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`tAdopted") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formName}") | Out-Null + $formMetaSb.AppendLine("`t`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formUuid}") | Out-Null + $formMetaSb.AppendLine("`t`t`tManaged") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t") | Out-Null + $formMetaSb.Append("
") | Out-Null + + # 4. Create directories + $formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms" + if (-not (Test-Path $formMetaDir)) { + New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null + } + + # Write form metadata + $formMetaFile = Join-Path $formMetaDir "${formName}.xml" + [System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc) + Info " Created: $formMetaFile" + + # 5. Generate Form.xml with BaseForm (visual elements only) + # Parse source Form.xml as XmlDocument + $srcFormDoc = New-Object System.Xml.XmlDocument + $srcFormDoc.PreserveWhitespace = $true + $srcFormDoc.Load($srcFormXmlPath) + $srcFormEl = $srcFormDoc.DocumentElement + + # Borrowed form must use the extension's format version (not the source form's), so the whole + # extension stays uniform — otherwise the platform rejects the import on a version mismatch + # (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version. + $formVersion = $script:formatVersion + + # Find direct children: form properties, AutoCommandBar, ChildItems + $srcAutoCmd = $null + $srcChildItems = $null + $formProps = @() + $reachedVisual = $false + foreach ($fc in $srcFormEl.ChildNodes) { + if ($fc.NodeType -ne 'Element') { continue } + if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { + $reachedVisual = $true; $srcAutoCmd = $fc; continue + } + if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { + $reachedVisual = $true; $srcChildItems = $fc; continue + } + if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') { + $reachedVisual = $true; continue + } + if (-not $reachedVisual) { + $formProps += $fc.OuterXml + } + } + + # Get OuterXml and strip redundant namespace redeclarations (they're on root
) + $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false + $autoCmdXml = "" + if ($srcAutoCmd) { + $autoCmdXml = $srcAutoCmd.OuterXml + $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') + $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') + $autoCmdXml = $autoCmdXml -replace 'true', 'false' + # Strip ExcludedCommand (references to standard commands invalid in extension) + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + # Strip data-binding tags whose root attribute isn't borrowed + $autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr) + } + + # ChildItems: copy full tree, clean up base-config references + $childItemsXml = "" + if ($srcChildItems) { + $childItemsXml = $srcChildItems.OuterXml + $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') + # Replace all CommandName values with 0 + $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') + # Strip data-binding tags whose root attribute isn't borrowed + # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*) + $childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr) + # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*Items\.[^<]*.*?', '') + # Strip element-level Events (base form handlers not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') + + # Collect CommonPicture references from ChildItems and AutoCommandBar + $referencedPictures = @{} + $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') + foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } + if ($autoCmdXml) { + $picRefs2 = [regex]::Matches($autoCmdXml, 'CommonPicture\.(\w+)') + foreach ($m in $picRefs2) { $referencedPictures[$m.Groups[1].Value] = $true } + } + + # Auto-borrow referenced CommonPictures (if not already borrowed) + $autoBorrowedPics = @() + foreach ($picName in $referencedPictures.Keys) { + if (-not (Test-ObjectBorrowed "CommonPicture" $picName)) { + $picSrcFile = Join-Path (Join-Path $cfgDir "CommonPictures") "${picName}.xml" + if (Test-Path $picSrcFile) { + $src = Read-SourceObject "CommonPicture" $picName + $borrowedXml = Build-BorrowedObjectXml "CommonPicture" $picName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "CommonPictures" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${picName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "CommonPicture" $picName + $autoBorrowedPics += $picName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: CommonPicture.${picName}" + } else { + Warn " CommonPicture.${picName} not found in source config — will strip from form" + } + } + } + + # Collect all borrowed CommonPictures (including previously borrowed) + $borrowedPicSet = @{} + $nsMgr2 = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable) + $nsMgr2.AddNamespace("md", $script:mdNs) + $picNodes = $script:xmlDoc.SelectNodes("//md:ChildObjects/md:CommonPicture", $nsMgr2) + foreach ($pn in $picNodes) { $borrowedPicSet[$pn.InnerText] = $true } + + # Strip blocks referencing non-borrowed CommonPictures + $picBlockPattern = '(?s)\s*\s*CommonPicture\.(\w+).*?' + $picMatches = [regex]::Matches($childItemsXml, $picBlockPattern) + # Process in reverse order to preserve positions + for ($mi = $picMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $picMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $childItemsXml = $childItemsXml.Remove($pm.Index, $pm.Length) + } + } + # Strip StdPicture blocks (except Print) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + + # Same Picture strip for AutoCommandBar + if ($autoCmdXml) { + $acPicMatches = [regex]::Matches($autoCmdXml, $picBlockPattern) + for ($mi = $acPicMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $acPicMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $autoCmdXml = $autoCmdXml.Remove($pm.Index, $pm.Length) + } + } + $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + } + + # Auto-borrow StyleItems referenced in ChildItems + # Pattern 1: , + # Pattern 2: style:XXX, style:XXX, etc. + $referencedStyles = @{} + $styleRefs1 = [regex]::Matches($childItemsXml, 'ref="style:(\w+)"[^>]*kind="StyleItem"') + foreach ($m in $styleRefs1) { $referencedStyles[$m.Groups[1].Value] = $true } + $styleRefs2 = [regex]::Matches($childItemsXml, '>style:(\w+)') + foreach ($m in $styleRefs2) { $referencedStyles[$m.Groups[1].Value] = $true } + + foreach ($styleName in $referencedStyles.Keys) { + if (-not (Test-ObjectBorrowed "StyleItem" $styleName)) { + $styleSrcFile = Join-Path (Join-Path $cfgDir "StyleItems") "${styleName}.xml" + if (Test-Path $styleSrcFile) { + $src = Read-SourceObject "StyleItem" $styleName + $borrowedXml = Build-BorrowedObjectXml "StyleItem" $styleName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "StyleItems" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${styleName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "StyleItem" $styleName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: StyleItem.${styleName}" + } else { + Warn " StyleItem.${styleName} not found in source config" + } + } + } + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef in ChoiceParameters + # Collect Enum -> [EnumValue names] map + $dtRefs = [regex]::Matches($childItemsXml, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)') + $referencedEnumValues = @{} + foreach ($m in $dtRefs) { + $eName = $m.Groups[1].Value + $evName = $m.Groups[2].Value + if (-not $referencedEnumValues.ContainsKey($eName)) { $referencedEnumValues[$eName] = @{} } + $referencedEnumValues[$eName][$evName] = $true + } + + foreach ($enumName in $referencedEnumValues.Keys) { + if (-not (Test-ObjectBorrowed "Enum" $enumName)) { + $enumSrcFile = Join-Path (Join-Path $cfgDir "Enums") "${enumName}.xml" + if (Test-Path $enumSrcFile) { + # Read source Enum to get UUID and EnumValue UUIDs + $srcParser = New-Object System.Xml.XmlDocument + $srcParser.PreserveWhitespace = $true + $srcParser.Load($enumSrcFile) + $srcEnumEl = $null + foreach ($cn in $srcParser.DocumentElement.ChildNodes) { + if ($cn.NodeType -eq 'Element') { $srcEnumEl = $cn; break } + } + $srcEnumUuid = $srcEnumEl.GetAttribute("uuid") + + # Find source EnumValues by name + $enumValueXmls = @() + $neededValues = $referencedEnumValues[$enumName] + $srcNsMgr = New-Object System.Xml.XmlNamespaceManager($srcParser.NameTable) + $srcNsMgr.AddNamespace("md", $script:mdNs) + $srcEvNodes = $srcEnumEl.SelectNodes("md:ChildObjects/md:EnumValue", $srcNsMgr) + foreach ($evNode in $srcEvNodes) { + $evUuid = $evNode.GetAttribute("uuid") + $evNameNode = $evNode.SelectSingleNode("md:Properties/md:Name", $srcNsMgr) + if ($evNameNode -and $neededValues.ContainsKey($evNameNode.InnerText)) { + $newEvUuid = [guid]::NewGuid().ToString() + $enumValueXmls += @" + + + + Adopted + $($evNameNode.InnerText) + + ${evUuid} + + +"@ + } + } + + # Build borrowed Enum with EnumValues in ChildObjects + $src = Read-SourceObject "Enum" $enumName + $borrowedXml = Build-BorrowedObjectXml "Enum" $enumName $src.Uuid $src.Properties + if ($enumValueXmls.Count -gt 0) { + $evBlock = ($enumValueXmls -join "`r`n") + $borrowedXml = $borrowedXml -replace '', "`r`n${evBlock}`r`n`t`t" + } + + $targetDir = Join-Path $extDir "Enums" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${enumName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "Enum" $enumName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" + } else { + Warn " Enum.${enumName} not found in source config" + } + } + } + } + + # Extract the opening tag from source text (preserves namespace declarations) + $xmlDecl = '' + $formTag = "" + if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] } + if ($srcFormContent -match '(]*>)') { $formTag = $Matches[1] } + + # Build output Form.xml + $formXmlSb = New-Object System.Text.StringBuilder + $formXmlSb.Append($xmlDecl) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append($formTag) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + # Part 1: form properties + AutoCommandBar + ChildItems + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t$propXml`r`n") | Out-Null + } + if ($autoCmdXml) { + $formXmlSb.Append("`t$autoCmdXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } + if ($childItemsXml) { + $formXmlSb.Append("`t$childItemsXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } + # Attributes: empty or with MainAttribute when BorrowMainAttr + if ($BorrowMainAttr) { + $objTypePrefix = "" + $gtList = $script:generatedTypes[$typeName] + if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } } + $mainAttrType = "cfg:${objTypePrefix}.${objName}" + $formXmlSb.Append("`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + } else { + $formXmlSb.Append("`t") | Out-Null + } + $formXmlSb.Append("`r`n") | Out-Null + + # BaseForm: same content, indented one more level + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null + } + if ($autoCmdXml) { + $acLines = $autoCmdXml -split "`r?`n" + for ($li = 0; $li -lt $acLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } + if ($childItemsXml) { + # Reindent ChildItems for BaseForm (+1 tab level) + $ciLines = $childItemsXml -split "`r?`n" + for ($li = 0; $li -lt $ciLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } + + # BaseForm Attributes: same as main section + if ($BorrowMainAttr) { + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t") | Out-Null + } else { + $formXmlSb.Append("`t`t") | Out-Null + } + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("") | Out-Null + + # Write Form.xml + $formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext" + if (-not (Test-Path $formXmlDir)) { + New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null + } + $formXmlFile = Join-Path $formXmlDir "Form.xml" + [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) + Info " Created: $formXmlFile" + + # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must + # not clobber user code added to the form module). + $moduleDir = Join-Path $formXmlDir "Form" + if (-not (Test-Path $moduleDir)) { + New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null + } + $moduleBslFile = Join-Path $moduleDir "Module.bsl" + if (Test-Path $moduleBslFile) { + Info " Preserved existing Module.bsl" + } else { + [System.IO.File]::WriteAllText($moduleBslFile, "", $enc) + Info " Created: $moduleBslFile" + } + + # 7. Register form in parent object ChildObjects + Register-FormInObject $typeName $objName $formName + + return @($formMetaFile, $formXmlFile, $moduleBslFile) +} + +# --- 10d. Helper: register form in parent object's ChildObjects --- +function Register-FormInObject { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { + Warn "Parent object file not found: $objFile — form not registered in ChildObjects" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + # Find the type element + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { + Warn "No type element in $objFile — form not registered" + return + } + + # Find or create ChildObjects + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + # Create ChildObjects element + $childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses") + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Check dedup + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) { + Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}" + return + } + } + + # Expand self-closing if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + # Add
formName
+ $formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formEl.InnerText = $formName + + $trailing = $childObjs.LastChild + $ws = $objDoc.CreateWhitespace("`r`n`t`t`t") + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $childObjs.InsertBefore($ws, $trailing) | Out-Null + $childObjs.InsertBefore($formEl, $trailing) | Out-Null + } else { + $childObjs.AppendChild($ws) | Out-Null + $childObjs.AppendChild($formEl) | Out-Null + } + + # Save object XML + $settings2 = New-Object System.Xml.XmlWriterSettings + $settings2.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings2.Indent = $false + $settings2.NewLineHandling = [System.Xml.NewLineHandling]::None + + $memStream2 = New-Object System.IO.MemoryStream + $writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2) + $objDoc.Save($writer2) + $writer2.Flush(); $writer2.Close() + + $bytes2 = $memStream2.ToArray() + $memStream2.Close() + $text2 = [System.Text.Encoding]::UTF8.GetString($bytes2) + if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) } + $text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + $utf8Bom2 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2) + Info " Registered form in: $objFile" +} + +# --- 10e. Helper: check if object is already borrowed in extension --- +function Test-ObjectBorrowed { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + return (Test-Path $objFile) +} + +# --- 11. Helper: generate InternalInfo XML --- +function Build-InternalInfoXml { + param([string]$typeName, [string]$objName, [string]$indent) + + $types = $script:generatedTypes[$typeName] + if (-not $types -or $types.Count -eq 0) { + return "${indent}" + } + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # ExchangePlan: ThisNode UUID before GeneratedTypes + if ($typeName -eq "ExchangePlan") { + $thisNodeUuid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t${thisNodeUuid}") | Out-Null + } + + foreach ($gt in $types) { + $fullName = "$($gt.prefix).${objName}" + $typeId = [guid]::NewGuid().ToString() + $valueId = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`t${typeId}") | Out-Null + $sb.AppendLine("${indent}`t`t${valueId}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11b. Collect DataPath references from source Form.xml --- +function Collect-FormDataPaths { + param([string]$formXmlPath) + + $enc = New-Object System.Text.UTF8Encoding($true) + $content = [System.IO.File]::ReadAllText($formXmlPath, $enc) + + $firstLevel = @{} + $deepPaths = @() + + # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) + # for Объект.* references — picture-path tags carry picture indices, not data attributes. + foreach ($tag in $script:formBindingDataTags) { + $bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)") + foreach ($m in $bms) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + if ($segments.Count -ge 2) { + $seg1 = $segments[1] + if ($script:standardFields -contains $seg1) { continue } + $seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null } + $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 } + } + } + } + + # Also scan Объект.X — object attributes referenced by filter/conditional-appearance + # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. + $fieldMatches = [regex]::Matches($content, "[^<]*\bОбъект\.(\w+(?:\.\w+)*)") + foreach ($m in $fieldMatches) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + if ($segments.Count -ge 2) { + $seg1 = $segments[1] + if ($script:standardFields -contains $seg1) { continue } + $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1 } + } + } + + # Deduplicate deep paths + $seen = @{} + $uniqueDeep = @() + foreach ($dp in $deepPaths) { + $key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)" + if (-not $seen.ContainsKey($key)) { + $seen[$key] = $true + $uniqueDeep += $dp + } + } + + return @{ FirstLevel = $firstLevel; DeepPaths = $uniqueDeep } +} + +# --- 11c. Resolve source attributes and tabular sections --- +function Resolve-SourceAttributes { + param([string]$typeName, [string]$objName, $firstLevelNames) + # $firstLevelNames: hashtable of names, or $null for "all" + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", $script:mdNs) + $srcNs.AddNamespace("xr", $script:xrNs) + $srcNs.AddNamespace("v8", $script:v8Ns) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { Write-Error "No metadata element in source: $srcFile"; exit 1 } + + $childObjs = $srcEl.SelectSingleNode("md:ChildObjects", $srcNs) + if (-not $childObjs) { return @{ Attributes = @(); TabularSections = @(); ExtraProps = @{} } } + + $attrs = @() + $tabSections = @() + + foreach ($child in $childObjs.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + + if ($child.LocalName -eq 'Attribute') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $attrName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($attrName)) { continue } + + $uuid = $child.GetAttribute("uuid") + $typeNode = $child.SelectSingleNode("md:Properties/md:Type", $srcNs) + $typeXml = if ($typeNode) { $typeNode.OuterXml } else { "" } + # Strip namespace declarations from Type + $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + + $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } + } + elseif ($child.LocalName -eq 'TabularSection') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $tsName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($tsName)) { continue } + + $tsUuid = $child.GetAttribute("uuid") + + # Extract GeneratedTypes from InternalInfo + $tsGenTypes = @() + $iiNode = $child.SelectSingleNode("md:InternalInfo", $srcNs) + if ($iiNode) { + $gtNodes = $iiNode.SelectNodes("xr:GeneratedType", $srcNs) + foreach ($gt in $gtNodes) { + $tsGenTypes += @{ + Name = $gt.GetAttribute("name") + Category = $gt.GetAttribute("category") + TypeId = $gt.SelectSingleNode("xr:TypeId", $srcNs).InnerText + ValueId = $gt.SelectSingleNode("xr:ValueId", $srcNs).InnerText + } + } + } + + # Extract ALL child attributes of TabularSection + $tsAttrs = @() + $tsChildObjs = $child.SelectSingleNode("md:ChildObjects", $srcNs) + if ($tsChildObjs) { + foreach ($tsChild in $tsChildObjs.ChildNodes) { + if ($tsChild.NodeType -ne 'Element' -or $tsChild.LocalName -ne 'Attribute') { continue } + $tsAttrName = $tsChild.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $tsAttrName) { continue } + $tsAttrUuid = $tsChild.GetAttribute("uuid") + $tsTypeNode = $tsChild.SelectSingleNode("md:Properties/md:Type", $srcNs) + $tsTypeXml = if ($tsTypeNode) { $tsTypeNode.OuterXml } else { "" } + $tsTypeXml = [regex]::Replace($tsTypeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + $tsAttrs += @{ Name = $tsAttrName.InnerText; Uuid = $tsAttrUuid; TypeXml = $tsTypeXml } + } + } + + $tabSections += @{ Name = $tsName; Uuid = $tsUuid; GeneratedTypes = $tsGenTypes; Attributes = $tsAttrs } + } + } + + # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) + # Ordered so PS emits the same property order as the Python port (dict preserves insertion order). + $extraProps = [ordered]@{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", + "NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") + foreach ($pName in $propsToExtract) { + $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) + if ($pNode) { $extraProps[$pName] = $pNode.InnerText } + } + } + + return @{ Attributes = $attrs; TabularSections = $tabSections; ExtraProps = $extraProps } +} + +# --- 11d. Build adopted attribute XML --- +function Build-AdoptedAttributeXml { + param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${name}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11e. Build adopted tabular section XML --- +function Build-AdoptedTabularSectionXml { + param([string]$tsName, [string]$sourceUuid, $generatedTypes, $childAttrs, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) + if ($generatedTypes -and $generatedTypes.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($gt in $generatedTypes) { + $newTid = [guid]::NewGuid().ToString() + $newVid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newTid}") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newVid}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${tsName}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + + # ChildObjects with all attributes + if ($childAttrs -and $childAttrs.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($ca in $childAttrs) { + $caXml = Build-AdoptedAttributeXml $ca.Name $ca.Uuid $ca.TypeXml "${indent}`t`t" + $sb.AppendLine($caXml) | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11f. Collect reference types from attribute Type XML strings --- +function Collect-ReferenceTypes { + param([string[]]$typeXmls) + + $result = @{} + foreach ($typeXml in $typeXmls) { + # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. + $refMatches = [regex]::Matches($typeXml, 'cfg:(\w+)Ref\.(\w+)') + foreach ($m in $refMatches) { + $refPrefix = $m.Groups[1].Value # e.g. "Catalog", "Enum", "Document" + $objName = $m.Groups[2].Value + $key = "${refPrefix}.${objName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = $refPrefix; ObjName = $objName } + } + } + # cfg:DefinedType.XXX (via v8:TypeSet or v8:Type) + $dtMatches = [regex]::Matches($typeXml, 'cfg:DefinedType\.(\w+)') + foreach ($m in $dtMatches) { + $dtName = $m.Groups[1].Value + $key = "DefinedType.${dtName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = "DefinedType"; ObjName = $dtName } + } + } + } + return @($result.Values) +} + +# --- 11g. Merge adopted attributes into existing extension object XML --- +function Merge-AttributesIntoObject { + param([string]$typeName, [string]$objName, $attrsToAdd) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + if (-not (Test-Path $objFile)) { + Warn "Cannot merge attributes: $objFile not found" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", $script:mdNs) + + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { Warn "No type element in $objFile"; return } + + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + $childObjs = $objDoc.CreateElement("ChildObjects", $script:mdNs) + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Collect existing attribute names for dedup + $existingNames = @{} + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -ne 'Element' -or $c.LocalName -ne 'Attribute') { continue } + $nameNode = $c.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($nameNode) { $existingNames[$nameNode.InnerText] = $true } + } + + $added = 0 + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + + # Expand self-closing ChildObjects if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + $added++ + } + + if ($added -gt 0) { + # Build all adopted attributes as text and do string-level insertion + $allAttrXml = "" + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") + } + + # Save via text manipulation to avoid namespace issues with InnerXml + $settings3 = New-Object System.Xml.XmlWriterSettings + $settings3.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings3.Indent = $false + $settings3.NewLineHandling = [System.Xml.NewLineHandling]::None + $memStream3 = New-Object System.IO.MemoryStream + $writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3) + $objDoc.Save($writer3) + $writer3.Flush(); $writer3.Close() + $bytes3 = $memStream3.ToArray() + $memStream3.Close() + $text3 = [System.Text.Encoding]::UTF8.GetString($bytes3) + if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } + $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + # Insert attributes before
+ $text3 = $text3 -replace '
', "${allAttrXml}`r`n`t`t
" + + $utf8Bom3 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3) + Info " Merged $added attribute(s) into: $objFile" + } +} + +# --- 11h. Borrow-MainAttribute orchestrator --- +function Borrow-MainAttribute { + param([string]$typeName, [string]$objName, [string]$formName, [string]$mode) + + $dirName = $childTypeDirMap[$typeName] + Info "Borrowing main attribute for ${typeName}.${objName} (mode: $mode)..." + + # Step 1: Collect DataPaths (Form mode) or take all (All mode) + $firstLevelNames = $null + $deepPaths = @() + if ($mode -eq "Form") { + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $dp = Collect-FormDataPaths $srcFormXmlPath + $firstLevelNames = $dp.FirstLevel + $deepPaths = $dp.DeepPaths + Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" + } else { + Info " Mode All: borrowing all attributes and tabular sections" + } + + # Step 2: Resolve source attributes + $resolved = Resolve-SourceAttributes $typeName $objName $firstLevelNames + $srcAttrs = $resolved.Attributes + $srcTS = $resolved.TabularSections + $extraProps = $resolved.ExtraProps + Info " Resolved: $($srcAttrs.Count) attributes, $($srcTS.Count) tabular section(s)" + + # Identify which FirstLevel names are TabularSections (for deep path filtering) + $tsNames = @{} + foreach ($ts in $srcTS) { $tsNames[$ts.Name] = $true } + + # Step 3: Build the adopted content and insert into main object XML + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + # Read existing object XML (needed for dedup + enrichment) + $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) + + # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow) + $existingChildNames = @{} + if ($objContent -match '(?s)(.*?)') { + foreach ($nm in [regex]::Matches($Matches[1], '(\w+)')) { + $existingChildNames[$nm.Groups[1].Value] = $true + } + } + $insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) + $insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) + + # Generate full object XML with attributes and TS + $contentSb = New-Object System.Text.StringBuilder + foreach ($attr in $insertAttrs) { + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + $contentSb.AppendLine($attrXml) | Out-Null + } + foreach ($ts in $insertTS) { + $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" + $contentSb.AppendLine($tsXml) | Out-Null + } + $adoptedContent = $contentSb.ToString().TrimEnd() + + # Inject extra properties into the object's OWN Properties only — idempotent and anchored to the + # first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their + # own ExtendedConfigurationObject; a global replace would push object props inside every . + if ($extraProps.Count -gt 0) { + $objPropsBlock = "" + if ($objContent -match '(?s)(.*?)') { $objPropsBlock = $Matches[1] } + $propsSb = New-Object System.Text.StringBuilder + foreach ($pName in $extraProps.Keys) { + if ($objPropsBlock -match "<$pName>") { continue } + $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])") | Out-Null + } + if ($propsSb.Length -gt 0) { + $objContent = ([regex]'').Replace($objContent, "$($propsSb.ToString())", 1) + } + } + + # Replace empty ChildObjects with adopted content + if ($adoptedContent) { + # Handle (self-closing) + if ($objContent -match '') { + $objContent = $objContent -replace '', "`r`n${adoptedContent}`r`n`t`t" + } + # Handle ... (may already have Form entry) + elseif ($objContent -match '(?s)(.*?)') { + $existingInner = $Matches[1] + $objContent = $objContent -replace '(?s)(.*?)', "${existingInner}`r`n${adoptedContent}`r`n`t`t" + } + } + + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $objContent, $encBom) + Info " Enriched object: $objFile" + + # Step 4: Collect all reference types and borrow as shells + $allTypeXmls = @() + foreach ($a in $srcAttrs) { $allTypeXmls += $a.TypeXml } + foreach ($ts in $srcTS) { + foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } + } + $refTypes = Collect-ReferenceTypes $allTypeXmls + Info " Reference types to borrow: $($refTypes.Count)" + + foreach ($rt in $refTypes) { + if (-not $childTypeDirMap.ContainsKey($rt.TypeName)) { + Warn " Unknown reference type: $($rt.TypeName).$($rt.ObjName)" + continue + } + if (Test-ObjectBorrowed $rt.TypeName $rt.ObjName) { + Info " Already borrowed: $($rt.TypeName).$($rt.ObjName)" + continue + } + $rtSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$rt.TypeName]) "$($rt.ObjName).xml" + if (-not (Test-Path $rtSrcFile)) { + Warn " Source not found: $($rt.TypeName).$($rt.ObjName)" + continue + } + $src = Read-SourceObject $rt.TypeName $rt.ObjName + $borrowedXml = Build-BorrowedObjectXml $rt.TypeName $rt.ObjName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir $childTypeDirMap[$rt.TypeName] + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "$($rt.ObjName).xml" + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects $rt.TypeName $rt.ObjName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: $($rt.TypeName).$($rt.ObjName)" + } + + # Step 5: Handle deep paths (Form mode only) + if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { + # Top-level ref deep paths: Объект.. — borrow the ref attribute's catalog with the sub-attribute + $deepByAttr = @{} + foreach ($dp in $deepPaths) { + if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue } + if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() } + if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr } + } + if ($deepByAttr.Count -gt 0) { + Info " Processing $($deepByAttr.Count) deep path attribute(s)..." + foreach ($attrName in $deepByAttr.Keys) { + $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 + if (-not $attrInfo) { continue } + $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') + if (-not $catMatch.Success) { continue } + Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName] + } + } + + # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>. — borrow the column's catalog with the sub-attribute + $tsDeepByCol = @{} + foreach ($dp in $deepPaths) { + if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue } + if (-not $dp.SubSubAttr) { continue } + if ($script:standardFields -contains $dp.SubSubAttr) { continue } + $k = "$($dp.ObjectAttr)|$($dp.SubAttr)" + if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() } + if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr } + } + if ($tsDeepByCol.Count -gt 0) { + Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..." + foreach ($k in $tsDeepByCol.Keys) { + $parts = $k.Split("|") + $tsName = $parts[0]; $colName = $parts[1] + $tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1 + if (-not $tsInfo) { continue } + $colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1 + if (-not $colInfo) { continue } + $catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') + if (-not $catMatch.Success) { continue } + Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k] + } + } + } + + Info " Main attribute borrowing complete" +} + +# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes --- +# Used for both Объект.. (top-level ref attr) and Объект.<ТЧ>.<Колонка>. (tabular-section +# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows, +# otherwise the platform rejects the deep DataPath ("Неверный путь к данным"). +function Borrow-DeepTargetAttrs { + param([string]$targetTypeName, [string]$targetObjName, $subAttrNames) + + $encBomLocal = New-Object System.Text.UTF8Encoding($true) + + # Ensure target is borrowed (shell) + if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { + $tSrc = Read-SourceObject $targetTypeName $targetObjName + $tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties + $tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] + if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null } + $tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" + [System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal) + Add-ToChildObjects $targetTypeName $targetObjName + $script:borrowedFiles += $tTargetFile + Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" + } + + # Resolve sub-attributes in target catalog and merge them in + $subNames = @{} + foreach ($sn in $subAttrNames) { $subNames[$sn] = $true } + $subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames + if ($subResolved.Attributes.Count -gt 0) { + Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes + + # Borrow ref types referenced by the sub-attributes + $subTypeXmls = @() + foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml } + $subRefTypes = Collect-ReferenceTypes $subTypeXmls + foreach ($srt in $subRefTypes) { + if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue } + if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue } + $sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml" + if (-not (Test-Path $sSrcFile)) { continue } + $sSrc = Read-SourceObject $srt.TypeName $srt.ObjName + $sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties + $sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName] + if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null } + $sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml" + [System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal) + Add-ToChildObjects $srt.TypeName $srt.ObjName + $script:borrowedFiles += $sTargetFile + Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)" + } + } +} + +# --- 12. Helper: build borrowed object XML --- +function Build-BorrowedObjectXml { + param( + [string]$typeName, + [string]$objName, + [string]$sourceUuid, + [hashtable]$sourceProps + ) + + $newUuid = [guid]::NewGuid().ToString() + $internalInfoXml = Build-InternalInfoXml $typeName $objName "`t`t" + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("") | Out-Null + $sb.AppendLine("") | Out-Null + $sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null + + # InternalInfo + $sb.AppendLine($internalInfoXml) | Out-Null + + # Properties + $sb.AppendLine("`t`t") | Out-Null + $sb.AppendLine("`t`t`tAdopted") | Out-Null + $sb.AppendLine("`t`t`t${objName}") | Out-Null + $sb.AppendLine("`t`t`t") | Out-Null + $sb.AppendLine("`t`t`t${sourceUuid}") | Out-Null + + # CommonModule: extra properties from source + if ($typeName -eq "CommonModule") { + foreach ($propName in $commonModuleProps) { + $propVal = "false" + if ($sourceProps.ContainsKey($propName)) { + $propVal = $sourceProps[$propName] + } + $sb.AppendLine("`t`t`t<${propName}>${propVal}") | Out-Null + } + } + + # DefinedType: emit the carried definition (needed for the alias to resolve, e.g. totals) + if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) { + $sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null + } + + $sb.AppendLine("`t`t") | Out-Null + + # ChildObjects (for types that need it) + if ($typesWithChildObjects -contains $typeName) { + $sb.AppendLine("`t`t") | Out-Null + } + + $sb.AppendLine("`t") | Out-Null + $sb.Append("") | Out-Null + + return $sb.ToString() +} + +# --- 13. Helper: add object to extension ChildObjects --- +function Add-ToChildObjects { + param([string]$typeName, [string]$objName) + + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing ChildObjects if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName' for ChildObjects ordering" + exit 1 + } + + # Dedup check + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objName) { + Warn "Already in ChildObjects: ${typeName}.${objName}" + return + } + } + + # Find insertion point: after last element of same type, or before first element of later type + $insertBefore = $null + $lastSameType = $null + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type -- check alphabetical order + if ($child.InnerText -gt $objName -and -not $insertBefore) { + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type -- insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objName + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + Info "Added to ChildObjects: ${typeName}.${objName}" +} + +# --- 14. Process each item --- +$script:borrowedFiles = @() +$borrowedCount = 0 + +foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $remainder = $item.Substring($dotIdx + 1) + + # Resolve Russian synonym to English type name + if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { + Write-Error "Unknown type '${typeName}'" + exit 1 + } + + # Check for .Form. pattern: Type.ObjName.Form.FormName + $formName = $null + $formIdx = $remainder.IndexOf(".Form.") + if ($formIdx -gt 0) { + $objName = $remainder.Substring(0, $formIdx) + $formName = $remainder.Substring($formIdx + 6) # skip ".Form." + } else { + $objName = $remainder + } + + $dirName = $childTypeDirMap[$typeName] + + if ($formName) { + # --- Form borrowing --- + Info "Borrowing form ${typeName}.${objName}.Form.${formName}..." + + # Auto-borrow parent object if not yet borrowed + if (-not (Test-ObjectBorrowed $typeName $objName)) { + Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + $script:borrowedFiles += $targetFile + } + + # Borrow the form + $hasBMA = [bool]$BorrowMainAttribute + $formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA + $script:borrowedFiles += $formFiles + $borrowedCount++ + + # Borrow main attribute if requested + if ($hasBMA) { + Borrow-MainAttribute $typeName $objName $formName $BorrowMainAttribute + } + } else { + # --- Object borrowing (existing logic) --- + Info "Borrowing ${typeName}.${objName}..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + + $script:borrowedFiles += $targetFile + $borrowedCount++ + } +} + +# --- 15. Save modified Configuration.xml --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) +Info "Saved: $extResolvedPath" + +# --- 16. Summary --- +Write-Host "" +Write-Host "=== cfe-borrow summary ===" +Write-Host " Extension: $extDir" +Write-Host " Config: $cfgDir" +Write-Host " Borrowed: $borrowedCount object(s)" +foreach ($f in $script:borrowedFiles) { + Write-Host " - $f" +} +exit 0 diff --git a/.claude/skills/cfe-diff/SKILL.md b/.claude/skills/cfe-diff/SKILL.md index 79a2c4e8..94405510 100644 --- a/.claude/skills/cfe-diff/SKILL.md +++ b/.claude/skills/cfe-diff/SKILL.md @@ -1,57 +1,57 @@ ---- -name: cfe-diff -description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию -argument-hint: -ExtensionPath -ConfigPath [-Mode A|B] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cfe-diff — Анализ расширения конфигурации - -Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). - -## Параметры - -| Параметр | Описание | По умолчанию | -|----------|----------|--------------| -| `ExtensionPath` | Путь к расширению (обязат.) | — | -| `ConfigPath` | Путь к конфигурации (обязат.) | — | -| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A -``` - -## Mode A — обзор расширения - -Для каждого объекта показывает: -- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы -- `[OWN]` — собственный: количество реквизитов, ТЧ, форм - -Для каждой формы заимствованного объекта показывается: -- `(borrowed)` / `(own)` — заимствованная или собственная форма -- callType-события формы и элементов -- callType на командах - -## Mode B — проверка переноса - -Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. - -Статусы: -- `[TRANSFERRED]` — код найден в конфигурации -- `[NOT_TRANSFERRED]` — код не найден -- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден - -## Примеры - -```powershell -# Обзор — что изменено в расширении -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A - -# Проверка переноса — все ли #Вставка перенесены -... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B -``` +--- +name: cfe-diff +description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию +argument-hint: -ExtensionPath -ConfigPath [-Mode A|B] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-diff — Анализ расширения конфигурации + +Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ConfigPath` | Путь к конфигурации (обязат.) | — | +| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A +``` + +## Mode A — обзор расширения + +Для каждого объекта показывает: +- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы +- `[OWN]` — собственный: количество реквизитов, ТЧ, форм + +Для каждой формы заимствованного объекта показывается: +- `(borrowed)` / `(own)` — заимствованная или собственная форма +- callType-события формы и элементов +- callType на командах + +## Mode B — проверка переноса + +Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. + +Статусы: +- `[TRANSFERRED]` — код найден в конфигурации +- `[NOT_TRANSFERRED]` — код не найден +- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден + +## Примеры + +```powershell +# Обзор — что изменено в расширении +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A + +# Проверка переноса — все ли #Вставка перенесены +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B +``` diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 index 0f6dbdc4..48d19ec1 100644 --- a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -1,471 +1,471 @@ -# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$ExtensionPath, - - [Parameter(Mandatory)] - [string]$ConfigPath, - - [ValidateSet("A","B")] - [string]$Mode = "A" -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve paths --- -if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { - $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath -} -if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath -} -if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } -if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } - -$extCfg = Join-Path $ExtensionPath "Configuration.xml" -$srcCfg = Join-Path $ConfigPath "Configuration.xml" -if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } -if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } - -# --- Type -> directory mapping --- -$childTypeDirMap = @{ - "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" - "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" - "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" - "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" - "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" - "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" - "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" - "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" - "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" - "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" - "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" - "FunctionalOptionsParameter"="FunctionalOptionsParameters" - "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" - "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" - "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" - "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" - "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" - "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" - "CommonAttribute"="CommonAttributes" -} - -# --- Parse extension Configuration.xml --- -$extDoc = New-Object System.Xml.XmlDocument -$extDoc.PreserveWhitespace = $false -$extDoc.Load($extCfg) - -$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - -$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) -$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) -$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } -$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) -$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } -$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) -$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } - -Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" -Write-Host " NamePrefix: $namePrefix" -Write-Host "" - -# --- Collect ChildObjects --- -$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) -if (-not $childObjNode) { - Write-Host "[WARN] No ChildObjects in extension" - exit 0 -} - -$objects = @() -foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -eq "Language") { continue } - $objects += @{ Type = $child.LocalName; Name = $child.InnerText } -} - -if ($objects.Count -eq 0) { - Write-Host "No objects (besides Language) in extension." - exit 0 -} - -# --- Helper: check if object is borrowed --- -function Get-ObjectInfo { - param([string]$objType, [string]$objName) - - if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } - $dirName = $childTypeDirMap[$objType] - $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" - - if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } - - $doc = New-Object System.Xml.XmlDocument - $doc.PreserveWhitespace = $false - $doc.Load($objFile) - - $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) - $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $objEl = $null - foreach ($c in $doc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $objEl = $c; break } - } - if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } - - $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) - $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } - - $info = @{ - Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") - File = $objFile - Exists = $true - Type = $objType - Name = $objName - DirName = $dirName - ObjElement = $objEl - ObjNs = $objNs - } - return $info -} - -# --- Helper: find .bsl files for object --- -function Get-BslFiles { - param([string]$objType, [string]$objName) - - if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } - $dirName = $childTypeDirMap[$objType] - $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName - - if (-not (Test-Path $objDir -PathType Container)) { return @() } - - $bslFiles = @() - $extDir = Join-Path $objDir "Ext" - if (Test-Path $extDir) { - $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue - foreach ($item in $items) { $bslFiles += $item.FullName } - } - - # Forms - $formsDir = Join-Path $objDir "Forms" - if (Test-Path $formsDir) { - $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue - foreach ($fm in $formModules) { $bslFiles += $fm.FullName } - } - - return $bslFiles -} - -# --- Helper: parse interceptors from .bsl --- -function Get-Interceptors { - param([string]$bslPath) - - if (-not (Test-Path $bslPath)) { return @() } - $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) - $interceptors = @() - $i = 0 - while ($i -lt $lines.Count) { - $line = $lines[$i].Trim() - if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { - $type = $Matches[1] - $method = $Matches[2] - $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } - } - $i++ - } - return $interceptors -} - -# --- Helper: extract #Вставка blocks from .bsl --- -function Get-InsertionBlocks { - param([string]$bslPath) - - if (-not (Test-Path $bslPath)) { return @() } - $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) - $blocks = @() - $inBlock = $false - $blockLines = @() - $startLine = 0 - - for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i].Trim() - if ($line -eq "#Вставка") { - $inBlock = $true - $blockLines = @() - $startLine = $i + 1 - } elseif ($line -eq "#КонецВставки" -and $inBlock) { - $inBlock = $false - $blocks += @{ - StartLine = $startLine - EndLine = $i + 1 - Code = ($blockLines -join "`n").Trim() - File = $bslPath - } - } elseif ($inBlock) { - $blockLines += $lines[$i] - } - } - return $blocks -} - -# --- Helper: analyze form for callType events and commands --- -function Get-FormInterceptors { - param([string]$formXmlPath) - - if (-not (Test-Path $formXmlPath)) { return $null } - - $formDoc = New-Object System.Xml.XmlDocument - $formDoc.PreserveWhitespace = $false - try { $formDoc.Load($formXmlPath) } catch { return $null } - - $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) - $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") - - $fRoot = $formDoc.DocumentElement - $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) - $isBorrowed = ($baseForm -ne $null) - - $interceptors = @() - - # Form-level events with callType - $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) - if ($eventsNode) { - foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { - $ct = $evt.GetAttribute("callType") - if ($ct) { - $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" - } - } - } - - # Element-level events with callType (scan all elements recursively) - $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) - if ($childItems) { - foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { - $elName = $evtNode.GetAttribute("name") - foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { - $ct = $evt.GetAttribute("callType") - $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" - } - } - } - - # Commands with callType on Action - foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { - $cmdName = $cmd.GetAttribute("name") - foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { - $ct = $action.GetAttribute("callType") - $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" - } - } - - return @{ - IsBorrowed = $isBorrowed - Interceptors = $interceptors - } -} - -# ============================================================ -# MODE A: Extension overview -# ============================================================ -if ($Mode -eq "A") { - $borrowedList = @() - $ownList = @() - - foreach ($obj in $objects) { - $info = Get-ObjectInfo $obj.Type $obj.Name - if (-not $info) { - Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" - continue - } - if (-not $info.Exists) { - Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" - continue - } - - if ($info.Borrowed) { - $borrowedList += $obj - - Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" - - # Find .bsl files and interceptors - $bslFiles = Get-BslFiles $obj.Type $obj.Name - foreach ($bsl in $bslFiles) { - $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") - $interceptors = Get-Interceptors $bsl - if ($interceptors.Count -gt 0) { - foreach ($ic in $interceptors) { - Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" - } - } else { - Write-Host " $relPath (no interceptors)" - } - } - - # Check for own attributes/forms in ChildObjects - if ($info.ObjElement) { - $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) - if ($childObj) { - $ownAttrs = 0 - $ownForms = 0 - $ownTS = 0 - $borrowedItems = 0 - $formNames = @() - foreach ($c in $childObj.ChildNodes) { - if ($c.NodeType -ne 'Element') { continue } - $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) - if ($cProps) { - $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) - if ($cOb -and $cOb.InnerText -eq "Adopted") { - $borrowedItems++ - continue - } - } - switch ($c.LocalName) { - "Attribute" { $ownAttrs++ } - "TabularSection" { $ownTS++ } - "Form" { $formNames += $c.InnerText; $ownForms++ } - } - } - $parts = @() - if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } - if ($ownTS -gt 0) { $parts += "$ownTS own TS" } - if ($ownForms -gt 0) { $parts += "$ownForms own forms" } - if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } - if ($parts.Count -gt 0) { - Write-Host " ChildObjects: $($parts -join ', ')" - } - - # Analyze forms - $borrowedFormCount = 0 - $ownFormCount = 0 - foreach ($fn in $formNames) { - $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" - $fi = Get-FormInterceptors $formXmlPath - if (-not $fi) { - Write-Host " Form.$fn (?)" - continue - } - $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } - if ($fi.Interceptors.Count -gt 0) { - Write-Host " Form.$fn ($formTag):" - foreach ($ic in $fi.Interceptors) { - Write-Host " $ic" - } - } else { - Write-Host " Form.$fn ($formTag)" - } - } - } - } - } else { - $ownList += $obj - Write-Host " [OWN] $($obj.Type).$($obj.Name)" - - # Brief info for own objects - if ($info.ObjElement) { - $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) - if ($childObj) { - $attrs = 0; $forms = 0; $ts = 0 - foreach ($c in $childObj.ChildNodes) { - if ($c.NodeType -ne 'Element') { continue } - switch ($c.LocalName) { - "Attribute" { $attrs++ } - "TabularSection" { $ts++ } - "Form" { $forms++ } - } - } - $parts = @() - if ($attrs -gt 0) { $parts += "$attrs attrs" } - if ($ts -gt 0) { $parts += "$ts TS" } - if ($forms -gt 0) { $parts += "$forms forms" } - if ($parts.Count -gt 0) { - Write-Host " $($parts -join ', ')" - } - } - } - } - } - - Write-Host "" - Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" -} - -# ============================================================ -# MODE B: Transfer check -# ============================================================ -if ($Mode -eq "B") { - $transferred = 0 - $notTransferred = 0 - $needsReview = 0 - - foreach ($obj in $objects) { - $info = Get-ObjectInfo $obj.Type $obj.Name - if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } - - # Find .bsl files with &ИзменениеИКонтроль - $bslFiles = Get-BslFiles $obj.Type $obj.Name - foreach ($bsl in $bslFiles) { - $interceptors = Get-Interceptors $bsl - $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) - - if ($macInterceptors.Count -eq 0) { continue } - - foreach ($ic in $macInterceptors) { - $methodName = $ic.Method - $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") - - # Find #Вставка blocks in this file - $insertBlocks = Get-InsertionBlocks $bsl - - if ($insertBlocks.Count -eq 0) { - Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" - $needsReview++ - continue - } - - # Find corresponding module in config - if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } - $dirName = $childTypeDirMap[$obj.Type] - $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) - - if (-not (Test-Path $configBsl)) { - Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" - $needsReview++ - continue - } - - $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) - - $allTransferred = $true - foreach ($block in $insertBlocks) { - $code = $block.Code - if (-not $code) { continue } - - # Normalize whitespace for comparison - $codeNorm = $code -replace '\s+', ' ' - $configNorm = $configContent -replace '\s+', ' ' - - if ($configNorm.Contains($codeNorm)) { - # Found in config - } else { - $allTransferred = $false - } - } - - if ($allTransferred) { - Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" - $transferred++ - } else { - Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" - $notTransferred++ - } - } - } - } - - Write-Host "" - Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" -} +# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ConfigPath, + + [ValidateSet("A","B")] + [string]$Mode = "A" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } +if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } + +$extCfg = Join-Path $ExtensionPath "Configuration.xml" +$srcCfg = Join-Path $ConfigPath "Configuration.xml" +if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } +if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } + +# --- Type -> directory mapping --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "CommonAttribute"="CommonAttributes" +} + +# --- Parse extension Configuration.xml --- +$extDoc = New-Object System.Xml.XmlDocument +$extDoc.PreserveWhitespace = $false +$extDoc.Load($extCfg) + +$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + +$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) +$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) +$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } +$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } +$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) +$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + +Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" +Write-Host " NamePrefix: $namePrefix" +Write-Host "" + +# --- Collect ChildObjects --- +$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) +if (-not $childObjNode) { + Write-Host "[WARN] No ChildObjects in extension" + exit 0 +} + +$objects = @() +foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Language") { continue } + $objects += @{ Type = $child.LocalName; Name = $child.InnerText } +} + +if ($objects.Count -eq 0) { + Write-Host "No objects (besides Language) in extension." + exit 0 +} + +# --- Helper: check if object is borrowed --- +function Get-ObjectInfo { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } + $dirName = $childTypeDirMap[$objType] + $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } + + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $objEl = $null + foreach ($c in $doc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } + + $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) + $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } + + $info = @{ + Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") + File = $objFile + Exists = $true + Type = $objType + Name = $objName + DirName = $dirName + ObjElement = $objEl + ObjNs = $objNs + } + return $info +} + +# --- Helper: find .bsl files for object --- +function Get-BslFiles { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } + $dirName = $childTypeDirMap[$objType] + $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName + + if (-not (Test-Path $objDir -PathType Container)) { return @() } + + $bslFiles = @() + $extDir = Join-Path $objDir "Ext" + if (Test-Path $extDir) { + $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue + foreach ($item in $items) { $bslFiles += $item.FullName } + } + + # Forms + $formsDir = Join-Path $objDir "Forms" + if (Test-Path $formsDir) { + $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue + foreach ($fm in $formModules) { $bslFiles += $fm.FullName } + } + + return $bslFiles +} + +# --- Helper: parse interceptors from .bsl --- +function Get-Interceptors { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $interceptors = @() + $i = 0 + while ($i -lt $lines.Count) { + $line = $lines[$i].Trim() + if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { + $type = $Matches[1] + $method = $Matches[2] + $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } + } + $i++ + } + return $interceptors +} + +# --- Helper: extract #Вставка blocks from .bsl --- +function Get-InsertionBlocks { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $blocks = @() + $inBlock = $false + $blockLines = @() + $startLine = 0 + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ($line -eq "#Вставка") { + $inBlock = $true + $blockLines = @() + $startLine = $i + 1 + } elseif ($line -eq "#КонецВставки" -and $inBlock) { + $inBlock = $false + $blocks += @{ + StartLine = $startLine + EndLine = $i + 1 + Code = ($blockLines -join "`n").Trim() + File = $bslPath + } + } elseif ($inBlock) { + $blockLines += $lines[$i] + } + } + return $blocks +} + +# --- Helper: analyze form for callType events and commands --- +function Get-FormInterceptors { + param([string]$formXmlPath) + + if (-not (Test-Path $formXmlPath)) { return $null } + + $formDoc = New-Object System.Xml.XmlDocument + $formDoc.PreserveWhitespace = $false + try { $formDoc.Load($formXmlPath) } catch { return $null } + + $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) + $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + + $fRoot = $formDoc.DocumentElement + $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) + $isBorrowed = ($baseForm -ne $null) + + $interceptors = @() + + # Form-level events with callType + $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) + if ($eventsNode) { + foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Element-level events with callType (scan all elements recursively) + $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) + if ($childItems) { + foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { + $elName = $evtNode.GetAttribute("name") + foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { + $ct = $evt.GetAttribute("callType") + $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Commands with callType on Action + foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { + $cmdName = $cmd.GetAttribute("name") + foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { + $ct = $action.GetAttribute("callType") + $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" + } + } + + return @{ + IsBorrowed = $isBorrowed + Interceptors = $interceptors + } +} + +# ============================================================ +# MODE A: Extension overview +# ============================================================ +if ($Mode -eq "A") { + $borrowedList = @() + $ownList = @() + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info) { + Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" + continue + } + if (-not $info.Exists) { + Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" + continue + } + + if ($info.Borrowed) { + $borrowedList += $obj + + Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" + + # Find .bsl files and interceptors + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + $interceptors = Get-Interceptors $bsl + if ($interceptors.Count -gt 0) { + foreach ($ic in $interceptors) { + Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" + } + } else { + Write-Host " $relPath (no interceptors)" + } + } + + # Check for own attributes/forms in ChildObjects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $ownAttrs = 0 + $ownForms = 0 + $ownTS = 0 + $borrowedItems = 0 + $formNames = @() + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) + if ($cProps) { + $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) + if ($cOb -and $cOb.InnerText -eq "Adopted") { + $borrowedItems++ + continue + } + } + switch ($c.LocalName) { + "Attribute" { $ownAttrs++ } + "TabularSection" { $ownTS++ } + "Form" { $formNames += $c.InnerText; $ownForms++ } + } + } + $parts = @() + if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } + if ($ownTS -gt 0) { $parts += "$ownTS own TS" } + if ($ownForms -gt 0) { $parts += "$ownForms own forms" } + if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } + if ($parts.Count -gt 0) { + Write-Host " ChildObjects: $($parts -join ', ')" + } + + # Analyze forms + $borrowedFormCount = 0 + $ownFormCount = 0 + foreach ($fn in $formNames) { + $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" + $fi = Get-FormInterceptors $formXmlPath + if (-not $fi) { + Write-Host " Form.$fn (?)" + continue + } + $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } + if ($fi.Interceptors.Count -gt 0) { + Write-Host " Form.$fn ($formTag):" + foreach ($ic in $fi.Interceptors) { + Write-Host " $ic" + } + } else { + Write-Host " Form.$fn ($formTag)" + } + } + } + } + } else { + $ownList += $obj + Write-Host " [OWN] $($obj.Type).$($obj.Name)" + + # Brief info for own objects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $attrs = 0; $forms = 0; $ts = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + switch ($c.LocalName) { + "Attribute" { $attrs++ } + "TabularSection" { $ts++ } + "Form" { $forms++ } + } + } + $parts = @() + if ($attrs -gt 0) { $parts += "$attrs attrs" } + if ($ts -gt 0) { $parts += "$ts TS" } + if ($forms -gt 0) { $parts += "$forms forms" } + if ($parts.Count -gt 0) { + Write-Host " $($parts -join ', ')" + } + } + } + } + } + + Write-Host "" + Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" +} + +# ============================================================ +# MODE B: Transfer check +# ============================================================ +if ($Mode -eq "B") { + $transferred = 0 + $notTransferred = 0 + $needsReview = 0 + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } + + # Find .bsl files with &ИзменениеИКонтроль + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $interceptors = Get-Interceptors $bsl + $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) + + if ($macInterceptors.Count -eq 0) { continue } + + foreach ($ic in $macInterceptors) { + $methodName = $ic.Method + $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + + # Find #Вставка blocks in this file + $insertBlocks = Get-InsertionBlocks $bsl + + if ($insertBlocks.Count -eq 0) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" + $needsReview++ + continue + } + + # Find corresponding module in config + if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } + $dirName = $childTypeDirMap[$obj.Type] + $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) + + if (-not (Test-Path $configBsl)) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" + $needsReview++ + continue + } + + $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) + + $allTransferred = $true + foreach ($block in $insertBlocks) { + $code = $block.Code + if (-not $code) { continue } + + # Normalize whitespace for comparison + $codeNorm = $code -replace '\s+', ' ' + $configNorm = $configContent -replace '\s+', ' ' + + if ($configNorm.Contains($codeNorm)) { + # Found in config + } else { + $allTransferred = $false + } + } + + if ($allTransferred) { + Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" + $transferred++ + } else { + Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" + $notTransferred++ + } + } + } + } + + Write-Host "" + Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" +} diff --git a/.claude/skills/cfe-init/SKILL.md b/.claude/skills/cfe-init/SKILL.md index 4d46c7c3..0fb7147d 100644 --- a/.claude/skills/cfe-init/SKILL.md +++ b/.claude/skills/cfe-init/SKILL.md @@ -1,71 +1,71 @@ ---- -name: cfe-init -description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации -argument-hint: [-ConfigPath ] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cfe-init — Создание расширения конфигурации 1С - -Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. - -## Подготовка - -Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. - -### Авто-определение ConfigPath - -Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: -1. Прочитай `.v8-project.json` из корня проекта -2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) -3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` -4. Если `configSrc` нет — спроси у пользователя - -Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). - -## Параметры - -| Параметр | Описание | По умолчанию | -|----------|----------|--------------| -| `Name` | Имя расширения (обязат.) | — | -| `Synonym` | Синоним | = Name | -| `NamePrefix` | Префикс собственных объектов | = Name + "_" | -| `OutputDir` | Каталог для создания | `src` | -| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | -| `Version` | Версия расширения | — | -| `Vendor` | Поставщик | — | -| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | -| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | -| `NoRole` | Без основной роли | false | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -``` - -## Примеры - -```powershell -# Расширение для ERP с авто-определением совместимости из базовой конфигурации -... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src - -# Расширение-исправление с явным режимом совместимости -... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src - -# Расширение-доработка с версией -... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src - -# Без роли, с явным префиксом -... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src -``` - -## Верификация - -``` -/cfe-validate -``` - +--- +name: cfe-init +description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации +argument-hint: [-ConfigPath ] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-init — Создание расширения конфигурации 1С + +Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. + +## Подготовка + +Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. + +### Авто-определение ConfigPath + +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + +Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `Name` | Имя расширения (обязат.) | — | +| `Synonym` | Синоним | = Name | +| `NamePrefix` | Префикс собственных объектов | = Name + "_" | +| `OutputDir` | Каталог для создания | `src` | +| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | +| `Version` | Версия расширения | — | +| `Vendor` | Поставщик | — | +| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | +| `NoRole` | Без основной роли | false | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" +``` + +## Примеры + +```powershell +# Расширение для ERP с авто-определением совместимости из базовой конфигурации +... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src + +# Расширение-исправление с явным режимом совместимости +... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src + +# Расширение-доработка с версией +... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src + +# Без роли, с явным префиксом +... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.claude/skills/cfe-init/scripts/cfe-init.ps1 b/.claude/skills/cfe-init/scripts/cfe-init.ps1 index c9d22f64..6860f97e 100644 --- a/.claude/skills/cfe-init/scripts/cfe-init.ps1 +++ b/.claude/skills/cfe-init/scripts/cfe-init.ps1 @@ -1,279 +1,279 @@ -# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$Name, - [string]$Synonym = $Name, - [string]$NamePrefix, - [string]$OutputDir = "src", - [ValidateSet("Patch","Customization","AddOn")] - [string]$Purpose = "Customization", - [string]$Version, - [string]$Vendor, - [string]$CompatibilityMode = "Version8_3_24", - [string]$ConfigPath, - [switch]$NoRole -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Default NamePrefix --- -if (-not $NamePrefix) { - $NamePrefix = "${Name}_" -} - -# --- Resolve output dir --- -if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { - $OutputDir = Join-Path (Get-Location).Path $OutputDir -} - -# --- Check existing --- -$cfgFile = Join-Path $OutputDir "Configuration.xml" -if (Test-Path $cfgFile) { - Write-Error "Configuration.xml already exists: $cfgFile" - exit 1 -} - -# MDClasses format version — inherited from the base config so the extension stays uniform -# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch). -$formatVersion = "2.17" - -# --- Resolve ConfigPath --- -$baseLangUuid = "00000000-0000-0000-0000-000000000000" -if ($ConfigPath) { - if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { - $ConfigPath = Join-Path (Get-Location).Path $ConfigPath - } - if (Test-Path $ConfigPath -PathType Container) { - $candidate = Join-Path $ConfigPath "Configuration.xml" - if (Test-Path $candidate) { $ConfigPath = $candidate } - else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } - } - if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } - $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent - - # 3a. Read Language UUID from base config - $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" - if (Test-Path $baseLangFile) { - $baseLangDoc = New-Object System.Xml.XmlDocument - $baseLangDoc.PreserveWhitespace = $false - $baseLangDoc.Load($baseLangFile) - $langEl = $null - foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } - } - if ($langEl) { - $baseLangUuid = $langEl.GetAttribute("uuid") - Write-Host "[INFO] Base config Language UUID: $baseLangUuid" - } else { - Write-Host "[WARN] No element in $baseLangFile" - } - } else { - Write-Host "[WARN] Base config language not found: $baseLangFile" - } - - # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config - $baseCfgDoc = New-Object System.Xml.XmlDocument - $baseCfgDoc.PreserveWhitespace = $false - $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) - $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) - $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version") - if ($fmtVer) { - $formatVersion = $fmtVer - Write-Host "[INFO] Base config format version: $formatVersion" - } - $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) - if ($compatNode -and $compatNode.InnerText) { - $CompatibilityMode = $compatNode.InnerText.Trim() - Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" - } else { - Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" - } - $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) - if ($ifcNode -and $ifcNode.InnerText) { - $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() - Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" - } else { - $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" - Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" - } -} else { - $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" - Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." -} - -# --- Generate UUIDs --- -$uuidCfg = [guid]::NewGuid().ToString() -$uuidLang = [guid]::NewGuid().ToString() -$uuidRole = [guid]::NewGuid().ToString() - -# 7 ContainedObject ObjectIds -$co1 = [guid]::NewGuid().ToString() -$co2 = [guid]::NewGuid().ToString() -$co3 = [guid]::NewGuid().ToString() -$co4 = [guid]::NewGuid().ToString() -$co5 = [guid]::NewGuid().ToString() -$co6 = [guid]::NewGuid().ToString() -$co7 = [guid]::NewGuid().ToString() - -# --- Synonym XML --- -$synonymXml = "" -if ($Synonym) { - $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" -} - -# --- Optional properties --- -$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } -$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } - -# --- Role name --- -$roleName = "${NamePrefix}ОсновнаяРоль" - -# --- DefaultRoles XML --- -$defaultRolesXml = "" -if (-not $NoRole) { - $defaultRolesXml = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" -} - -# --- ChildObjects --- -$childObjectsXml = "`r`n`t`t`tРусский" -if (-not $NoRole) { - $childObjectsXml += "`r`n`t`t`t$roleName" -} -$childObjectsXml += "`r`n`t`t" - -# --- Configuration.xml --- -$cfgXml = @" - - - - - - 9cd510cd-abfc-11d4-9434-004095e12fc7 - $co1 - - - 9fcd25a0-4822-11d4-9414-008048da11f9 - $co2 - - - e3687481-0a87-462c-a166-9f34594f9bba - $co3 - - - 9de14907-ec23-4a07-96f0-85521cb6b53b - $co4 - - - 51f2d5d8-ea4d-4064-8892-82951750031e - $co5 - - - e68182ea-4237-4383-967f-90c1e3370bc7 - $co6 - - - fb282519-d103-4dd3-bc12-cb271d631dfc - $co7 - - - - Adopted - $([System.Security.SecurityElement]::Escape($Name)) - $synonymXml - - $Purpose - true - $([System.Security.SecurityElement]::Escape($NamePrefix)) - $CompatibilityMode - ManagedApplication - - PlatformApplication - - Russian - $defaultRolesXml - $vendorXml - $versionXml - Language.Русский - - - - - - $InterfaceCompatibilityMode - - $childObjectsXml - - -"@ - -# --- Languages/Русский.xml (adopted format) --- -$langXml = @" - - - - - - Adopted - Русский - - $baseLangUuid - ru - - - -"@ - -# --- Role XML --- -$roleXml = @" - - - - - $([System.Security.SecurityElement]::Escape($roleName)) - - - - - -"@ - -# --- Create directories --- -if (-not (Test-Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null -} -$langDir = Join-Path $OutputDir "Languages" -if (-not (Test-Path $langDir)) { - New-Item -ItemType Directory -Path $langDir -Force | Out-Null -} - -# --- Write files with UTF-8 BOM --- -$enc = New-Object System.Text.UTF8Encoding($true) - -[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) -$langFile = Join-Path $langDir "Русский.xml" -[System.IO.File]::WriteAllText($langFile, $langXml, $enc) - -# --- Role --- -if (-not $NoRole) { - $roleDir = Join-Path $OutputDir "Roles" - if (-not (Test-Path $roleDir)) { - New-Item -ItemType Directory -Path $roleDir -Force | Out-Null - } - $roleFile = Join-Path $roleDir "$roleName.xml" - [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) -} - -# --- Output --- -Write-Host "[OK] Создано расширение: $Name" -Write-Host " Каталог: $OutputDir" -Write-Host " Назначение: $Purpose" -Write-Host " Префикс: $NamePrefix" -Write-Host " Совместимость: $CompatibilityMode" -Write-Host " Configuration.xml: $cfgFile" -Write-Host " Languages: $langFile" -if (-not $NoRole) { - Write-Host " Role: $roleFile" -} +# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$NamePrefix, + [string]$OutputDir = "src", + [ValidateSet("Patch","Customization","AddOn")] + [string]$Purpose = "Customization", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24", + [string]$ConfigPath, + [switch]$NoRole +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Default NamePrefix --- +if (-not $NamePrefix) { + $NamePrefix = "${Name}_" +} + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# MDClasses format version — inherited from the base config so the extension stays uniform +# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch). +$formatVersion = "2.17" + +# --- Resolve ConfigPath --- +$baseLangUuid = "00000000-0000-0000-0000-000000000000" +if ($ConfigPath) { + if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath + } + if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } + } + if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } + $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent + + # 3a. Read Language UUID from base config + $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" + if (Test-Path $baseLangFile) { + $baseLangDoc = New-Object System.Xml.XmlDocument + $baseLangDoc.PreserveWhitespace = $false + $baseLangDoc.Load($baseLangFile) + $langEl = $null + foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } + } + if ($langEl) { + $baseLangUuid = $langEl.GetAttribute("uuid") + Write-Host "[INFO] Base config Language UUID: $baseLangUuid" + } else { + Write-Host "[WARN] No element in $baseLangFile" + } + } else { + Write-Host "[WARN] Base config language not found: $baseLangFile" + } + + # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config + $baseCfgDoc = New-Object System.Xml.XmlDocument + $baseCfgDoc.PreserveWhitespace = $false + $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) + $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) + $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version") + if ($fmtVer) { + $formatVersion = $fmtVer + Write-Host "[INFO] Base config format version: $formatVersion" + } + $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) + if ($compatNode -and $compatNode.InnerText) { + $CompatibilityMode = $compatNode.InnerText.Trim() + Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" + } else { + Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" + } + $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) + if ($ifcNode -and $ifcNode.InnerText) { + $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() + Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" + } else { + $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" + Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" + } +} else { + $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" + Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +$uuidRole = [guid]::NewGuid().ToString() + +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Role name --- +$roleName = "${NamePrefix}ОсновнаяРоль" + +# --- DefaultRoles XML --- +$defaultRolesXml = "" +if (-not $NoRole) { + $defaultRolesXml = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" +} + +# --- ChildObjects --- +$childObjectsXml = "`r`n`t`t`tРусский" +if (-not $NoRole) { + $childObjectsXml += "`r`n`t`t`t$roleName" +} +$childObjectsXml += "`r`n`t`t" + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + Adopted + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + $Purpose + true + $([System.Security.SecurityElement]::Escape($NamePrefix)) + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + $defaultRolesXml + $vendorXml + $versionXml + Language.Русский + + + + + + $InterfaceCompatibilityMode + + $childObjectsXml + + +"@ + +# --- Languages/Русский.xml (adopted format) --- +$langXml = @" + + + + + + Adopted + Русский + + $baseLangUuid + ru + + + +"@ + +# --- Role XML --- +$roleXml = @" + + + + + $([System.Security.SecurityElement]::Escape($roleName)) + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) + +# --- Role --- +if (-not $NoRole) { + $roleDir = Join-Path $OutputDir "Roles" + if (-not (Test-Path $roleDir)) { + New-Item -ItemType Directory -Path $roleDir -Force | Out-Null + } + $roleFile = Join-Path $roleDir "$roleName.xml" + [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) +} + +# --- Output --- +Write-Host "[OK] Создано расширение: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Назначение: $Purpose" +Write-Host " Префикс: $NamePrefix" +Write-Host " Совместимость: $CompatibilityMode" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +if (-not $NoRole) { + Write-Host " Role: $roleFile" +} diff --git a/.claude/skills/cfe-patch-method/SKILL.md b/.claude/skills/cfe-patch-method/SKILL.md index c441dd4c..cbc0ea35 100644 --- a/.claude/skills/cfe-patch-method/SKILL.md +++ b/.claude/skills/cfe-patch-method/SKILL.md @@ -1,78 +1,78 @@ ---- -name: cfe-patch-method -description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального -argument-hint: -ExtensionPath -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cfe-patch-method — Генерация перехватчика метода - -Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. - -## Предусловие - -Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. - -## Параметры - -| Параметр | Описание | По умолчанию | -|----------|----------|--------------| -| `ExtensionPath` | Путь к расширению (обязат.) | — | -| `ModulePath` | Путь к модулю (обязат.) | — | -| `MethodName` | Имя перехватываемого метода (обязат.) | — | -| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | -| `Context` | Директива контекста | `НаСервере` | -| `IsFunction` | Метод — функция (добавит `Возврат`) | false | - -## Формат ModulePath - -| ModulePath | Файл | -|------------|------| -| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` | -| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` | -| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` | -| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` | -| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` | -| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | - -Аналогично для Report, DataProcessor, InformationRegister и других типов. - -## Типы перехвата - -| InterceptorType | Декоратор | Назначение | -|-----------------|-----------|------------| -| `Before` | `&Перед` | Код до вызова оригинального метода | -| `After` | `&После` | Код после вызова оригинального метода | -| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before -``` - -## Примеры - -```powershell -# Перехват &Перед на сервере -... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before - -# Перехват &После на клиенте -... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте" - -# ИзменениеИКонтроль для функции -... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction -``` - -## Генерируемый код (Before) - -```bsl -&НаСервере -&Перед("ПриЗаписи") -Процедура Расш1_ПриЗаписи() - // TODO: код перед вызовом оригинального метода -КонецПроцедуры -``` +--- +name: cfe-patch-method +description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального +argument-hint: -ExtensionPath -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-patch-method — Генерация перехватчика метода + +Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. + +## Предусловие + +Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ModulePath` | Путь к модулю (обязат.) | — | +| `MethodName` | Имя перехватываемого метода (обязат.) | — | +| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | +| `Context` | Директива контекста | `НаСервере` | +| `IsFunction` | Метод — функция (добавит `Возврат`) | false | + +## Формат ModulePath + +| ModulePath | Файл | +|------------|------| +| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` | +| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` | +| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` | +| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` | +| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` | +| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | + +Аналогично для Report, DataProcessor, InformationRegister и других типов. + +## Типы перехвата + +| InterceptorType | Декоратор | Назначение | +|-----------------|-----------|------------| +| `Before` | `&Перед` | Код до вызова оригинального метода | +| `After` | `&После` | Код после вызова оригинального метода | +| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +``` + +## Примеры + +```powershell +# Перехват &Перед на сервере +... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before + +# Перехват &После на клиенте +... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте" + +# ИзменениеИКонтроль для функции +... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction +``` + +## Генерируемый код (Before) + +```bsl +&НаСервере +&Перед("ПриЗаписи") +Процедура Расш1_ПриЗаписи() + // TODO: код перед вызовом оригинального метода +КонецПроцедуры +``` diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 5b46fd3d..6a1cff2e 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: cfe-validate -description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /cfe-validate — валидация расширения конфигурации (CFE) - -Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|---------------|:-----:|---------|-------------------------------------------------| -| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" -``` +--- +name: cfe-validate +description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-validate — валидация расширения конфигурации (CFE) + +Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|---------------|:-----:|---------|-------------------------------------------------| +| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" +``` diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 2b6d5738..fcfc9155 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,939 +1,939 @@ -# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$ExtensionPath, - - [switch]$Detailed, - - [int]$MaxErrors = 30, - - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { - $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath -} - -if (Test-Path $ExtensionPath -PathType Container) { - $candidate = Join-Path $ExtensionPath "Configuration.xml" - if (Test-Path $candidate) { - $ExtensionPath = $candidate - } else { - Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath" - exit 1 - } -} - -if (-not (Test-Path $ExtensionPath)) { - Write-Host "[ERROR] File not found: $ExtensionPath" - exit 1 -} - -$resolvedPath = (Resolve-Path $ExtensionPath).Path -$configDir = Split-Path $resolvedPath -Parent - -# --- Output infrastructure --- -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$finalize = { - $checks = $script:okCount + $script:errors + $script:warnings - if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: Extension.$objName ($checks checks) ===" - } else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() - } - Write-Host $result - - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" - } -} - -# --- Reference tables --- -$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' - -# 7 fixed ClassIds for Configuration -$validClassIds = @( - "9cd510cd-abfc-11d4-9434-004095e12fc7", - "9fcd25a0-4822-11d4-9414-008048da11f9", - "e3687481-0a87-462c-a166-9f34594f9bba", - "9de14907-ec23-4a07-96f0-85521cb6b53b", - "51f2d5d8-ea4d-4064-8892-82951750031e", - "e68182ea-4237-4383-967f-90c1e3370bc7", - "fb282519-d103-4dd3-bc12-cb271d631dfc" -) - -# 44 types in canonical order -$childObjectTypes = @( - "Language","Subsystem","StyleItem","Style", - "CommonPicture","SessionParameter","Role","CommonTemplate", - "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", - "XDTOPackage","WebService","HTTPService","WSReference", - "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", - "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", - "Constant","CommonForm","Catalog","Document", - "DocumentNumerator","Sequence","DocumentJournal","Enum", - "Report","DataProcessor","InformationRegister","AccumulationRegister", - "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", - "ChartOfCalculationTypes","CalculationRegister", - "BusinessProcess","Task","IntegrationService" -) - -# Type -> directory mapping -$childTypeDirMap = @{ - "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" - "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" - "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" - "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" - "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" - "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" - "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" - "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" - "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" - "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" - "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" - "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" - "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" - "AccumulationRegister"="AccumulationRegisters" - "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" - "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" - "ChartOfCalculationTypes"="ChartsOfCalculationTypes" - "CalculationRegister"="CalculationRegisters" - "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" - "IntegrationService"="IntegrationServices" -} - -# Valid enum values for extension properties -$validEnumValues = @{ - "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") - "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") - "ScriptVariant" = @("Russian","English") - "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") -} - -# --- 1. Parse XML --- -Out-Line "" - -$xmlDoc = $null -try { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $false - $xmlDoc.Load($resolvedPath) -} catch { - Out-Line "=== Validation: Extension (parse failed) ===" - Out-Line "" - Report-Error "1. XML parse failed: $($_.Exception.Message)" - & $finalize - exit 1 -} - -# --- Register namespaces --- -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") - -$root = $xmlDoc.DocumentElement - -# --- Check 1: Root structure --- -$check1Ok = $true -$expectedNs = "http://v8.1c.ru/8.3/MDClasses" - -if ($root.LocalName -ne "MetaDataObject") { - Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" - & $finalize - exit 1 -} - -if ($root.NamespaceURI -ne $expectedNs) { - Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" - $check1Ok = $false -} - -$version = $root.GetAttribute("version") -if (-not $version) { - Report-Warn "1. Missing version attribute on MetaDataObject" -} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { - Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" -} - -# Must have Configuration child -$cfgNode = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { - $cfgNode = $child; break - } -} - -if (-not $cfgNode) { - Report-Error "1. No element found inside MetaDataObject" - & $finalize - exit 1 -} - -# UUID -$cfgUuid = $cfgNode.GetAttribute("uuid") -if (-not $cfgUuid) { - Report-Error "1. Missing uuid on " - $check1Ok = $false -} elseif ($cfgUuid -notmatch $guidPattern) { - Report-Error "1. Invalid uuid '$cfgUuid' on " - $check1Ok = $false -} - -# Get name early for header -$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) -$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } -$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } - -$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null - -if ($check1Ok) { - Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 2: InternalInfo --- -$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) -$check2Ok = $true - -if (-not $internalInfo) { - Report-Error "2. InternalInfo: missing" -} else { - $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) - if ($contained.Count -ne 7) { - Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" - } - - $foundClassIds = @{} - foreach ($co in $contained) { - $classId = $co.SelectSingleNode("xr:ClassId", $ns) - $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) - - if (-not $classId -or -not $classId.InnerText) { - Report-Error "2. ContainedObject missing ClassId" - $check2Ok = $false - continue - } - - $cid = $classId.InnerText - if ($validClassIds -notcontains $cid) { - Report-Error "2. Unknown ClassId: $cid" - $check2Ok = $false - } - - if ($foundClassIds.ContainsKey($cid)) { - Report-Error "2. Duplicate ClassId: $cid" - $check2Ok = $false - } - $foundClassIds[$cid] = $true - - if (-not $objectId -or -not $objectId.InnerText) { - Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" - $check2Ok = $false - } elseif ($objectId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" - $check2Ok = $false - } - } - - $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) - if ($missingIds.Count -gt 0) { - Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" - } - - if ($check2Ok) { - Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 3: Extension-specific properties --- -if (-not $propsNode) { - Report-Error "3. Properties block missing" -} else { - $check3Ok = $true - - # ObjectBelonging = Adopted - $obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns) - if (-not $obNode -or $obNode.InnerText -ne "Adopted") { - Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'" - $check3Ok = $false - } - - # Name - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "3. Name is missing or empty" - $check3Ok = $false - } else { - $nameVal = $nameNode.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "3. Name '$nameVal' is not a valid 1C identifier" - $check3Ok = $false - } - } - - # ConfigurationExtensionPurpose - $purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) - $validPurposes = @("Patch","Customization","AddOn") - if (-not $purposeNode -or -not $purposeNode.InnerText) { - Report-Error "3. ConfigurationExtensionPurpose is missing" - $check3Ok = $false - } elseif ($validPurposes -notcontains $purposeNode.InnerText) { - Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)" - $check3Ok = $false - } - - # NamePrefix - $prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns) - if (-not $prefixNode -or -not $prefixNode.InnerText) { - Report-Warn "3. NamePrefix is empty" - } - - # KeepMappingToExtendedConfigurationObjectsByIDs - $keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns) - if (-not $keepMapNode) { - Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing" - } - - # DefaultLanguage - $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) - $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } - - if ($check3Ok) { - $purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" } - $prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" } - Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 4: Enum property values --- -if ($propsNode) { - $enumChecked = 0 - $check4Ok = $true - - foreach ($propName in $validEnumValues.Keys) { - $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) - if ($propNode -and $propNode.InnerText) { - $val = $propNode.InnerText - $allowed = $validEnumValues[$propName] - if ($allowed -notcontains $val) { - Report-Error "4. Property '$propName' has invalid value '$val'" - $check4Ok = $false - } - $enumChecked++ - } - } - - if ($check4Ok) { - Report-OK "4. Property values: $enumChecked enum properties checked" - } -} else { - Report-Warn "4. No Properties block to check" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 5: ChildObjects — valid types, no duplicates, order --- -$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) - -if (-not $childObjNode) { - Report-Error "5. ChildObjects block missing" -} else { - $check5Ok = $true - $totalCount = 0 - $script:childObjectIndex = @{} - $duplicates = @{} - $typeFirstIndex = @{} - $lastTypeOrder = -1 - $orderOk = $true - - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - $objNameVal = $child.InnerText - - $typeIdx = $childObjectTypes.IndexOf($typeName) - if ($typeIdx -lt 0) { - Report-Error "5. Unknown type '$typeName' in ChildObjects" - $check5Ok = $false - } else { - if (-not $typeFirstIndex.ContainsKey($typeName)) { - $typeFirstIndex[$typeName] = $typeIdx - if ($typeIdx -lt $lastTypeOrder) { - Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" - $orderOk = $false - } - $lastTypeOrder = $typeIdx - } - } - - if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } - if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { - if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { - Report-Error "5. Duplicate: $typeName.$objNameVal" - $duplicates["$typeName.$objNameVal"] = $true - $check5Ok = $false - } - } else { - $script:childObjectIndex[$typeName][$objNameVal] = $true - } - - $totalCount++ - } - - $typeCount = $script:childObjectIndex.Count - if ($check5Ok) { - $orderInfo = if ($orderOk) { ", order correct" } else { "" } - Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- -if ($defLang -and $childObjNode) { - $langName = $defLang - if ($langName.StartsWith("Language.")) { - $langName = $langName.Substring(9) - } - - $found = $false - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { - $found = $true; break - } - } - - if ($found) { - Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" - } else { - Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" - } -} else { - if (-not $defLang) { - Report-Warn "6. Cannot check DefaultLanguage (empty)" - } else { - Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 7: Language files exist --- -if ($childObjNode) { - $langNames = @() - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { - $langNames += $child.InnerText - } - } - - if ($langNames.Count -gt 0) { - $existCount = 0 - foreach ($ln in $langNames) { - $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" - if (Test-Path $langFile) { - $existCount++ - } else { - Report-Warn "7. Language file missing: Languages/$ln.xml" - } - } - if ($existCount -eq $langNames.Count) { - Report-OK "7. Language files: $existCount/$($langNames.Count) exist" - } - } else { - Report-Warn "7. No Language entries in ChildObjects" - } -} else { - Report-Warn "7. Cannot check language files (no ChildObjects)" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 8: Object directories exist --- -if ($childObjNode) { - $dirsToCheck = @{} - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - if ($typeName -eq "Language") { continue } - if ($childTypeDirMap.ContainsKey($typeName)) { - $dirName = $childTypeDirMap[$typeName] - if (-not $dirsToCheck.ContainsKey($dirName)) { - $dirsToCheck[$dirName] = 0 - } - $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 - } - } - - $missingDirs = @() - foreach ($dir in $dirsToCheck.Keys) { - $dirPath = Join-Path $configDir $dir - if (-not (Test-Path $dirPath -PathType Container)) { - $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" - } - } - - if ($missingDirs.Count -eq 0) { - Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" - } else { - foreach ($md in $missingDirs) { - Report-Warn "8. Missing directory: $md" - } - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- -$script:enumValuesIndex = @{} -$script:formList = @() - -# Helper: check if sub-item has explicit borrowed metadata -function Test-BorrowedSubItem { - param($subItem, $nsm) - $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) - if (-not $subProps) { return $false } - $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) - if ($subOb -and $subOb.InnerText) { return $true } - $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) - return [bool]($subExt -and $subExt.InnerText) -} - -# Helper: validate a borrowed Attribute/EnumValue sub-item -function Validate-BorrowedSubItem { - param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) - $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) - if (-not $subProps) { - Report-Error "${checkNum}. ${context}: ${subType} missing Properties" - return $false - } - $ok = $true - $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) - if (-not $subOb -or $subOb.InnerText -ne "Adopted") { - Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" - $ok = $false - } - $subName = $subProps.SelectSingleNode("md:Name", $nsm) - if (-not $subName -or -not $subName.InnerText) { - Report-Error "${checkNum}. ${context}: ${subType} missing Name" - $ok = $false - } - $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) - if (-not $subExt -or -not $subExt.InnerText) { - Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" - $ok = $false - } elseif ($subExt.InnerText -notmatch $guidPattern) { - Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" - $ok = $false - } - return $ok -} - -if ($childObjNode) { - $borrowedCount = 0 - $borrowedOk = 0 - $check9Ok = $true - $check10Ok = $true - $subItemCount = 0 - - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $typeName = $child.LocalName - $childName = $child.InnerText - if ($typeName -eq "Language") { continue } - - if (-not $childTypeDirMap.ContainsKey($typeName)) { continue } - $dirName = $childTypeDirMap[$typeName] - $objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml" - - if (-not (Test-Path $objFile)) { continue } - - # Parse object XML - $objDoc = $null - try { - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $false - $objDoc.Load($objFile) - } catch { - Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)" - continue - } - - $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - - # Find the object element (Catalog, Document, etc.) - $objRoot = $objDoc.DocumentElement - $objEl = $null - foreach ($c in $objRoot.ChildNodes) { - if ($c.NodeType -eq 'Element') { $objEl = $c; break } - } - if (-not $objEl) { continue } - - $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) - if (-not $objProps) { continue } - - # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- - $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) - if ($obNode -and $obNode.InnerText -eq "Adopted") { - $borrowedCount++ - - $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) - if (-not $extObj -or -not $extObj.InnerText) { - Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" - $check9Ok = $false - } elseif ($extObj.InnerText -notmatch $guidPattern) { - Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'" - $check9Ok = $false - } else { - $borrowedOk++ - } - } - - # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- - $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) - if ($objChildObjects) { - $ctx = "${typeName}.${childName}" - foreach ($subItem in $objChildObjects.ChildNodes) { - if ($subItem.NodeType -ne 'Element') { continue } - $subType = $subItem.LocalName - - if ($subType -eq "Attribute") { - if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } - $subItemCount++ - if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { - $check10Ok = $false - } - } - elseif ($subType -eq "TabularSection") { - if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } - $subItemCount++ - if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { - $check10Ok = $false - } else { - # Check InternalInfo GeneratedTypes - $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) - $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) - $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } - if (-not $tsInfo) { - Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" - $check10Ok = $false - } else { - $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) - $hasTSCat = $false; $hasTSRCat = $false - foreach ($gt in $gtNodes) { - $cat = $gt.GetAttribute("category") - if ($cat -eq "TabularSection") { $hasTSCat = $true } - if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } - } - if (-not $hasTSCat -or -not $hasTSRCat) { - Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" - $check10Ok = $false - } - } - # Recurse into TS ChildObjects/Attribute - $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) - if ($tsChildObjs) { - foreach ($tsAttr in $tsChildObjs.ChildNodes) { - if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } - if (-not (Test-BorrowedSubItem $tsAttr $objNs)) { continue } - $subItemCount++ - if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { - $check10Ok = $false - } - } - } - } - } - elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { - if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } - $subItemCount++ - if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { - $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) - if ($evName -and $evName.InnerText) { - if (-not $script:enumValuesIndex.ContainsKey($childName)) { - $script:enumValuesIndex[$childName] = @{} - } - $script:enumValuesIndex[$childName][$evName.InnerText] = $true - } - } else { - $check10Ok = $false - } - } - elseif ($subType -eq "Form") { - $formName = $subItem.InnerText - if ($formName) { - $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" - if (-not (Test-Path $formMetaFile)) { - Report-Error "10. ${ctx}: Form.${formName} metadata file missing" - $check10Ok = $false - } - $script:formList += @{ - TypeName = $typeName; ObjName = $childName - FormName = $formName; DirName = $dirName - } - $subItemCount++ - } - } - } - } - - if ($script:stopped) { break } - } - - if ($borrowedCount -eq 0) { - Report-OK "9. Borrowed objects: none found" - } elseif ($check9Ok) { - Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" - } - - if ($subItemCount -eq 0) { - Report-OK "10. Sub-items: none found" - } elseif ($check10Ok) { - Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 11: Borrowed form structure --- -$script:borrowedFormsWithTree = @() -$check11Ok = $true -$formCount = 0 - -foreach ($fi in $script:formList) { - $formCount++ - $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName - $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" - $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" - $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" - $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" - - # Validate form metadata XML - if (Test-Path $formMetaFile) { - try { - $fmDoc = New-Object System.Xml.XmlDocument - $fmDoc.PreserveWhitespace = $false - $fmDoc.Load($formMetaFile) - $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) - $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $fmEl = $null - foreach ($c in $fmDoc.DocumentElement.ChildNodes) { - if ($c.NodeType -eq 'Element') { $fmEl = $c; break } - } - if ($fmEl) { - $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) - if ($fmProps) { - $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) - $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" - if ($isBorrowed) { - $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) - if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { - Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" - $check11Ok = $false - } - } - $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) - if ($fmType -and $fmType.InnerText -ne "Managed") { - Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" - $check11Ok = $false - } - } - } - } catch { - Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" - } - } - - # Form.xml must exist - if (-not (Test-Path $formXmlFile)) { - Report-Error "11. ${ctx}: Ext/Form.xml missing" - $check11Ok = $false - continue - } - - # Module.bsl should exist - if (-not (Test-Path $moduleBslFile)) { - Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" - } - - # Read Form.xml as raw text for BaseForm checks - $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) - - if ($formRawText -match ']+version=') { - Report-Warn "11. ${ctx}: missing version attribute" - } - - $script:borrowedFormsWithTree += @{ - Path = $formXmlFile; RawText = $formRawText; Context = $ctx - } - } -} - -if ($formCount -eq 0) { - Report-OK "11. Borrowed forms: none found" -} elseif ($check11Ok) { - $bfCount = $script:borrowedFormsWithTree.Count - Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 12: Form dependency references --- -$platformStyleItems = @{ - "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true - "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true - "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true - "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true - "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true - "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true - "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true - "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true -} -$check12Ok = $true -$depCheckCount = 0 - -foreach ($bf in $script:borrowedFormsWithTree) { - $raw = $bf.RawText - $ctx = $bf.Context - $missingItems = @() - - # CommonPicture references - $cpRefs = @{} - foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { - $cpRefs[$m.Groups[1].Value] = $true - } - $cpIndex = $script:childObjectIndex["CommonPicture"] - foreach ($cpName in $cpRefs.Keys) { - $depCheckCount++ - if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { - $missingItems += "CommonPicture.${cpName}" - } - } - - # StyleItem references - $siRefs = @{} - foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { - $siRefs[$m.Groups[1].Value] = $true - } - $siIndex = $script:childObjectIndex["StyleItem"] - foreach ($siName in $siRefs.Keys) { - $depCheckCount++ - if ($platformStyleItems.ContainsKey($siName)) { continue } - if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { - $missingItems += "StyleItem.${siName}" - } - } - - # Enum DesignTimeRef references - $enumRefs = @{} - foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { - $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" - $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } - } - $eIndex = $script:childObjectIndex["Enum"] - foreach ($entry in $enumRefs.Values) { - $depCheckCount++ - if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { - $missingItems += "Enum.$($entry.Enum)" - } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { - $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" - } - } - - foreach ($mi in $missingItems) { - Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" - $check12Ok = $false - } -} - -if ($script:borrowedFormsWithTree.Count -eq 0) { - Report-OK "12. Form dependencies: no borrowed forms with tree" -} elseif ($check12Ok) { - Report-OK "12. Form dependencies: $depCheckCount references checked" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 13: TypeLink with human-readable paths --- -$check13Ok = $true -$typeLinkCount = 0 - -foreach ($bf in $script:borrowedFormsWithTree) { - $raw = $bf.RawText - $ctx = $bf.Context - $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') - if ($matches.Count -gt 0) { - $typeLinkCount += $matches.Count - Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" - $check13Ok = $false - } -} - -if ($script:borrowedFormsWithTree.Count -eq 0) { - Report-OK "13. TypeLink: no borrowed forms with tree" -} elseif ($check13Ok) { - Report-OK "13. TypeLink: clean" -} - -# --- Final output --- -& $finalize - -if ($script:errors -gt 0) { - exit 1 -} -exit 0 +# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ExtensionPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} + +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { + $ExtensionPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath" + exit 1 + } +} + +if (-not (Test-Path $ExtensionPath)) { + Write-Host "[ERROR] File not found: $ExtensionPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ExtensionPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Extension.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "9cd510cd-abfc-11d4-9434-004095e12fc7", + "9fcd25a0-4822-11d4-9414-008048da11f9", + "e3687481-0a87-462c-a166-9f34594f9bba", + "9de14907-ec23-4a07-96f0-85521cb6b53b", + "51f2d5d8-ea4d-4064-8892-82951750031e", + "e68182ea-4237-4383-967f-90c1e3370bc7", + "fb282519-d103-4dd3-bc12-cb271d631dfc" +) + +# 44 types in canonical order +$childObjectTypes = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# Type -> directory mapping +$childTypeDirMap = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" + "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" + "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" + "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" + "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" + "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" + "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" + "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" + "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "IntegrationService"="IntegrationServices" +} + +# Valid enum values for extension properties +$validEnumValues = @{ + "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Extension (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Extension-specific properties --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # ObjectBelonging = Adopted + $obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns) + if (-not $obNode -or $obNode.InnerText -ne "Adopted") { + Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'" + $check3Ok = $false + } + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # ConfigurationExtensionPurpose + $purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) + $validPurposes = @("Patch","Customization","AddOn") + if (-not $purposeNode -or -not $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose is missing" + $check3Ok = $false + } elseif ($validPurposes -notcontains $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)" + $check3Ok = $false + } + + # NamePrefix + $prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns) + if (-not $prefixNode -or -not $prefixNode.InnerText) { + Report-Warn "3. NamePrefix is empty" + } + + # KeepMappingToExtendedConfigurationObjectsByIDs + $keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns) + if (-not $keepMapNode) { + Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing" + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + + if ($check3Ok) { + $purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + $prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" } + Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Enum property values --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $script:childObjectIndex = @{} + $duplicates = @{} + $typeFirstIndex = @{} + $lastTypeOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } + if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $script:childObjectIndex[$typeName][$objNameVal] = $true + } + + $totalCount++ + } + + $typeCount = $script:childObjectIndex.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- +$script:enumValuesIndex = @{} +$script:formList = @() + +# Helper: check if sub-item has explicit borrowed metadata +function Test-BorrowedSubItem { + param($subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { return $false } + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if ($subOb -and $subOb.InnerText) { return $true } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + return [bool]($subExt -and $subExt.InnerText) +} + +# Helper: validate a borrowed Attribute/EnumValue sub-item +function Validate-BorrowedSubItem { + param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { + Report-Error "${checkNum}. ${context}: ${subType} missing Properties" + return $false + } + $ok = $true + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if (-not $subOb -or $subOb.InnerText -ne "Adopted") { + Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" + $ok = $false + } + $subName = $subProps.SelectSingleNode("md:Name", $nsm) + if (-not $subName -or -not $subName.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType} missing Name" + $ok = $false + } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + if (-not $subExt -or -not $subExt.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" + $ok = $false + } elseif ($subExt.InnerText -notmatch $guidPattern) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" + $ok = $false + } + return $ok +} + +if ($childObjNode) { + $borrowedCount = 0 + $borrowedOk = 0 + $check9Ok = $true + $check10Ok = $true + $subItemCount = 0 + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $childName = $child.InnerText + if ($typeName -eq "Language") { continue } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { continue } + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml" + + if (-not (Test-Path $objFile)) { continue } + + # Parse object XML + $objDoc = $null + try { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $false + $objDoc.Load($objFile) + } catch { + Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)" + continue + } + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the object element (Catalog, Document, etc.) + $objRoot = $objDoc.DocumentElement + $objEl = $null + foreach ($c in $objRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { continue } + + $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) + if (-not $objProps) { continue } + + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) + if ($obNode -and $obNode.InnerText -eq "Adopted") { + $borrowedCount++ + + $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) + if (-not $extObj -or -not $extObj.InnerText) { + Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" + $check9Ok = $false + } elseif ($extObj.InnerText -notmatch $guidPattern) { + Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'" + $check9Ok = $false + } else { + $borrowedOk++ + } + } + + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if ($objChildObjects) { + $ctx = "${typeName}.${childName}" + foreach ($subItem in $objChildObjects.ChildNodes) { + if ($subItem.NodeType -ne 'Element') { continue } + $subType = $subItem.LocalName + + if ($subType -eq "Attribute") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { + $check10Ok = $false + } + } + elseif ($subType -eq "TabularSection") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { + $check10Ok = $false + } else { + # Check InternalInfo GeneratedTypes + $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) + $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + if (-not $tsInfo) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" + $check10Ok = $false + } else { + $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) + $hasTSCat = $false; $hasTSRCat = $false + foreach ($gt in $gtNodes) { + $cat = $gt.GetAttribute("category") + if ($cat -eq "TabularSection") { $hasTSCat = $true } + if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } + } + if (-not $hasTSCat -or -not $hasTSRCat) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" + $check10Ok = $false + } + } + # Recurse into TS ChildObjects/Attribute + $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) + if ($tsChildObjs) { + foreach ($tsAttr in $tsChildObjs.ChildNodes) { + if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } + if (-not (Test-BorrowedSubItem $tsAttr $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { + $check10Ok = $false + } + } + } + } + } + elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { + $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($evName -and $evName.InnerText) { + if (-not $script:enumValuesIndex.ContainsKey($childName)) { + $script:enumValuesIndex[$childName] = @{} + } + $script:enumValuesIndex[$childName][$evName.InnerText] = $true + } + } else { + $check10Ok = $false + } + } + elseif ($subType -eq "Form") { + $formName = $subItem.InnerText + if ($formName) { + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" + if (-not (Test-Path $formMetaFile)) { + Report-Error "10. ${ctx}: Form.${formName} metadata file missing" + $check10Ok = $false + } + $script:formList += @{ + TypeName = $typeName; ObjName = $childName + FormName = $formName; DirName = $dirName + } + $subItemCount++ + } + } + } + } + + if ($script:stopped) { break } + } + + if ($borrowedCount -eq 0) { + Report-OK "9. Borrowed objects: none found" + } elseif ($check9Ok) { + Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" + } + + if ($subItemCount -eq 0) { + Report-OK "10. Sub-items: none found" + } elseif ($check10Ok) { + Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: Borrowed form structure --- +$script:borrowedFormsWithTree = @() +$check11Ok = $true +$formCount = 0 + +foreach ($fi in $script:formList) { + $formCount++ + $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName + $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" + $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" + $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" + $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" + + # Validate form metadata XML + if (Test-Path $formMetaFile) { + try { + $fmDoc = New-Object System.Xml.XmlDocument + $fmDoc.PreserveWhitespace = $false + $fmDoc.Load($formMetaFile) + $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) + $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $fmEl = $null + foreach ($c in $fmDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $fmEl = $c; break } + } + if ($fmEl) { + $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) + if ($fmProps) { + $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) + $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" + if ($isBorrowed) { + $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) + if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { + Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" + $check11Ok = $false + } + } + $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) + if ($fmType -and $fmType.InnerText -ne "Managed") { + Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" + $check11Ok = $false + } + } + } + } catch { + Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" + } + } + + # Form.xml must exist + if (-not (Test-Path $formXmlFile)) { + Report-Error "11. ${ctx}: Ext/Form.xml missing" + $check11Ok = $false + continue + } + + # Module.bsl should exist + if (-not (Test-Path $moduleBslFile)) { + Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" + } + + # Read Form.xml as raw text for BaseForm checks + $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) + + if ($formRawText -match ']+version=') { + Report-Warn "11. ${ctx}: missing version attribute" + } + + $script:borrowedFormsWithTree += @{ + Path = $formXmlFile; RawText = $formRawText; Context = $ctx + } + } +} + +if ($formCount -eq 0) { + Report-OK "11. Borrowed forms: none found" +} elseif ($check11Ok) { + $bfCount = $script:borrowedFormsWithTree.Count + Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Form dependency references --- +$platformStyleItems = @{ + "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true + "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true + "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true + "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true + "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true + "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true + "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true + "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true +} +$check12Ok = $true +$depCheckCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $missingItems = @() + + # CommonPicture references + $cpRefs = @{} + foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { + $cpRefs[$m.Groups[1].Value] = $true + } + $cpIndex = $script:childObjectIndex["CommonPicture"] + foreach ($cpName in $cpRefs.Keys) { + $depCheckCount++ + if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { + $missingItems += "CommonPicture.${cpName}" + } + } + + # StyleItem references + $siRefs = @{} + foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { + $siRefs[$m.Groups[1].Value] = $true + } + $siIndex = $script:childObjectIndex["StyleItem"] + foreach ($siName in $siRefs.Keys) { + $depCheckCount++ + if ($platformStyleItems.ContainsKey($siName)) { continue } + if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { + $missingItems += "StyleItem.${siName}" + } + } + + # Enum DesignTimeRef references + $enumRefs = @{} + foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { + $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" + $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } + } + $eIndex = $script:childObjectIndex["Enum"] + foreach ($entry in $enumRefs.Values) { + $depCheckCount++ + if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { + $missingItems += "Enum.$($entry.Enum)" + } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { + $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" + } + } + + foreach ($mi in $missingItems) { + Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" + $check12Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "12. Form dependencies: no borrowed forms with tree" +} elseif ($check12Ok) { + Report-OK "12. Form dependencies: $depCheckCount references checked" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: TypeLink with human-readable paths --- +$check13Ok = $true +$typeLinkCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') + if ($matches.Count -gt 0) { + $typeLinkCount += $matches.Count + Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" + $check13Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "13. TypeLink: no borrowed forms with tree" +} elseif ($check13Ok) { + Report-OK "13. TypeLink: clean" +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/db-create/SKILL.md b/.claude/skills/db-create/SKILL.md index 21cfda61..82eb3308 100644 --- a/.claude/skills/db-create/SKILL.md +++ b/.claude/skills/db-create/SKILL.md @@ -1,70 +1,70 @@ ---- -name: db-create -description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Glob - - AskUserQuestion ---- - -# /db-create — Создание информационной базы - -Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`. - -## Usage - -``` -/db-create — файловая база по указанному пути -/db-create / — серверная база -/db-create — интерактивно -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -После создания базы предложи зарегистрировать через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Путь к файловой базе | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | -| `-AddToList` | нет | Добавить в список баз 1С | -| `-ListName <имя>` | нет | Имя базы в списке | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## После создания - -Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`) -3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона - -## Примеры - -```powershell -# Создать файловую базу -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" - -# Создать серверную базу -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" - -# Создать из шаблона CF -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" - -# Создать и добавить в список баз -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" -``` +--- +name: db-create +description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob + - AskUserQuestion +--- + +# /db-create — Создание информационной базы + +Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`. + +## Usage + +``` +/db-create — файловая база по указанному пути +/db-create / — серверная база +/db-create — интерактивно +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +После создания базы предложи зарегистрировать через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Путь к файловой базе | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | +| `-AddToList` | нет | Добавить в список баз 1С | +| `-ListName <имя>` | нет | Имя базы в списке | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## После создания + +Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`) +3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона + +## Примеры + +```powershell +# Создать файловую базу +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" + +# Создать серверную базу +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" + +# Создать из шаблона CF +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" + +# Создать и добавить в список баз +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" +``` diff --git a/.claude/skills/db-create/scripts/db-create.ps1 b/.claude/skills/db-create/scripts/db-create.ps1 index 7911a670..a18ef69d 100644 --- a/.claude/skills/db-create/scripts/db-create.ps1 +++ b/.claude/skills/db-create/scripts/db-create.ps1 @@ -1,220 +1,220 @@ -# db-create v1.4 — Create 1C information base -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Создание информационной базы 1С - -.DESCRIPTION - Создаёт новую информационную базу 1С (файловую или серверную). - Поддерживает создание из шаблона и добавление в список баз. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UseTemplate - Путь к файлу шаблона (.cf или .dt) - -.PARAMETER AddToList - Добавить в список баз 1С - -.PARAMETER ListName - Имя базы в списке - -.EXAMPLE - .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" - -.EXAMPLE - .\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" - -.EXAMPLE - .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UseTemplate, - - [Parameter(Mandatory=$false)] - [switch]$AddToList, - - [Parameter(Mandatory=$false)] - [string]$ListName -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Validate template --- -if ($UseTemplate -and -not (Test-Path $UseTemplate)) { - Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red - exit 1 -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only) --- - $arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database") - if ($UseTemplate) { - if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") { - $arguments += "--restore=$UseTemplate" - } else { - $arguments += "--load=$UseTemplate", "--apply" - } - } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green - } else { - Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("CREATEINFOBASE") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" - } else { - $arguments += "File=`"$InfoBasePath`"" - } - - # --- Template --- - if ($UseTemplate) { - $arguments += "/UseTemplate", "`"$UseTemplate`"" - } - - # --- Add to list --- - if ($AddToList) { - if ($ListName) { - $arguments += "/AddToList", "`"$ListName`"" - } else { - $arguments += "/AddToList" - } - } - - # --- Output --- - $outFile = Join-Path $tempDir "create_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - if ($InfoBaseServer -and $InfoBaseRef) { - Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green - } else { - Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green - } - } else { - Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-create v1.4 — Create 1C information base +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Создание информационной базы 1С + +.DESCRIPTION + Создаёт новую информационную базу 1С (файловую или серверную). + Поддерживает создание из шаблона и добавление в список баз. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UseTemplate + Путь к файлу шаблона (.cf или .dt) + +.PARAMETER AddToList + Добавить в список баз 1С + +.PARAMETER ListName + Имя базы в списке + +.EXAMPLE + .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" + +.EXAMPLE + .\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" + +.EXAMPLE + .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UseTemplate, + + [Parameter(Mandatory=$false)] + [switch]$AddToList, + + [Parameter(Mandatory=$false)] + [string]$ListName +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate template --- +if ($UseTemplate -and -not (Test-Path $UseTemplate)) { + Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only) --- + $arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database") + if ($UseTemplate) { + if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") { + $arguments += "--restore=$UseTemplate" + } else { + $arguments += "--load=$UseTemplate", "--apply" + } + } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green + } else { + Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("CREATEINFOBASE") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" + } else { + $arguments += "File=`"$InfoBasePath`"" + } + + # --- Template --- + if ($UseTemplate) { + $arguments += "/UseTemplate", "`"$UseTemplate`"" + } + + # --- Add to list --- + if ($AddToList) { + if ($ListName) { + $arguments += "/AddToList", "`"$ListName`"" + } else { + $arguments += "/AddToList" + } + } + + # --- Output --- + $outFile = Join-Path $tempDir "create_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + if ($InfoBaseServer -and $InfoBaseRef) { + Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green + } else { + Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green + } + } else { + Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-dump-cf/SKILL.md b/.claude/skills/db-dump-cf/SKILL.md index faf43e76..a7c8572e 100644 --- a/.claude/skills/db-dump-cf/SKILL.md +++ b/.claude/skills/db-dump-cf/SKILL.md @@ -1,68 +1,68 @@ ---- -name: db-dump-cf -description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF -argument-hint: "[database] [output.cf]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-dump-cf — Выгрузка конфигурации в CF-файл - -Выгружает конфигурацию информационной базы в бинарный CF-файл. - -## Usage - -``` -/db-dump-cf [database] [output.cf] -/db-dump-cf dev config.cf -/db-dump-cf — база по умолчанию, файл config.cf -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-OutputFile <путь>` | да | Путь к выходному CF-файлу | -| `-Extension <имя>` | нет | Выгрузить расширение | -| `-AllExtensions` | нет | Выгрузить все расширения | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## Примеры - -```powershell -# Выгрузка конфигурации (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" - -# Выгрузка расширения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" -``` +--- +name: db-dump-cf +description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF +argument-hint: "[database] [output.cf]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-dump-cf — Выгрузка конфигурации в CF-файл + +Выгружает конфигурацию информационной базы в бинарный CF-файл. + +## Usage + +``` +/db-dump-cf [database] [output.cf] +/db-dump-cf dev config.cf +/db-dump-cf — база по умолчанию, файл config.cf +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-OutputFile <путь>` | да | Путь к выходному CF-файлу | +| `-Extension <имя>` | нет | Выгрузить расширение | +| `-AllExtensions` | нет | Выгрузить все расширения | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Примеры + +```powershell +# Выгрузка конфигурации (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" + +# Выгрузка расширения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" +``` diff --git a/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 b/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 index 08e87ae5..7c9d29b4 100644 --- a/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 +++ b/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 @@ -1,224 +1,224 @@ -# db-dump-cf v1.4 — Dump 1C configuration to CF file -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Выгрузка конфигурации 1С в CF-файл - -.DESCRIPTION - Выгружает конфигурацию информационной базы в бинарный CF-файл. - Поддерживает выгрузку расширений. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER OutputFile - Путь к выходному CF-файлу - -.PARAMETER Extension - Имя расширения для выгрузки - -.PARAMETER AllExtensions - Выгрузить все расширения - -.EXAMPLE - .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" - -.EXAMPLE - .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$OutputFile, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Ensure output directory exists --- -$outDir = Split-Path $OutputFile -Parent -if ($outDir -and -not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only) --- - if ($AllExtensions) { - Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red - exit 1 - } - $arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath") - if ($Extension) { $arguments += "--extension=$Extension" } - $arguments += "$OutputFile" - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green - } else { - Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/DumpCfg", "`"$OutputFile`"" - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- Output --- - $outFile = Join-Path $tempDir "dump_cf_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green - } else { - Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-dump-cf v1.4 — Dump 1C configuration to CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Выгрузка конфигурации 1С в CF-файл + +.DESCRIPTION + Выгружает конфигурацию информационной базы в бинарный CF-файл. + Поддерживает выгрузку расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER OutputFile + Путь к выходному CF-файлу + +.PARAMETER Extension + Имя расширения для выгрузки + +.PARAMETER AllExtensions + Выгрузить все расширения + +.EXAMPLE + .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" + +.EXAMPLE + .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$OutputFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +$outDir = Split-Path $OutputFile -Parent +if ($outDir -and -not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only) --- + if ($AllExtensions) { + Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red + exit 1 + } + $arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath") + if ($Extension) { $arguments += "--extension=$Extension" } + $arguments += "$OutputFile" + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpCfg", "`"$OutputFile`"" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "dump_cf_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-dump-dt/SKILL.md b/.claude/skills/db-dump-dt/SKILL.md index 3bb393ad..41793e6e 100644 --- a/.claude/skills/db-dump-dt/SKILL.md +++ b/.claude/skills/db-dump-dt/SKILL.md @@ -1,72 +1,72 @@ ---- -name: db-dump-dt -description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt -argument-hint: "[database] [output.dt]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-dump-dt — Выгрузка информационной базы в DT-файл - -Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ. - -> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные, -> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных. - -## Usage - -``` -/db-dump-dt [database] [output.dt] -/db-dump-dt dev backup.dt -/db-dump-dt — база по умолчанию, имя файла по базе и дате -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-OutputFile <путь>` | да | Путь к выходному DT-файлу | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## Примеры - -```powershell -# Выгрузка ИБ (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" -``` - -## Связанные навыки - -- `/db-load-dt` — загрузка ИБ из DT (обратная операция) -- `/db-dump-cf` — выгрузка только конфигурации (без данных) -- `/db-create` — создать новую базу (в т.ч. из DT-шаблона) +--- +name: db-dump-dt +description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt +argument-hint: "[database] [output.dt]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-dump-dt — Выгрузка информационной базы в DT-файл + +Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ. + +> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные, +> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных. + +## Usage + +``` +/db-dump-dt [database] [output.dt] +/db-dump-dt dev backup.dt +/db-dump-dt — база по умолчанию, имя файла по базе и дате +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-OutputFile <путь>` | да | Путь к выходному DT-файлу | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Примеры + +```powershell +# Выгрузка ИБ (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" +``` + +## Связанные навыки + +- `/db-load-dt` — загрузка ИБ из DT (обратная операция) +- `/db-dump-cf` — выгрузка только конфигурации (без данных) +- `/db-create` — создать новую базу (в т.ч. из DT-шаблона) diff --git a/.claude/skills/db-dump-xml/SKILL.md b/.claude/skills/db-dump-xml/SKILL.md index 69e3ba04..0aa131d7 100644 --- a/.claude/skills/db-dump-xml/SKILL.md +++ b/.claude/skills/db-dump-xml/SKILL.md @@ -1,90 +1,90 @@ ---- -name: db-dump-xml -description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles -argument-hint: "[database] [outputDir]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-dump-xml — Выгрузка конфигурации в XML - -Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo. - -## Usage - -``` -/db-dump-xml [database] [outputDir] -/db-dump-xml dev src/config -/db-dump-xml dev src/config -Mode Full -/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. -Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-ConfigDir <путь>` | да | Каталог для выгрузки | -| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` | -| `-Objects <список>` | для Partial | Имена объектов через запятую | -| `-Extension <имя>` | нет | Выгрузить расширение | -| `-AllExtensions` | нет | Выгрузить все расширения | -| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -### Режимы выгрузки - -| Режим | Описание | -|-------|----------| -| `Full` | Полная выгрузка — все объекты конфигурации | -| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) | -| `Partial` | Частичная — выбранные объекты из параметра `-Objects` | -| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов | - -> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`. - -## Примеры - -```powershell -# Полная выгрузка (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full - -# Инкрементальная выгрузка -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes - -# Частичная выгрузка -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full - -# Выгрузка расширения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" -``` +--- +name: db-dump-xml +description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles +argument-hint: "[database] [outputDir]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-dump-xml — Выгрузка конфигурации в XML + +Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo. + +## Usage + +``` +/db-dump-xml [database] [outputDir] +/db-dump-xml dev src/config +/db-dump-xml dev src/config -Mode Full +/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог для выгрузки | +| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` | +| `-Objects <список>` | для Partial | Имена объектов через запятую | +| `-Extension <имя>` | нет | Выгрузить расширение | +| `-AllExtensions` | нет | Выгрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Режимы выгрузки + +| Режим | Описание | +|-------|----------| +| `Full` | Полная выгрузка — все объекты конфигурации | +| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) | +| `Partial` | Частичная — выбранные объекты из параметра `-Objects` | +| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов | + +> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`. + +## Примеры + +```powershell +# Полная выгрузка (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Инкрементальная выгрузка +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes + +# Частичная выгрузка +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Выгрузка расширения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" +``` diff --git a/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 b/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 index 16c60fee..5217c968 100644 --- a/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 +++ b/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 @@ -1,294 +1,294 @@ -# db-dump-xml v1.6 — Dump 1C configuration to XML files -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Выгрузка конфигурации 1С в XML-файлы - -.DESCRIPTION - Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: - - Full: полная выгрузка всей конфигурации - - Changes: инкрементальная выгрузка изменённых объектов - - Partial: выгрузка конкретных объектов из списка - - UpdateInfo: обновление только ConfigDumpInfo.xml - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER ConfigDir - Каталог для выгрузки конфигурации - -.PARAMETER Mode - Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) - -.PARAMETER Objects - Имена объектов метаданных через запятую (для режима Partial) - -.PARAMETER Extension - Имя расширения для выгрузки - -.PARAMETER AllExtensions - Выгрузить все расширения - -.PARAMETER Format - Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) - -.EXAMPLE - .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full - -.EXAMPLE - .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$ConfigDir, - - [Parameter(Mandatory=$false)] - [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] - [string]$Mode = "Changes", - - [Parameter(Mandatory=$false)] - [string]$Objects, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions, - - [Parameter(Mandatory=$false)] - [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical" -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Validate Partial mode --- -if ($Mode -eq "Partial" -and -not $Objects) { - Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red - exit 1 -} - -# --- Create output dir if needed --- -if (-not (Test-Path $ConfigDir)) { - New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null - Write-Host "Created output directory: $ConfigDir" -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only; hierarchical Full/Changes) --- - if ($Format -eq "Plain") { - Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red - exit 1 - } - if ($AllExtensions) { - $arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath") - } elseif ($Mode -eq "UpdateInfo") { - Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red - exit 1 - } elseif ($Mode -eq "Partial") { - $objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $arguments = @("infobase", "config", "export", "objects") + $objList - $arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath" - if ($Extension) { $arguments += "--extension=$Extension" } - } else { - $arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath") - if ($Extension) { $arguments += "--extension=$Extension" } - $arguments += "$ConfigDir" - } - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green - } else { - Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" - $arguments += "-Format", $Format - - switch ($Mode) { - "Full" { - Write-Host "Executing full configuration dump..." - } - "Changes" { - Write-Host "Executing incremental configuration dump..." - $arguments += "-update" - $arguments += "-force" - } - "Partial" { - Write-Host "Executing partial configuration dump..." - $objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - - $listFile = Join-Path $tempDir "dump_list.txt" - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom) - - $arguments += "-listFile", "`"$listFile`"" - Write-Host "Objects to dump: $($objectList.Count)" - foreach ($obj in $objectList) { Write-Host " $obj" } - } - "UpdateInfo" { - Write-Host "Updating ConfigDumpInfo.xml..." - $arguments += "-configDumpInfoOnly" - } - } - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- Output --- - $outFile = Join-Path $tempDir "dump_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Dump completed successfully" -ForegroundColor Green - Write-Host "Configuration dumped to: $ConfigDir" - } else { - Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-dump-xml v1.6 — Dump 1C configuration to XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Выгрузка конфигурации 1С в XML-файлы + +.DESCRIPTION + Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: + - Full: полная выгрузка всей конфигурации + - Changes: инкрементальная выгрузка изменённых объектов + - Partial: выгрузка конкретных объектов из списка + - UpdateInfo: обновление только ConfigDumpInfo.xml + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог для выгрузки конфигурации + +.PARAMETER Mode + Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) + +.PARAMETER Objects + Имена объектов метаданных через запятую (для режима Partial) + +.PARAMETER Extension + Имя расширения для выгрузки + +.PARAMETER AllExtensions + Выгрузить все расширения + +.PARAMETER Format + Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full + +.EXAMPLE + .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] + [string]$Mode = "Changes", + + [Parameter(Mandatory=$false)] + [string]$Objects, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical" +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate Partial mode --- +if ($Mode -eq "Partial" -and -not $Objects) { + Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red + exit 1 +} + +# --- Create output dir if needed --- +if (-not (Test-Path $ConfigDir)) { + New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null + Write-Host "Created output directory: $ConfigDir" +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only; hierarchical Full/Changes) --- + if ($Format -eq "Plain") { + Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red + exit 1 + } + if ($AllExtensions) { + $arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath") + } elseif ($Mode -eq "UpdateInfo") { + Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red + exit 1 + } elseif ($Mode -eq "Partial") { + $objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $arguments = @("infobase", "config", "export", "objects") + $objList + $arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath" + if ($Extension) { $arguments += "--extension=$Extension" } + } else { + $arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath") + if ($Extension) { $arguments += "--extension=$Extension" } + $arguments += "$ConfigDir" + } + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green + } else { + Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" + $arguments += "-Format", $Format + + switch ($Mode) { + "Full" { + Write-Host "Executing full configuration dump..." + } + "Changes" { + Write-Host "Executing incremental configuration dump..." + $arguments += "-update" + $arguments += "-force" + } + "Partial" { + Write-Host "Executing partial configuration dump..." + $objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + $listFile = Join-Path $tempDir "dump_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom) + + $arguments += "-listFile", "`"$listFile`"" + Write-Host "Objects to dump: $($objectList.Count)" + foreach ($obj in $objectList) { Write-Host " $obj" } + } + "UpdateInfo" { + Write-Host "Updating ConfigDumpInfo.xml..." + $arguments += "-configDumpInfoOnly" + } + } + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "dump_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Dump completed successfully" -ForegroundColor Green + Write-Host "Configuration dumped to: $ConfigDir" + } else { + Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-list/SKILL.md b/.claude/skills/db-list/SKILL.md index 41e0cb26..b2e6cdbf 100644 --- a/.claude/skills/db-list/SKILL.md +++ b/.claude/skills/db-list/SKILL.md @@ -1,158 +1,158 @@ ---- -name: db-list -description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть -argument-hint: "[add|remove|show]" -allowed-tools: - - Read - - Write - - Glob - - AskUserQuestion ---- - -# /db-list — Управление реестром баз данных - -Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git. - -## Usage - -``` -/db-list — показать список баз -/db-list add — добавить базу (интерактивно) -/db-list remove — удалить базу из реестра -/db-list show — подробности по базе -``` - -## Формат `.v8-project.json` - -Файл размещается в корне проекта (рядом с `.git/`). - -```json -{ - "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", - "databases": [ - { - "id": "dev", - "name": "Разработка", - "type": "file", - "path": "C:\\Bases\\MyApp_Dev", - "user": "Admin", - "password": "", - "aliases": ["dev", "разработка"], - "branches": ["dev", "develop", "feature/*"], - "configSrc": "C:\\WS\\myapp\\cfsrc" - }, - { - "id": "test", - "name": "Тестовая", - "type": "server", - "server": "srv01", - "ref": "MyApp_Test", - "user": "Admin", - "password": "123", - "aliases": ["test", "тест"] - } - ], - "default": "dev" -} -``` - -### Поля корневого объекта - -| Поле | Тип | Описание | -|------|-----|----------| -| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | -| `databases` | array | Массив баз данных | -| `default` | string | id базы по умолчанию | - -### Поля объекта базы данных - -| Поле | Тип | Обязательное | Описание | -|------|-----|:------------:|----------| -| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) | -| `name` | string | да | Человекочитаемое имя | -| `type` | `"file"` / `"server"` | да | Тип подключения | -| `path` | string | для file | Путь к каталогу файловой базы | -| `server` | string | для server | Адрес сервера 1С | -| `ref` | string | для server | Имя базы на сервере | -| `user` | string | нет | Имя пользователя 1С | -| `password` | string | нет | Пароль | -| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | -| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | -| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | - -## Алгоритм разрешения базы данных - -Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы. - -1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую -2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: - 1. По `id` (точное совпадение) - 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») - 3. По `name` (нечёткое совпадение с учётом морфологии и регистра) -3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: - - Точное совпадение: ветка `dev` → `"branches": ["dev"]` - - Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]` -4. Если ветка не совпала — используй `default` -5. Если не найдено или неоднозначно — спроси пользователя -6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл - -После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. - -### Автоопределение платформы - -Если `v8path` не задан в конфиге: - -```powershell -$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 -``` - -## Операции - -### Показать список баз - -Прочитай `.v8-project.json`, выведи таблицу: - -``` -ID Имя Тип Путь/Сервер По умолч. -dev Разработка file C:\Bases\MyApp_Dev ✓ -test Тестовая server srv01/MyApp_Test -``` - -### Добавить базу - -Спроси у пользователя через AskUserQuestion: -- id, name, type (file/server) -- path (для file) или server + ref (для server) -- user, password (необязательно) -- aliases, branches (необязательно) - -Добавь в массив `databases`. Если это первая база — установи как `default`. - -### Удалить базу - -Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default. - -### Подробности по базе - -Выведи все поля конкретной базы. - -## Формирование строки подключения - -Для использования в шаблонах команд других навыков: - -**Файловая база:** -``` -/F "" -``` - -**Серверная база:** -``` -/S "/" -``` - -**Аутентификация** (добавляется если user задан): -``` -/N"" /P"" -``` - -> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком. +--- +name: db-list +description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть +argument-hint: "[add|remove|show]" +allowed-tools: + - Read + - Write + - Glob + - AskUserQuestion +--- + +# /db-list — Управление реестром баз данных + +Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git. + +## Usage + +``` +/db-list — показать список баз +/db-list add — добавить базу (интерактивно) +/db-list remove — удалить базу из реестра +/db-list show — подробности по базе +``` + +## Формат `.v8-project.json` + +Файл размещается в корне проекта (рядом с `.git/`). + +```json +{ + "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", + "databases": [ + { + "id": "dev", + "name": "Разработка", + "type": "file", + "path": "C:\\Bases\\MyApp_Dev", + "user": "Admin", + "password": "", + "aliases": ["dev", "разработка"], + "branches": ["dev", "develop", "feature/*"], + "configSrc": "C:\\WS\\myapp\\cfsrc" + }, + { + "id": "test", + "name": "Тестовая", + "type": "server", + "server": "srv01", + "ref": "MyApp_Test", + "user": "Admin", + "password": "123", + "aliases": ["test", "тест"] + } + ], + "default": "dev" +} +``` + +### Поля корневого объекта + +| Поле | Тип | Описание | +|------|-----|----------| +| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | +| `databases` | array | Массив баз данных | +| `default` | string | id базы по умолчанию | + +### Поля объекта базы данных + +| Поле | Тип | Обязательное | Описание | +|------|-----|:------------:|----------| +| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) | +| `name` | string | да | Человекочитаемое имя | +| `type` | `"file"` / `"server"` | да | Тип подключения | +| `path` | string | для file | Путь к каталогу файловой базы | +| `server` | string | для server | Адрес сервера 1С | +| `ref` | string | для server | Имя базы на сервере | +| `user` | string | нет | Имя пользователя 1С | +| `password` | string | нет | Пароль | +| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | +| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | +| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | + +## Алгоритм разрешения базы данных + +Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы. + +1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую +2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: + 1. По `id` (точное совпадение) + 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») + 3. По `name` (нечёткое совпадение с учётом морфологии и регистра) +3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: + - Точное совпадение: ветка `dev` → `"branches": ["dev"]` + - Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]` +4. Если ветка не совпала — используй `default` +5. Если не найдено или неоднозначно — спроси пользователя +6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл + +После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. + +### Автоопределение платформы + +Если `v8path` не задан в конфиге: + +```powershell +$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 +``` + +## Операции + +### Показать список баз + +Прочитай `.v8-project.json`, выведи таблицу: + +``` +ID Имя Тип Путь/Сервер По умолч. +dev Разработка file C:\Bases\MyApp_Dev ✓ +test Тестовая server srv01/MyApp_Test +``` + +### Добавить базу + +Спроси у пользователя через AskUserQuestion: +- id, name, type (file/server) +- path (для file) или server + ref (для server) +- user, password (необязательно) +- aliases, branches (необязательно) + +Добавь в массив `databases`. Если это первая база — установи как `default`. + +### Удалить базу + +Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default. + +### Подробности по базе + +Выведи все поля конкретной базы. + +## Формирование строки подключения + +Для использования в шаблонах команд других навыков: + +**Файловая база:** +``` +/F "" +``` + +**Серверная база:** +``` +/S "/" +``` + +**Аутентификация** (добавляется если user задан): +``` +/N"" /P"" +``` + +> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком. diff --git a/.claude/skills/db-load-cf/SKILL.md b/.claude/skills/db-load-cf/SKILL.md index e989979c..97e94dd1 100644 --- a/.claude/skills/db-load-cf/SKILL.md +++ b/.claude/skills/db-load-cf/SKILL.md @@ -1,73 +1,73 @@ ---- -name: db-load-cf -description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF -argument-hint: [database] -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-load-cf — Загрузка конфигурации из CF-файла - -Загружает конфигурацию из бинарного CF-файла в информационную базу. - -## Usage - -``` -/db-load-cf [database] -/db-load-cf config.cf dev -``` - -> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя. - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-InputFile <путь>` | да | Путь к CF-файлу | -| `-Extension <имя>` | нет | Загрузить как расширение | -| `-AllExtensions` | нет | Загрузить все расширения из архива | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## После выполнения - -**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` - -## Примеры - -```powershell -# Файловая база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" - -# Загрузка расширения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" -``` +--- +name: db-load-cf +description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF +argument-hint: [database] +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-cf — Загрузка конфигурации из CF-файла + +Загружает конфигурацию из бинарного CF-файла в информационную базу. + +## Usage + +``` +/db-load-cf [database] +/db-load-cf config.cf dev +``` + +> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к CF-файлу | +| `-Extension <имя>` | нет | Загрузить как расширение | +| `-AllExtensions` | нет | Загрузить все расширения из архива | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## После выполнения + +**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` + +## Примеры + +```powershell +# Файловая база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" + +# Загрузка расширения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" +``` diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 index e0a32f84..a993073a 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 @@ -1,224 +1,224 @@ -# db-load-cf v1.4 — Load 1C configuration from CF file -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Загрузка конфигурации 1С из CF-файла - -.DESCRIPTION - Загружает конфигурацию из бинарного CF-файла в информационную базу. - Поддерживает загрузку расширений. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER InputFile - Путь к CF-файлу для загрузки - -.PARAMETER Extension - Загрузить как расширение - -.PARAMETER AllExtensions - Загрузить все расширения из архива - -.EXAMPLE - .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" - -.EXAMPLE - .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$InputFile, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Validate input file --- -if (-not (Test-Path $InputFile)) { - Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red - exit 1 -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only) --- - if ($AllExtensions) { - Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red - exit 1 - } - $arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath") - if ($Extension) { $arguments += "--extension=$Extension" } - $arguments += "$InputFile" - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green - } else { - Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/LoadCfg", "`"$InputFile`"" - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- Output --- - $outFile = Join-Path $tempDir "load_cf_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green - } else { - Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-load-cf v1.4 — Load 1C configuration from CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка конфигурации 1С из CF-файла + +.DESCRIPTION + Загружает конфигурацию из бинарного CF-файла в информационную базу. + Поддерживает загрузку расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER InputFile + Путь к CF-файлу для загрузки + +.PARAMETER Extension + Загрузить как расширение + +.PARAMETER AllExtensions + Загрузить все расширения из архива + +.EXAMPLE + .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" + +.EXAMPLE + .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$InputFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate input file --- +if (-not (Test-Path $InputFile)) { + Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only) --- + if ($AllExtensions) { + Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red + exit 1 + } + $arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath") + if ($Extension) { $arguments += "--extension=$Extension" } + $arguments += "$InputFile" + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadCfg", "`"$InputFile`"" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_cf_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-load-dt/SKILL.md b/.claude/skills/db-load-dt/SKILL.md index 9cd07565..be0c58b8 100644 --- a/.claude/skills/db-load-dt/SKILL.md +++ b/.claude/skills/db-load-dt/SKILL.md @@ -1,92 +1,92 @@ ---- -name: db-load-dt -description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt -disable-model-invocation: true -argument-hint: [database] -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-load-dt — Загрузка информационной базы из DT-файла - -Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла. - -> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и -> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки -> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка. - -## Когда НЕ использовать - -- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку - в существующую. -- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`. - -## Usage - -``` -/db-load-dt [database] -/db-load-dt backup.dt dev -``` - -## Порядок действий перед загрузкой - -1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка - отката (восстановиться будет нечем, если не сохранить). -2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана. -3. Только после подтверждения выполняй загрузку. - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-InputFile <путь>` | да | Путь к DT-файлу | -| `-JobsCount ` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) | -| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## После выполнения - -Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно -передать `-UnlockCode`; иначе освободи базу и повтори. - -## Примеры - -```powershell -# Файловая база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" - -# Серверная база с ускорением загрузки -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 -``` - -## Связанные навыки - -- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой) -- `/db-create` — создать новую базу (в т.ч. из DT-шаблона) +--- +name: db-load-dt +description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt +disable-model-invocation: true +argument-hint: [database] +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-dt — Загрузка информационной базы из DT-файла + +Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла. + +> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и +> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки +> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка. + +## Когда НЕ использовать + +- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку + в существующую. +- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`. + +## Usage + +``` +/db-load-dt [database] +/db-load-dt backup.dt dev +``` + +## Порядок действий перед загрузкой + +1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка + отката (восстановиться будет нечем, если не сохранить). +2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана. +3. Только после подтверждения выполняй загрузку. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к DT-файлу | +| `-JobsCount ` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) | +| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## После выполнения + +Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно +передать `-UnlockCode`; иначе освободи базу и повтори. + +## Примеры + +```powershell +# Файловая база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" + +# Серверная база с ускорением загрузки +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 +``` + +## Связанные навыки + +- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой) +- `/db-create` — создать новую базу (в т.ч. из DT-шаблона) diff --git a/.claude/skills/db-load-git/SKILL.md b/.claude/skills/db-load-git/SKILL.md index 2915f308..fd273dcf 100644 --- a/.claude/skills/db-load-git/SKILL.md +++ b/.claude/skills/db-load-git/SKILL.md @@ -1,77 +1,77 @@ ---- -name: db-load-git -description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита -argument-hint: "[database] [source]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-load-git — Загрузка изменений из Git - -Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу. - -## Usage - -``` -/db-load-git [database] -/db-load-git dev — все незафиксированные изменения -/db-load-git dev -Source Staged — только staged -/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD" -/db-load-git dev -DryRun — только показать что будет загружено -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. -Если в записи базы указан `configSrc` — используй как каталог конфигурации. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) | -| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | -| `-CommitRange ` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | -| `-Extension <имя>` | нет | Загрузить в расширение | -| `-AllExtensions` | нет | Загрузить все расширения | -| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | -| `-DryRun` | нет | Только показать что будет загружено (без загрузки) | -| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## После выполнения - -Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД - -## Примеры - -```powershell -# Все незафиксированные изменения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB - -# Из диапазона коммитов -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" -``` +--- +name: db-load-git +description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита +argument-hint: "[database] [source]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-git — Загрузка изменений из Git + +Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу. + +## Usage + +``` +/db-load-git [database] +/db-load-git dev — все незафиксированные изменения +/db-load-git dev -Source Staged — только staged +/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD" +/db-load-git dev -DryRun — только показать что будет загружено +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог конфигурации. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) | +| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | +| `-CommitRange ` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | +| `-Extension <имя>` | нет | Загрузить в расширение | +| `-AllExtensions` | нет | Загрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-DryRun` | нет | Только показать что будет загружено (без загрузки) | +| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## После выполнения + +Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД + +## Примеры + +```powershell +# Все незафиксированные изменения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB + +# Из диапазона коммитов +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" +``` diff --git a/.claude/skills/db-load-git/scripts/db-load-git.ps1 b/.claude/skills/db-load-git/scripts/db-load-git.ps1 index f9deb2db..811c6564 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.ps1 +++ b/.claude/skills/db-load-git/scripts/db-load-git.ps1 @@ -1,445 +1,445 @@ -# db-load-git v1.8 — Load Git changes into 1C database -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Загрузка изменений из Git в базу 1С - -.DESCRIPTION - Определяет изменённые файлы конфигурации по данным Git и выполняет - частичную загрузку в информационную базу. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER ConfigDir - Каталог XML-выгрузки конфигурации (git-репозиторий) - -.PARAMETER Source - Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All) - -.PARAMETER CommitRange - Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD - -.PARAMETER Extension - Имя расширения для загрузки - -.PARAMETER AllExtensions - Загрузить все расширения - -.PARAMETER Format - Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) - -.PARAMETER DryRun - Только показать что будет загружено (без загрузки) - -.EXAMPLE - .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All - -.EXAMPLE - .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD" - -.EXAMPLE - .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$ConfigDir, - - [Parameter(Mandatory=$false)] - [ValidateSet("All", "Staged", "Unstaged", "Commit")] - [string]$Source = "All", - - [Parameter(Mandatory=$false)] - [string]$CommitRange, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions, - - [Parameter(Mandatory=$false)] - [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical", - - [Parameter(Mandatory=$false)] - [switch]$DryRun, - - [Parameter(Mandatory=$false)] - [switch]$UpdateDB -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML --- -function Get-ObjectXmlFromSubFile { - param([string]$RelativePath) - - $parts = $RelativePath -split '[\\/]' - if ($parts.Count -ge 2) { - return "$($parts[0])/$($parts[1]).xml" - } - return $null -} - -# --- Resolve V8Path (skip if DryRun) --- -if (-not $DryRun) { - function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null - } - - if (-not $V8Path) { - $V8Path = Find-ProjectV8Path - } - if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } - } - if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" - } - - if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 - } -} - -# --- Detect engine + validate connection (skip if DryRun) --- -$engine = "1cv8" -if (-not $DryRun) { - $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } - } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 - } -} - -# --- Validate config dir --- -if (-not (Test-Path $ConfigDir)) { - Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red - exit 1 -} - -# --- Validate Commit mode --- -if ($Source -eq "Commit" -and -not $CommitRange) { - Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red - exit 1 -} - -# --- Check git --- -try { - $null = git --version 2>&1 -} catch { - Write-Host "Error: git not found in PATH" -ForegroundColor Red - exit 1 -} - -# --- Get changed files from Git --- -$changedFiles = @() -$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') -$configDirNormalized = $ConfigDir.Replace('\', '/') - -Push-Location $ConfigDir -try { - switch ($Source) { - "Staged" { - Write-Host "Getting staged changes..." - $raw = git diff --cached --name-only --relative 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - } - "Unstaged" { - Write-Host "Getting unstaged changes..." - $raw = git diff --name-only --relative 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - $raw = git ls-files --others --exclude-standard 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - } - "Commit" { - Write-Host "Getting changes from $CommitRange..." - $raw = git diff --name-only --relative $CommitRange 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - } - "All" { - Write-Host "Getting all uncommitted changes..." - $raw = git diff --cached --name-only --relative 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - $raw = git diff --name-only --relative 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - $raw = git ls-files --others --exclude-standard 2>&1 - if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } - } - } -} finally { - Pop-Location -} - -$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique - -if ($changedFiles.Count -eq 0) { - Write-Host "No changes found" - exit 0 -} - -Write-Host "Git changes detected: $($changedFiles.Count) files" - -# --- Filter and map to config files --- -$configFiles = @() -$supportSkipped = @() - -foreach ($file in $changedFiles) { - $file = $file.Trim().Replace('\', '/') - if ([string]::IsNullOrWhiteSpace($file)) { continue } - - # Skip service files (not partially loadable). Support-state files are tracked - # to warn the user: support changes apply only via a full load. - if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue } - if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue } - - $fullPath = Join-Path $ConfigDir $file - - if ($file -match '\.xml$') { - # XML file — add directly if exists - if (Test-Path $fullPath) { - if ($configFiles -notcontains $file) { - $configFiles += $file - } - } - } - else { - # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files - $objectXml = Get-ObjectXmlFromSubFile -RelativePath $file - if ($objectXml) { - $fullXmlPath = Join-Path $ConfigDir $objectXml - if (Test-Path $fullXmlPath) { - if ($configFiles -notcontains $objectXml) { - $configFiles += $objectXml - } - if ((Test-Path $fullPath) -and $configFiles -notcontains $file) { - $configFiles += $file - } - - # Add all files from Ext/ directory of the object - $parts = $file -split '[\\/]' - if ($parts.Count -ge 2) { - $extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext" - if (Test-Path $extDir) { - Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object { - $extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/') - if ($configFiles -notcontains $extRelPath) { - $configFiles += $extRelPath - } - } - } - } - } - } - } -} - -if ($supportSkipped.Count -gt 0) { - Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow - foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow } - Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow -} - -if ($configFiles.Count -eq 0) { - Write-Host "No configuration files found in changes" - exit 0 -} - -Write-Host "Files for loading: $($configFiles.Count)" -foreach ($f in $configFiles) { Write-Host " $f" } - -# --- DryRun: stop here --- -if ($DryRun) { - Write-Host "" - Write-Host "DryRun mode - no changes applied" - exit 0 -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only; import specific files) --- - if ($Format -eq "Plain") { - Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red - exit 1 - } - if ($AllExtensions) { - Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red - exit 1 - } - $arguments = @("infobase", "config", "import", "files") + $configFiles - $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath" - if ($Extension) { $arguments += "--extension=$Extension" } - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green - if ($output) { Write-Host ($output | Out-String) } - if ($UpdateDB) { - $applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") - if ($UserName) { $applyArgs += "--user=$UserName" } - if ($Password) { $applyArgs += "--password=$Password" } - $applyArgs += "--data=$tempDir" - Write-Host "Running: ibcmd $($applyArgs -join ' ')" - $applyOut = & $V8Path @applyArgs 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Database configuration updated successfully" -ForegroundColor Green - } else { - Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red - } - if ($applyOut) { Write-Host ($applyOut | Out-String) } - } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Write list file (UTF-8 with BOM) --- - $listFile = Join-Path $tempDir "load_list.txt" - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom) - - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" - $arguments += "-listFile", "`"$listFile`"" - $arguments += "-Format", $Format - $arguments += "-partial" - $arguments += "-updateConfigDumpInfo" - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- UpdateDB --- - if ($UpdateDB) { - $arguments += "/UpdateDBCfg" - } - - # --- Output --- - $outFile = Join-Path $tempDir "load_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "" - Write-Host "Executing partial configuration load..." - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - Write-Host "" - if ($exitCode -eq 0) { - Write-Host "Load completed successfully" -ForegroundColor Green - } else { - Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-load-git v1.8 — Load Git changes into 1C database +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка изменений из Git в базу 1С + +.DESCRIPTION + Определяет изменённые файлы конфигурации по данным Git и выполняет + частичную загрузку в информационную базу. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог XML-выгрузки конфигурации (git-репозиторий) + +.PARAMETER Source + Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All) + +.PARAMETER CommitRange + Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD + +.PARAMETER Extension + Имя расширения для загрузки + +.PARAMETER AllExtensions + Загрузить все расширения + +.PARAMETER Format + Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) + +.PARAMETER DryRun + Только показать что будет загружено (без загрузки) + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD" + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("All", "Staged", "Unstaged", "Commit")] + [string]$Source = "All", + + [Parameter(Mandatory=$false)] + [string]$CommitRange, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [switch]$UpdateDB +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML --- +function Get-ObjectXmlFromSubFile { + param([string]$RelativePath) + + $parts = $RelativePath -split '[\\/]' + if ($parts.Count -ge 2) { + return "$($parts[0])/$($parts[1]).xml" + } + return $null +} + +# --- Resolve V8Path (skip if DryRun) --- +if (-not $DryRun) { + function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null + } + + if (-not $V8Path) { + $V8Path = Find-ProjectV8Path + } + if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } + } + if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" + } + + if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 + } +} + +# --- Detect engine + validate connection (skip if DryRun) --- +$engine = "1cv8" +if (-not $DryRun) { + $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } + } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 + } +} + +# --- Validate config dir --- +if (-not (Test-Path $ConfigDir)) { + Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red + exit 1 +} + +# --- Validate Commit mode --- +if ($Source -eq "Commit" -and -not $CommitRange) { + Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red + exit 1 +} + +# --- Check git --- +try { + $null = git --version 2>&1 +} catch { + Write-Host "Error: git not found in PATH" -ForegroundColor Red + exit 1 +} + +# --- Get changed files from Git --- +$changedFiles = @() +$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') +$configDirNormalized = $ConfigDir.Replace('\', '/') + +Push-Location $ConfigDir +try { + switch ($Source) { + "Staged" { + Write-Host "Getting staged changes..." + $raw = git diff --cached --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "Unstaged" { + Write-Host "Getting unstaged changes..." + $raw = git diff --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git ls-files --others --exclude-standard 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "Commit" { + Write-Host "Getting changes from $CommitRange..." + $raw = git diff --name-only --relative $CommitRange 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "All" { + Write-Host "Getting all uncommitted changes..." + $raw = git diff --cached --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git diff --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git ls-files --others --exclude-standard 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + } +} finally { + Pop-Location +} + +$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + +if ($changedFiles.Count -eq 0) { + Write-Host "No changes found" + exit 0 +} + +Write-Host "Git changes detected: $($changedFiles.Count) files" + +# --- Filter and map to config files --- +$configFiles = @() +$supportSkipped = @() + +foreach ($file in $changedFiles) { + $file = $file.Trim().Replace('\', '/') + if ([string]::IsNullOrWhiteSpace($file)) { continue } + + # Skip service files (not partially loadable). Support-state files are tracked + # to warn the user: support changes apply only via a full load. + if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue } + if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue } + + $fullPath = Join-Path $ConfigDir $file + + if ($file -match '\.xml$') { + # XML file — add directly if exists + if (Test-Path $fullPath) { + if ($configFiles -notcontains $file) { + $configFiles += $file + } + } + } + else { + # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files + $objectXml = Get-ObjectXmlFromSubFile -RelativePath $file + if ($objectXml) { + $fullXmlPath = Join-Path $ConfigDir $objectXml + if (Test-Path $fullXmlPath) { + if ($configFiles -notcontains $objectXml) { + $configFiles += $objectXml + } + if ((Test-Path $fullPath) -and $configFiles -notcontains $file) { + $configFiles += $file + } + + # Add all files from Ext/ directory of the object + $parts = $file -split '[\\/]' + if ($parts.Count -ge 2) { + $extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext" + if (Test-Path $extDir) { + Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object { + $extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/') + if ($configFiles -notcontains $extRelPath) { + $configFiles += $extRelPath + } + } + } + } + } + } + } +} + +if ($supportSkipped.Count -gt 0) { + Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow + foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow } + Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow +} + +if ($configFiles.Count -eq 0) { + Write-Host "No configuration files found in changes" + exit 0 +} + +Write-Host "Files for loading: $($configFiles.Count)" +foreach ($f in $configFiles) { Write-Host " $f" } + +# --- DryRun: stop here --- +if ($DryRun) { + Write-Host "" + Write-Host "DryRun mode - no changes applied" + exit 0 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only; import specific files) --- + if ($Format -eq "Plain") { + Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red + exit 1 + } + if ($AllExtensions) { + Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red + exit 1 + } + $arguments = @("infobase", "config", "import", "files") + $configFiles + $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath" + if ($Extension) { $arguments += "--extension=$Extension" } + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green + if ($output) { Write-Host ($output | Out-String) } + if ($UpdateDB) { + $applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") + if ($UserName) { $applyArgs += "--user=$UserName" } + if ($Password) { $applyArgs += "--password=$Password" } + $applyArgs += "--data=$tempDir" + Write-Host "Running: ibcmd $($applyArgs -join ' ')" + $applyOut = & $V8Path @applyArgs 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Database configuration updated successfully" -ForegroundColor Green + } else { + Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red + } + if ($applyOut) { Write-Host ($applyOut | Out-String) } + } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Write list file (UTF-8 with BOM) --- + $listFile = Join-Path $tempDir "load_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom) + + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" + $arguments += "-listFile", "`"$listFile`"" + $arguments += "-Format", $Format + $arguments += "-partial" + $arguments += "-updateConfigDumpInfo" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- UpdateDB --- + if ($UpdateDB) { + $arguments += "/UpdateDBCfg" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "" + Write-Host "Executing partial configuration load..." + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + Write-Host "" + if ($exitCode -eq 0) { + Write-Host "Load completed successfully" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-load-xml/SKILL.md b/.claude/skills/db-load-xml/SKILL.md index 3cfb283c..a61a0b17 100644 --- a/.claude/skills/db-load-xml/SKILL.md +++ b/.claude/skills/db-load-xml/SKILL.md @@ -1,101 +1,101 @@ ---- -name: db-load-xml -description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles -argument-hint: [database] -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-load-xml — Загрузка конфигурации из XML - -Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку. - -## Usage - -``` -/db-load-xml [database] -/db-load-xml src/config dev -/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" -``` - -> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя. - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. -Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-ConfigDir <путь>` | да | Каталог XML-исходников | -| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` | -| `-Files <список>` | для Partial | Относительные пути файлов через запятую | -| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | -| `-Extension <имя>` | нет | Загрузить в расширение | -| `-AllExtensions` | нет | Загрузить все расширения | -| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | -| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -### Режимы загрузки - -| Режим | Описание | -|-------|----------| -| `Full` | Полная загрузка — замена всей конфигурации из каталога XML | -| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) | - -### Формат файла списка (listFile) - -Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**: - -``` -Catalogs/Номенклатура.xml -Catalogs/Номенклатура/Ext/ObjectModule.bsl -Documents/Заказ.xml -Documents/Заказ/Forms/ФормаДокумента.xml -``` - -## После выполнения - -Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД - -## Примеры - -```powershell -# Полная загрузка -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full - -# Частичная загрузка конкретных файлов -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" - -# Загрузка расширения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" - -# Загрузка + обновление БД в одном запуске -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB -``` +--- +name: db-load-xml +description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles +argument-hint: [database] +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-xml — Загрузка конфигурации из XML + +Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку. + +## Usage + +``` +/db-load-xml [database] +/db-load-xml src/config dev +/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" +``` + +> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог XML-исходников | +| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` | +| `-Files <список>` | для Partial | Относительные пути файлов через запятую | +| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | +| `-Extension <имя>` | нет | Загрузить в расширение | +| `-AllExtensions` | нет | Загрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Режимы загрузки + +| Режим | Описание | +|-------|----------| +| `Full` | Полная загрузка — замена всей конфигурации из каталога XML | +| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) | + +### Формат файла списка (listFile) + +Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**: + +``` +Catalogs/Номенклатура.xml +Catalogs/Номенклатура/Ext/ObjectModule.bsl +Documents/Заказ.xml +Documents/Заказ/Forms/ФормаДокумента.xml +``` + +## После выполнения + +Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД + +## Примеры + +```powershell +# Полная загрузка +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Частичная загрузка конкретных файлов +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" + +# Загрузка расширения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" + +# Загрузка + обновление БД в одном запуске +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB +``` diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 index 9ee53898..4746702a 100644 --- a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 @@ -1,388 +1,388 @@ -# db-load-xml v1.10 — Load 1C configuration from XML files -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Загрузка конфигурации 1С из XML-файлов - -.DESCRIPTION - Загружает конфигурацию в информационную базу из XML-файлов. - Поддерживает полную и частичную загрузку. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER ConfigDir - Каталог XML-исходников конфигурации - -.PARAMETER Mode - Режим загрузки: Full или Partial (по умолчанию Full) - -.PARAMETER Files - Относительные пути файлов через запятую (для режима Partial) - -.PARAMETER ListFile - Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) - -.PARAMETER Extension - Имя расширения для загрузки - -.PARAMETER AllExtensions - Загрузить все расширения - -.PARAMETER Format - Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) - -.EXAMPLE - .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full - -.EXAMPLE - .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$ConfigDir, - - [Parameter(Mandatory=$false)] - [ValidateSet("Full", "Partial")] - [string]$Mode = "Full", - - [Parameter(Mandatory=$false)] - [string]$Files, - - [Parameter(Mandatory=$false)] - [string]$ListFile, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions, - - [Parameter(Mandatory=$false)] - [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical", - - [Parameter(Mandatory=$false)] - [switch]$UpdateDB, - - [Parameter(Mandatory=$false)] - [switch]$StrictLog -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Validate config dir --- -if (-not (Test-Path $ConfigDir)) { - Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red - exit 1 -} - -# --- Validate Partial mode --- -if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { - Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red - exit 1 -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only; hierarchical full-directory import) --- - if ($Format -eq "Plain") { - Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red - exit 1 - } - if ($AllExtensions) { - $arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath") - } elseif ($Mode -eq "Partial" -or $Files -or $ListFile) { - # partial: import specific files (relative to ConfigDir) - $fileList = @() - if ($ListFile) { - if (-not (Test-Path $ListFile)) { - Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red - exit 1 - } - $fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } elseif ($Files) { - $fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } - if ($fileList.Count -eq 0) { - Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red - exit 1 - } - $arguments = @("infobase", "config", "import", "files") + $fileList - $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath" - if ($Extension) { $arguments += "--extension=$Extension" } - } else { - $arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath") - if ($Extension) { $arguments += "--extension=$Extension" } - $arguments += "$ConfigDir" - } - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green - if ($output) { Write-Host ($output | Out-String) } - - if ($UpdateDB) { - $applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") - if ($UserName) { $applyArgs += "--user=$UserName" } - if ($Password) { $applyArgs += "--password=$Password" } - $applyArgs += "--data=$tempDir" - Write-Host "Running: ibcmd $($applyArgs -join ' ')" - $applyOut = & $V8Path @applyArgs 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Database configuration updated successfully" -ForegroundColor Green - } else { - Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red - } - if ($applyOut) { Write-Host ($applyOut | Out-String) } - } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" - - if ($Mode -eq "Full") { - Write-Host "Executing full configuration load..." - } else { - Write-Host "Executing partial configuration load..." - - # Build list file - $rawList = @() - if ($ListFile) { - if (-not (Test-Path $ListFile)) { - Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red - exit 1 - } - $rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } else { - $rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } - - # Support-state service files are NOT partially loadable — exclude with a hint. - $supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$' - $supportFiles = @($rawList | Where-Object { $_ -match $supportRe }) - $fileList = @($rawList | Where-Object { $_ -notmatch $supportRe }) - if ($supportFiles.Count -gt 0) { - Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow - foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow } - Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow - } - if ($fileList.Count -eq 0) { - Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red - exit 1 - } - $generatedListFile = Join-Path $tempDir "load_list.txt" - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom) - Write-Host "Files to load: $($fileList.Count)" - foreach ($f in $fileList) { Write-Host " $f" } - - $arguments += "-listFile", "`"$generatedListFile`"" - $arguments += "-partial" - $arguments += "-updateConfigDumpInfo" - } - - $arguments += "-Format", $Format - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- UpdateDB --- - if ($UpdateDB) { - $arguments += "/UpdateDBCfg" - } - - # --- Output --- - $outFile = Join-Path $tempDir "load_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Read log --- - $logContent = $null - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - } - - # --- Scan log for silent rejections --- - # Platform often writes load-time rejections into /Out but exits with code 0. - # These patterns flag cases where metadata was dropped or rejected silently. - $fatalLogPatterns = @( - 'Неверное свойство объекта метаданных', - 'не входит в состав объекта метаданных', - 'Неизвестное имя типа', - 'Неизвестный объект метаданных', - 'Ни один из документов не является регистратором для регистра', - 'Неверное значение перечисления', - 'не может быть приведен к типу' - ) - $silentFailures = @() - if ($logContent) { - foreach ($line in ($logContent -split "`r?`n")) { - foreach ($pat in $fatalLogPatterns) { - if ($line -match [regex]::Escape($pat)) { - $silentFailures += $line.Trim() - break - } - } - } - } - - # --- Result --- - # Default: mirror platform's verdict via exit code. Log content (including any - # rejection warnings) is always printed to stdout for visibility. With -StrictLog, - # elevate exit code to 1 when rejection patterns are found even if platform said 0. - if ($exitCode -eq 0) { - Write-Host "Load completed successfully" -ForegroundColor Green - } else { - Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red - } - - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - - if ($silentFailures.Count -gt 0) { - $msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" - if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" } - Write-Host $msg -ForegroundColor Yellow - foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } - if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-load-xml v1.10 — Load 1C configuration from XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка конфигурации 1С из XML-файлов + +.DESCRIPTION + Загружает конфигурацию в информационную базу из XML-файлов. + Поддерживает полную и частичную загрузку. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог XML-исходников конфигурации + +.PARAMETER Mode + Режим загрузки: Full или Partial (по умолчанию Full) + +.PARAMETER Files + Относительные пути файлов через запятую (для режима Partial) + +.PARAMETER ListFile + Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) + +.PARAMETER Extension + Имя расширения для загрузки + +.PARAMETER AllExtensions + Загрузить все расширения + +.PARAMETER Format + Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full + +.EXAMPLE + .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Full", "Partial")] + [string]$Mode = "Full", + + [Parameter(Mandatory=$false)] + [string]$Files, + + [Parameter(Mandatory=$false)] + [string]$ListFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [switch]$UpdateDB, + + [Parameter(Mandatory=$false)] + [switch]$StrictLog +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate config dir --- +if (-not (Test-Path $ConfigDir)) { + Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red + exit 1 +} + +# --- Validate Partial mode --- +if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { + Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only; hierarchical full-directory import) --- + if ($Format -eq "Plain") { + Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red + exit 1 + } + if ($AllExtensions) { + $arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath") + } elseif ($Mode -eq "Partial" -or $Files -or $ListFile) { + # partial: import specific files (relative to ConfigDir) + $fileList = @() + if ($ListFile) { + if (-not (Test-Path $ListFile)) { + Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red + exit 1 + } + $fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } elseif ($Files) { + $fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + if ($fileList.Count -eq 0) { + Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red + exit 1 + } + $arguments = @("infobase", "config", "import", "files") + $fileList + $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath" + if ($Extension) { $arguments += "--extension=$Extension" } + } else { + $arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath") + if ($Extension) { $arguments += "--extension=$Extension" } + $arguments += "$ConfigDir" + } + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green + if ($output) { Write-Host ($output | Out-String) } + + if ($UpdateDB) { + $applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") + if ($UserName) { $applyArgs += "--user=$UserName" } + if ($Password) { $applyArgs += "--password=$Password" } + $applyArgs += "--data=$tempDir" + Write-Host "Running: ibcmd $($applyArgs -join ' ')" + $applyOut = & $V8Path @applyArgs 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Database configuration updated successfully" -ForegroundColor Green + } else { + Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red + } + if ($applyOut) { Write-Host ($applyOut | Out-String) } + } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" + + if ($Mode -eq "Full") { + Write-Host "Executing full configuration load..." + } else { + Write-Host "Executing partial configuration load..." + + # Build list file + $rawList = @() + if ($ListFile) { + if (-not (Test-Path $ListFile)) { + Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red + exit 1 + } + $rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } else { + $rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + + # Support-state service files are NOT partially loadable — exclude with a hint. + $supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$' + $supportFiles = @($rawList | Where-Object { $_ -match $supportRe }) + $fileList = @($rawList | Where-Object { $_ -notmatch $supportRe }) + if ($supportFiles.Count -gt 0) { + Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow + foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow } + Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow + } + if ($fileList.Count -eq 0) { + Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red + exit 1 + } + $generatedListFile = Join-Path $tempDir "load_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom) + Write-Host "Files to load: $($fileList.Count)" + foreach ($f in $fileList) { Write-Host " $f" } + + $arguments += "-listFile", "`"$generatedListFile`"" + $arguments += "-partial" + $arguments += "-updateConfigDumpInfo" + } + + $arguments += "-Format", $Format + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- UpdateDB --- + if ($UpdateDB) { + $arguments += "/UpdateDBCfg" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Read log --- + $logContent = $null + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + } + + # --- Scan log for silent rejections --- + # Platform often writes load-time rejections into /Out but exits with code 0. + # These patterns flag cases where metadata was dropped or rejected silently. + $fatalLogPatterns = @( + 'Неверное свойство объекта метаданных', + 'не входит в состав объекта метаданных', + 'Неизвестное имя типа', + 'Неизвестный объект метаданных', + 'Ни один из документов не является регистратором для регистра', + 'Неверное значение перечисления', + 'не может быть приведен к типу' + ) + $silentFailures = @() + if ($logContent) { + foreach ($line in ($logContent -split "`r?`n")) { + foreach ($pat in $fatalLogPatterns) { + if ($line -match [regex]::Escape($pat)) { + $silentFailures += $line.Trim() + break + } + } + } + } + + # --- Result --- + # Default: mirror platform's verdict via exit code. Log content (including any + # rejection warnings) is always printed to stdout for visibility. With -StrictLog, + # elevate exit code to 1 when rejection patterns are found even if platform said 0. + if ($exitCode -eq 0) { + Write-Host "Load completed successfully" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + + if ($silentFailures.Count -gt 0) { + $msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" + if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" } + Write-Host $msg -ForegroundColor Yellow + foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } + if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-run/SKILL.md b/.claude/skills/db-run/SKILL.md index 2c0ac7f3..c768b6ee 100644 --- a/.claude/skills/db-run/SKILL.md +++ b/.claude/skills/db-run/SKILL.md @@ -1,76 +1,76 @@ ---- -name: db-run -description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие -argument-hint: "[database]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-run — Запуск 1С:Предприятие - -Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). - -## Usage - -``` -/db-run [database] -/db-run dev -/db-run dev /Execute process.epf -/db-run dev /C "параметр запуска" -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | -| `-CParam <строка>` | нет | Параметр запуска (/C) | -| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -## Важно - -Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу. - -## Примеры - -```powershell -# Простой запуск -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" - -# Запуск с обработкой -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" - -# Открыть по навигационной ссылке -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" - -# Серверная база с параметром запуска -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" -``` +--- +name: db-run +description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие +argument-hint: "[database]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-run — Запуск 1С:Предприятие + +Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). + +## Usage + +``` +/db-run [database] +/db-run dev +/db-run dev /Execute process.epf +/db-run dev /C "параметр запуска" +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | +| `-CParam <строка>` | нет | Параметр запуска (/C) | +| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Важно + +Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу. + +## Примеры + +```powershell +# Простой запуск +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" + +# Запуск с обработкой +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" + +# Открыть по навигационной ссылке +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" + +# Серверная база с параметром запуска +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" +``` diff --git a/.claude/skills/db-run/scripts/db-run.ps1 b/.claude/skills/db-run/scripts/db-run.ps1 index dfb9a95b..560f9fef 100644 --- a/.claude/skills/db-run/scripts/db-run.ps1 +++ b/.claude/skills/db-run/scripts/db-run.ps1 @@ -1,170 +1,170 @@ -# db-run v1.1 — Launch 1C:Enterprise -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Запуск 1С:Предприятие - -.DESCRIPTION - Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). - Запуск в фоне — не ждёт завершения процесса. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER Execute - Путь к внешней обработке для запуска - -.PARAMETER CParam - Параметр запуска (/C) - -.PARAMETER URL - Навигационная ссылка (e1cib/...) - -.EXAMPLE - .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" - -.EXAMPLE - .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" - -.EXAMPLE - .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$false)] - [string]$Execute, - - [Parameter(Mandatory=$false)] - [string]$CParam, - - [Parameter(Mandatory=$false)] - [string]$URL -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Validate connection --- -if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Build arguments as single string --- -# Note: Start-Process without -NoNewWindow uses ShellExecute. -# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute -# re-joins elements. Single string avoids this. -$argString = "ENTERPRISE" - -if ($InfoBaseServer -and $InfoBaseRef) { - $argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" -} else { - $argString += " /F `"$InfoBasePath`"" -} - -if ($UserName) { $argString += " /N`"$UserName`"" } -if ($Password) { $argString += " /P`"$Password`"" } - -# --- Optional params --- -if ($Execute) { - $ext = [System.IO.Path]::GetExtension($Execute).ToLower() - if ($ext -eq ".erf") { - Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow - Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow - Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow - $Execute = "" - } -} -if ($Execute) { - $argString += " /Execute `"$Execute`"" -} -if ($CParam) { - $argString += " /C `"$CParam`"" -} -if ($URL) { - $argString += " /URL `"$URL`"" -} - -$argString += " /DisableStartupDialogs" - -# --- Execute (background, no wait) --- -Write-Host "Running: 1cv8.exe $argString" -Start-Process -FilePath $V8Path -ArgumentList $argString -Write-Host "1C:Enterprise launched" -ForegroundColor Green +# db-run v1.1 — Launch 1C:Enterprise +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Запуск 1С:Предприятие + +.DESCRIPTION + Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). + Запуск в фоне — не ждёт завершения процесса. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER Execute + Путь к внешней обработке для запуска + +.PARAMETER CParam + Параметр запуска (/C) + +.PARAMETER URL + Навигационная ссылка (e1cib/...) + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Execute, + + [Parameter(Mandatory=$false)] + [string]$CParam, + + [Parameter(Mandatory=$false)] + [string]$URL +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Build arguments as single string --- +# Note: Start-Process without -NoNewWindow uses ShellExecute. +# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute +# re-joins elements. Single string avoids this. +$argString = "ENTERPRISE" + +if ($InfoBaseServer -and $InfoBaseRef) { + $argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" +} else { + $argString += " /F `"$InfoBasePath`"" +} + +if ($UserName) { $argString += " /N`"$UserName`"" } +if ($Password) { $argString += " /P`"$Password`"" } + +# --- Optional params --- +if ($Execute) { + $ext = [System.IO.Path]::GetExtension($Execute).ToLower() + if ($ext -eq ".erf") { + Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow + Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow + Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow + $Execute = "" + } +} +if ($Execute) { + $argString += " /Execute `"$Execute`"" +} +if ($CParam) { + $argString += " /C `"$CParam`"" +} +if ($URL) { + $argString += " /URL `"$URL`"" +} + +$argString += " /DisableStartupDialogs" + +# --- Execute (background, no wait) --- +Write-Host "Running: 1cv8.exe $argString" +Start-Process -FilePath $V8Path -ArgumentList $argString +Write-Host "1C:Enterprise launched" -ForegroundColor Green diff --git a/.claude/skills/db-update/SKILL.md b/.claude/skills/db-update/SKILL.md index fc788e1b..6f9426a6 100644 --- a/.claude/skills/db-update/SKILL.md +++ b/.claude/skills/db-update/SKILL.md @@ -1,86 +1,86 @@ ---- -name: db-update -description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg -argument-hint: "[database]" -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /db-update — Обновление конфигурации БД - -Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`. - -## Usage - -``` -/db-update [database] -/db-update dev -/db-update dev -Dynamic+ -``` - -## Параметры подключения - -Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: -1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -4. Если ветка не совпала — используй `default` -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если файла нет — предложи `/db-list add`. -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-Extension <имя>` | нет | Обновить расширение | -| `-AllExtensions` | нет | Обновить все расширения | -| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | -| `-Server` | нет | Обновление на стороне сервера | -| `-WarningsAsErrors` | нет | Предупреждения считать ошибками | - -> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` - -### Фоновое обновление (серверная база) - -| Параметр | Описание | -|----------|----------| -| `-BackgroundStart` | Начать фоновое обновление | -| `-BackgroundFinish` | Дождаться окончания | -| `-BackgroundCancel` | Отменить | -| `-BackgroundSuspend` | Приостановить | -| `-BackgroundResume` | Возобновить | - -## Предупреждения - -- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти) -- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки -- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно - -## Примеры - -```powershell -# Обычное обновление (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" - -# Динамическое обновление (серверная база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+" - -# Обновление расширения -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" -``` +--- +name: db-update +description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg +argument-hint: "[database]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-update — Обновление конфигурации БД + +Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`. + +## Usage + +``` +/db-update [database] +/db-update dev +/db-update dev -Dynamic+ +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-Extension <имя>` | нет | Обновить расширение | +| `-AllExtensions` | нет | Обновить все расширения | +| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | +| `-Server` | нет | Обновление на стороне сервера | +| `-WarningsAsErrors` | нет | Предупреждения считать ошибками | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Фоновое обновление (серверная база) + +| Параметр | Описание | +|----------|----------| +| `-BackgroundStart` | Начать фоновое обновление | +| `-BackgroundFinish` | Дождаться окончания | +| `-BackgroundCancel` | Отменить | +| `-BackgroundSuspend` | Приостановить | +| `-BackgroundResume` | Возобновить | + +## Предупреждения + +- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти) +- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки +- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно + +## Примеры + +```powershell +# Обычное обновление (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" + +# Динамическое обновление (серверная база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+" + +# Обновление расширения +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" +``` diff --git a/.claude/skills/db-update/scripts/db-update.ps1 b/.claude/skills/db-update/scripts/db-update.ps1 index 1bbbe4e8..52940032 100644 --- a/.claude/skills/db-update/scripts/db-update.ps1 +++ b/.claude/skills/db-update/scripts/db-update.ps1 @@ -1,243 +1,243 @@ -# db-update v1.4 — Update 1C database configuration -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Обновление конфигурации базы данных 1С - -.DESCRIPTION - Применяет изменения основной конфигурации к конфигурации базы данных. - Поддерживает динамическое обновление, обновление расширений. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER Extension - Имя расширения для обновления - -.PARAMETER AllExtensions - Обновить все расширения - -.PARAMETER Dynamic - Динамическое обновление: "+" включить, "-" отключить - -.PARAMETER Server - Обновление на стороне сервера - -.PARAMETER WarningsAsErrors - Предупреждения считать ошибками - -.EXAMPLE - .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" - -.EXAMPLE - .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$false)] - [string]$Extension, - - [Parameter(Mandatory=$false)] - [switch]$AllExtensions, - - [Parameter(Mandatory=$false)] - [ValidateSet("+", "-")] - [string]$Dynamic, - - [Parameter(Mandatory=$false)] - [switch]$Server, - - [Parameter(Mandatory=$false)] - [switch]$WarningsAsErrors -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } - -# --- Validate connection --- -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } -} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red - exit 1 -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch (file infobase only) --- - if ($AllExtensions) { - Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red - exit 1 - } - $arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") - if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" } - elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" } - if ($Extension) { $arguments += "--extension=$Extension" } - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "Database configuration updated successfully" -ForegroundColor Green - } else { - Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/UpdateDBCfg" - - # --- Options --- - if ($Dynamic) { - $arguments += "-Dynamic$Dynamic" - } - if ($Server) { - $arguments += "-Server" - } - if ($WarningsAsErrors) { - $arguments += "-WarningsAsErrors" - } - - # --- Extensions --- - if ($Extension) { - $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" - } - - # --- Output --- - $outFile = Join-Path $tempDir "update_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Database configuration updated successfully" -ForegroundColor Green - } else { - Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# db-update v1.4 — Update 1C database configuration +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Обновление конфигурации базы данных 1С + +.DESCRIPTION + Применяет изменения основной конфигурации к конфигурации базы данных. + Поддерживает динамическое обновление, обновление расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER Extension + Имя расширения для обновления + +.PARAMETER AllExtensions + Обновить все расширения + +.PARAMETER Dynamic + Динамическое обновление: "+" включить, "-" отключить + +.PARAMETER Server + Обновление на стороне сервера + +.PARAMETER WarningsAsErrors + Предупреждения считать ошибками + +.EXAMPLE + .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" + +.EXAMPLE + .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("+", "-")] + [string]$Dynamic, + + [Parameter(Mandatory=$false)] + [switch]$Server, + + [Parameter(Mandatory=$false)] + [switch]$WarningsAsErrors +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Validate connection --- +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } +} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch (file infobase only) --- + if ($AllExtensions) { + Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red + exit 1 + } + $arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") + if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" } + elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" } + if ($Extension) { $arguments += "--extension=$Extension" } + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "Database configuration updated successfully" -ForegroundColor Green + } else { + Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/UpdateDBCfg" + + # --- Options --- + if ($Dynamic) { + $arguments += "-Dynamic$Dynamic" + } + if ($Server) { + $arguments += "-Server" + } + if ($WarningsAsErrors) { + $arguments += "-WarningsAsErrors" + } + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "update_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Database configuration updated successfully" -ForegroundColor Green + } else { + Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/epf-bsp-add-command/SKILL.md b/.claude/skills/epf-bsp-add-command/SKILL.md index f2e0168b..3d3f9674 100644 --- a/.claude/skills/epf-bsp-add-command/SKILL.md +++ b/.claude/skills/epf-bsp-add-command/SKILL.md @@ -1,196 +1,196 @@ ---- -name: epf-bsp-add-command -description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП -argument-hint: <Идентификатор> [ТипКоманды] [Представление] -allowed-tools: - - Read - - Edit - - Glob - - Grep ---- - -# /epf-bsp-add-command — Добавление команды БСП - -Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик. - -Предварительно обработка должна быть инициализирована через `/epf-bsp-init`. - -## Usage - -``` -/epf-bsp-add-command <Идентификатор> [ТипКоманды] [Представление] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|-----------------------|--------------------------------------------| -| ProcessorName | да | — | Имя обработки | -| Идентификатор | да | — | Внутреннее имя команды (латиница) | -| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) | -| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя | -| SrcDir | нет | `src` | Каталог исходников | - -## Маппинг типов команд - -Пользователь может указать тип в свободной форме: - -| Пользователь пишет | ТипКоманды | -|---------------------------------------|-----------------------------------------------------| -| открыть форму, форма | `ТипКомандыОткрытиеФормы()` | -| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` | -| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` | -| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` | -| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` | - -Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`: - -| Вид обработки (из кода) | ТипКоманды по умолчанию | -|----------------------------|-------------------------------------------| -| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | -| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | -| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | -| Отчет | `ТипКомандыОткрытиеФормы()` | -| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | -| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | - -## Шаблон добавления команды - -Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`: - -```bsl - НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); - НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'"); - НоваяКоманда.Идентификатор = "{{Идентификатор}}"; - НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; - НоваяКоманда.ПоказыватьОповещение = Ложь; -``` - -Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также: - -```bsl - НоваяКоманда.Модификатор = "ПечатьMXL"; -``` - -Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`. - -## Шаблоны обработчиков - -### ВызовСерверногоМетода — если обработчик уже есть - -Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`: - -```bsl - ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда - // TODO: Реализация {{Идентификатор}} -``` - -### ВызовСерверногоМетода — если обработчика нет - -Для глобальных обработок (без `ОбъектыНазначения`): - -```bsl -Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт - - Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда - // TODO: Реализация {{Идентификатор}} - КонецЕсли; - -КонецПроцедуры -``` - -Для назначаемых обработок (с `ОбъектыНазначения`): - -```bsl -Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт - - Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда - // TODO: Реализация {{Идентификатор}} - КонецЕсли; - -КонецПроцедуры -``` - -### ПечатнаяФорма — если процедура Печать уже есть - -Добавь блок перед `КонецПроцедуры`: - -```bsl - ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); - Если ПечатнаяФорма <> Неопределено Тогда - ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); - ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); - КонецЕсли; -``` - -### ПечатнаяФорма — если процедуры Печать нет - -```bsl -Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт - - ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); - Если ПечатнаяФорма <> Неопределено Тогда - ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); - ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); - КонецЕсли; - -КонецПроцедуры -``` - -### ВызовКлиентскогоМетода - -Добавляется в **модуль формы** (`Forms//Ext/Form/Module.bsl`): - -Для глобальных обработок: - -```bsl -&НаКлиенте -Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт - - Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда - // TODO: Реализация {{Идентификатор}} - КонецЕсли; - -КонецПроцедуры -``` - -Для назначаемых обработок: - -```bsl -&НаКлиенте -Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт - - Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда - // TODO: Реализация {{Идентификатор}} - КонецЕсли; - -КонецПроцедуры -``` - -Если процедура уже есть — добавь ветку `ИначеЕсли`. - -## Инструкции - -1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` -2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init` -3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`) -4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации` -5. Добавь обработчик: - - Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс` - - Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`) -6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры -7. Используй табы для отступов - -## Пример - -Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"` - -В `СведенияОВнешнейОбработке()` перед `Возврат` добавится: - -```bsl - НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); - НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'"); - НоваяКоманда.Идентификатор = "ЗаказПокупателя"; - НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); - НоваяКоманда.ПоказыватьОповещение = Ложь; -``` - -И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. +--- +name: epf-bsp-add-command +description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП +argument-hint: <Идентификатор> [ТипКоманды] [Представление] +allowed-tools: + - Read + - Edit + - Glob + - Grep +--- + +# /epf-bsp-add-command — Добавление команды БСП + +Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик. + +Предварительно обработка должна быть инициализирована через `/epf-bsp-init`. + +## Usage + +``` +/epf-bsp-add-command <Идентификатор> [ТипКоманды] [Представление] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|-----------------------|--------------------------------------------| +| ProcessorName | да | — | Имя обработки | +| Идентификатор | да | — | Внутреннее имя команды (латиница) | +| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) | +| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя | +| SrcDir | нет | `src` | Каталог исходников | + +## Маппинг типов команд + +Пользователь может указать тип в свободной форме: + +| Пользователь пишет | ТипКоманды | +|---------------------------------------|-----------------------------------------------------| +| открыть форму, форма | `ТипКомандыОткрытиеФормы()` | +| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` | +| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` | +| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` | +| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` | + +Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`: + +| Вид обработки (из кода) | ТипКоманды по умолчанию | +|----------------------------|-------------------------------------------| +| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | +| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | +| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | +| Отчет | `ТипКомандыОткрытиеФормы()` | +| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | +| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | + +## Шаблон добавления команды + +Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`: + +```bsl + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'"); + НоваяКоманда.Идентификатор = "{{Идентификатор}}"; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; + НоваяКоманда.ПоказыватьОповещение = Ложь; +``` + +Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также: + +```bsl + НоваяКоманда.Модификатор = "ПечатьMXL"; +``` + +Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`. + +## Шаблоны обработчиков + +### ВызовСерверногоМетода — если обработчик уже есть + +Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`: + +```bsl + ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} +``` + +### ВызовСерверногоМетода — если обработчика нет + +Для глобальных обработок (без `ОбъектыНазначения`): + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Для назначаемых обработок (с `ОбъектыНазначения`): + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +### ПечатнаяФорма — если процедура Печать уже есть + +Добавь блок перед `КонецПроцедуры`: + +```bsl + ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); + Если ПечатнаяФорма <> Неопределено Тогда + ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); + ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); + КонецЕсли; +``` + +### ПечатнаяФорма — если процедуры Печать нет + +```bsl +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); + Если ПечатнаяФорма <> Неопределено Тогда + ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); + ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); + КонецЕсли; + +КонецПроцедуры +``` + +### ВызовКлиентскогоМетода + +Добавляется в **модуль формы** (`Forms//Ext/Form/Module.bsl`): + +Для глобальных обработок: + +```bsl +&НаКлиенте +Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Для назначаемых обработок: + +```bsl +&НаКлиенте +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Если процедура уже есть — добавь ветку `ИначеЕсли`. + +## Инструкции + +1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` +2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init` +3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`) +4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации` +5. Добавь обработчик: + - Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс` + - Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`) +6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры +7. Используй табы для отступов + +## Пример + +Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"` + +В `СведенияОВнешнейОбработке()` перед `Возврат` добавится: + +```bsl + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'"); + НоваяКоманда.Идентификатор = "ЗаказПокупателя"; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); + НоваяКоманда.ПоказыватьОповещение = Ложь; +``` + +И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. diff --git a/.claude/skills/epf-bsp-init/SKILL.md b/.claude/skills/epf-bsp-init/SKILL.md index a63bf6b6..49be36f6 100644 --- a/.claude/skills/epf-bsp-init/SKILL.md +++ b/.claude/skills/epf-bsp-init/SKILL.md @@ -1,208 +1,208 @@ ---- -name: epf-bsp-init -description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки» -argument-hint: <Вид> -allowed-tools: - - Read - - Edit - - Glob - - Grep ---- - -# /epf-bsp-init — Регистрация обработки в БСП - -Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП. - -## Usage - -``` -/epf-bsp-init <Вид> [Назначение...] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|--------------|---------------------------------------------------------| -| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) | -| Вид | да | — | Вид обработки (см. маппинг ниже) | -| Назначение | * | — | Объекты метаданных для назначаемых видов | -| SrcDir | нет | `src` | Каталог исходников | - -\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов. - -## Маппинг вида обработки - -Пользователь может указать вид в свободной форме. Определи нужный по контексту: - -| Пользователь пишет | Вид | API-метод | -|-------------------------------------------|----------------------------|----------------------------------------------| -| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` | -| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` | -| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` | -| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` | -| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` | -| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` | - -## Тип команды по умолчанию - -| Вид | ТипКоманды по умолчанию | -|----------------------------|-------------------------------------------| -| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | -| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | -| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | -| Отчет | `ТипКомандыОткрытиеФормы()` | -| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | -| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | - -## Шаблон: СведенияОВнешнейОбработке - -Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции. - -```bsl -Функция СведенияОВнешнейОбработке() Экспорт - - МетаданныеОбработки = Метаданные(); - - ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); - ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}}; - ПараметрыРегистрации.Версия = "1.0"; - - {{СЕКЦИЯ_НАЗНАЧЕНИЕ}} - - НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); - НоваяКоманда.Представление = МетаданныеОбработки.Представление(); - НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; - НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; - НоваяКоманда.ПоказыватьОповещение = Ложь; - {{СЕКЦИЯ_МОДИФИКАТОР}} - - Возврат ПараметрыРегистрации; - -КонецФункции -``` - -### Подстановки - -- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида -- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию - -### Условные секции - -**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект: - -```bsl - ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); -``` - -Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`). - -Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой. - -**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма: - -```bsl - НоваяКоманда.Модификатор = "ПечатьMXL"; -``` - -Для остальных видов — удалить вместе с пустой строкой. - -## Шаблоны серверных обработчиков - -Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`. - -### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов - -```bsl -Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт - - // TODO: Реализация - -КонецПроцедуры -``` - -### Для ПечатнаяФорма - -```bsl -Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт - - // TODO: Реализация - -КонецПроцедуры -``` - -### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода) - -Если пользователь явно выбрал серверный метод вместо открытия формы: - -```bsl -Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт - - // TODO: Реализация - -КонецПроцедуры -``` - -Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`. - -## Инструкции - -1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` -2. Прочитай файл -3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй -4. Если файл не найден — предложи сначала вызвать `/epf-init` -5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти` -6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области -7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции -8. Используй табы для отступов (как в исходном файле) - -## Пример - -Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату` - -Результат в `ObjectModule.bsl`: - -```bsl -#Область ОписаниеПеременных - -#КонецОбласти - -#Область ПрограммныйИнтерфейс - -Функция СведенияОВнешнейОбработке() Экспорт - - МетаданныеОбработки = Метаданные(); - - ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); - ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма(); - ПараметрыРегистрации.Версия = "1.0"; - - ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); - - НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); - НоваяКоманда.Представление = МетаданныеОбработки.Представление(); - НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; - НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); - НоваяКоманда.ПоказыватьОповещение = Ложь; - НоваяКоманда.Модификатор = "ПечатьMXL"; - - Возврат ПараметрыРегистрации; - -КонецФункции - -Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт - - // TODO: Реализация - -КонецПроцедуры - -#КонецОбласти - -#Область СлужебныеПроцедурыИФункции - -#КонецОбласти -``` - -## Дальнейшие шаги - -- Добавить ещё команду: `/epf-bsp-add-command` -- Добавить форму: `/form-add` -- Добавить макет: `/template-add` -- Собрать EPF: `/epf-build` +--- +name: epf-bsp-init +description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки» +argument-hint: <Вид> +allowed-tools: + - Read + - Edit + - Glob + - Grep +--- + +# /epf-bsp-init — Регистрация обработки в БСП + +Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП. + +## Usage + +``` +/epf-bsp-init <Вид> [Назначение...] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|--------------|---------------------------------------------------------| +| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) | +| Вид | да | — | Вид обработки (см. маппинг ниже) | +| Назначение | * | — | Объекты метаданных для назначаемых видов | +| SrcDir | нет | `src` | Каталог исходников | + +\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов. + +## Маппинг вида обработки + +Пользователь может указать вид в свободной форме. Определи нужный по контексту: + +| Пользователь пишет | Вид | API-метод | +|-------------------------------------------|----------------------------|----------------------------------------------| +| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` | +| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` | +| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` | +| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` | +| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` | +| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` | + +## Тип команды по умолчанию + +| Вид | ТипКоманды по умолчанию | +|----------------------------|-------------------------------------------| +| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | +| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | +| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | +| Отчет | `ТипКомандыОткрытиеФормы()` | +| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | +| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | + +## Шаблон: СведенияОВнешнейОбработке + +Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции. + +```bsl +Функция СведенияОВнешнейОбработке() Экспорт + + МетаданныеОбработки = Метаданные(); + + ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); + ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}}; + ПараметрыРегистрации.Версия = "1.0"; + + {{СЕКЦИЯ_НАЗНАЧЕНИЕ}} + + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = МетаданныеОбработки.Представление(); + НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; + НоваяКоманда.ПоказыватьОповещение = Ложь; + {{СЕКЦИЯ_МОДИФИКАТОР}} + + Возврат ПараметрыРегистрации; + +КонецФункции +``` + +### Подстановки + +- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида +- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию + +### Условные секции + +**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект: + +```bsl + ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); +``` + +Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`). + +Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой. + +**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма: + +```bsl + НоваяКоманда.Модификатор = "ПечатьMXL"; +``` + +Для остальных видов — удалить вместе с пустой строкой. + +## Шаблоны серверных обработчиков + +Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`. + +### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +### Для ПечатнаяФорма + +```bsl +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода) + +Если пользователь явно выбрал серверный метод вместо открытия формы: + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`. + +## Инструкции + +1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` +2. Прочитай файл +3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй +4. Если файл не найден — предложи сначала вызвать `/epf-init` +5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти` +6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области +7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции +8. Используй табы для отступов (как в исходном файле) + +## Пример + +Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату` + +Результат в `ObjectModule.bsl`: + +```bsl +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +Функция СведенияОВнешнейОбработке() Экспорт + + МетаданныеОбработки = Метаданные(); + + ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); + ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма(); + ПараметрыРегистрации.Версия = "1.0"; + + ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); + + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = МетаданныеОбработки.Представление(); + НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); + НоваяКоманда.ПоказыватьОповещение = Ложь; + НоваяКоманда.Модификатор = "ПечатьMXL"; + + Возврат ПараметрыРегистрации; + +КонецФункции + +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + // TODO: Реализация + +КонецПроцедуры + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +``` + +## Дальнейшие шаги + +- Добавить ещё команду: `/epf-bsp-add-command` +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Собрать EPF: `/epf-build` diff --git a/.claude/skills/epf-build/SKILL.md b/.claude/skills/epf-build/SKILL.md index de59cc49..652ff42c 100644 --- a/.claude/skills/epf-build/SKILL.md +++ b/.claude/skills/epf-build/SKILL.md @@ -1,69 +1,69 @@ ---- -name: epf-build -description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников -argument-hint: -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# /epf-build — Сборка обработки - -## Usage - -``` -/epf-build [SrcDir] [OutDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|--------------|--------------------------------------| -| ProcessorName | да | — | Имя обработки (имя корневого XML) | -| SrcDir | нет | `src` | Каталог исходников | -| OutDir | нет | `build` | Каталог для результата | - -## Параметры подключения (опционально) - -Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. - -1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: -2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -5. Если ветка не совпала — используй `default` -6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. - -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | -| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | - -> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных - -## Примеры - -```powershell -# Сборка обработки (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" -``` +--- +name: epf-build +description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /epf-build — Сборка обработки + +## Usage + +``` +/epf-build [SrcDir] [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|--------------|--------------------------------------| +| ProcessorName | да | — | Имя обработки (имя корневого XML) | +| SrcDir | нет | `src` | Каталог исходников | +| OutDir | нет | `build` | Каталог для результата | + +## Параметры подключения (опционально) + +Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. + +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | +| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | + +> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных + +## Примеры + +```powershell +# Сборка обработки (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" +``` diff --git a/.claude/skills/epf-build/scripts/epf-build.ps1 b/.claude/skills/epf-build/scripts/epf-build.ps1 index 751ac2b2..12a05b31 100644 --- a/.claude/skills/epf-build/scripts/epf-build.ps1 +++ b/.claude/skills/epf-build/scripts/epf-build.ps1 @@ -1,225 +1,225 @@ -# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Сборка внешней обработки/отчёта 1С из XML-исходников - -.DESCRIPTION - Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. - Общий скрипт для epf-build и erf-build. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER SourceFile - Путь к корневому XML-файлу исходников - -.PARAMETER OutputFile - Путь к выходному EPF/ERF-файлу - -.EXAMPLE - .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" - -.EXAMPLE - .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$SourceFile, - - [Parameter(Mandatory=$true)] - [string]$OutputFile -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } -if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red - exit 1 -} - -# --- Auto-create stub database if no connection specified --- -$autoCreatedBase = $null -if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - $sourceDir = Split-Path $SourceFile -Parent - $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" - $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" - Write-Host "No database specified. Creating temporary stub database..." - $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" - $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru - if ($stubProc.ExitCode -ne 0) { - Write-Host "Error: failed to create stub database" -ForegroundColor Red - exit 1 - } - $InfoBasePath = $autoBasePath - $autoCreatedBase = $autoBasePath -} - -# --- Validate source file --- -if (-not (Test-Path $SourceFile)) { - Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red - exit 1 -} - -# --- Ensure output directory exists --- -$outDir = Split-Path $OutputFile -Parent -if ($outDir -and -not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch: build EPF/ERF via config import --out --- - $srcDir = Split-Path $SourceFile -Parent - $arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath") - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green - } else { - Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" - - # --- Output --- - $outFile = Join-Path $tempDir "build_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green - } else { - Write-Host "Error building (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } - if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { - Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue - } -} +# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Сборка внешней обработки/отчёта 1С из XML-исходников + +.DESCRIPTION + Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. + Общий скрипт для epf-build и erf-build. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER SourceFile + Путь к корневому XML-файлу исходников + +.PARAMETER OutputFile + Путь к выходному EPF/ERF-файлу + +.EXAMPLE + .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" + +.EXAMPLE + .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$SourceFile, + + [Parameter(Mandatory=$true)] + [string]$OutputFile +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red + exit 1 +} + +# --- Auto-create stub database if no connection specified --- +$autoCreatedBase = $null +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + $sourceDir = Split-Path $SourceFile -Parent + $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" + $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" + Write-Host "No database specified. Creating temporary stub database..." + $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" + $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru + if ($stubProc.ExitCode -ne 0) { + Write-Host "Error: failed to create stub database" -ForegroundColor Red + exit 1 + } + $InfoBasePath = $autoBasePath + $autoCreatedBase = $autoBasePath +} + +# --- Validate source file --- +if (-not (Test-Path $SourceFile)) { + Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +$outDir = Split-Path $OutputFile -Parent +if ($outDir -and -not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch: build EPF/ERF via config import --out --- + $srcDir = Split-Path $SourceFile -Parent + $arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath") + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" + + # --- Output --- + $outFile = Join-Path $tempDir "build_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error building (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { + Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/epf-dump/SKILL.md b/.claude/skills/epf-dump/SKILL.md index b6057563..06edb303 100644 --- a/.claude/skills/epf-dump/SKILL.md +++ b/.claude/skills/epf-dump/SKILL.md @@ -1,69 +1,69 @@ ---- -name: epf-dump -description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла -argument-hint: -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# /epf-dump — Разборка обработки - -## Usage - -``` -/epf-dump [OutDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|----------|:------------:|--------------|-------------------------------------| -| EpfFile | да | — | Путь к EPF-файлу | -| OutDir | нет | `src` | Каталог для выгрузки исходников | - -## Параметры подключения (обязательно) - -Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. - -1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: -2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -5. Если ветка не совпала — используй `default` -6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. - -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | -| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | -| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | - -> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) - -## Примеры - -```powershell -# Разборка обработки (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" -``` +--- +name: epf-dump +description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /epf-dump — Разборка обработки + +## Usage + +``` +/epf-dump [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|----------|:------------:|--------------|-------------------------------------| +| EpfFile | да | — | Путь к EPF-файлу | +| OutDir | нет | `src` | Каталог для выгрузки исходников | + +## Параметры подключения (обязательно) + +Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. + +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | +| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) + +## Примеры + +```powershell +# Разборка обработки (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" +``` diff --git a/.claude/skills/epf-dump/scripts/epf-dump.ps1 b/.claude/skills/epf-dump/scripts/epf-dump.ps1 index 85e3b53a..34a2f20b 100644 --- a/.claude/skills/epf-dump/scripts/epf-dump.ps1 +++ b/.claude/skills/epf-dump/scripts/epf-dump.ps1 @@ -1,224 +1,224 @@ -# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -<# -.SYNOPSIS - Разборка внешней обработки/отчёта 1С в XML-исходники - -.DESCRIPTION - Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. - Общий скрипт для epf-dump и erf-dump. - -.PARAMETER V8Path - Путь к каталогу bin платформы или к 1cv8.exe - -.PARAMETER InfoBasePath - Путь к файловой информационной базе - -.PARAMETER InfoBaseServer - Сервер 1С (для серверной базы) - -.PARAMETER InfoBaseRef - Имя базы на сервере - -.PARAMETER UserName - Имя пользователя 1С - -.PARAMETER Password - Пароль пользователя - -.PARAMETER InputFile - Путь к EPF/ERF-файлу - -.PARAMETER OutputDir - Каталог для выгрузки исходников - -.PARAMETER Format - Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) - -.EXAMPLE - .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" - -.EXAMPLE - .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$false)] - [string]$V8Path, - - [Parameter(Mandatory=$false)] - [string]$InfoBasePath, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseServer, - - [Parameter(Mandatory=$false)] - [string]$InfoBaseRef, - - [Parameter(Mandatory=$false)] - [string]$UserName, - - [Parameter(Mandatory=$false)] - [string]$Password, - - [Parameter(Mandatory=$true)] - [string]$InputFile, - - [Parameter(Mandatory=$true)] - [string]$OutputDir, - - [Parameter(Mandatory=$false)] - [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical" -) - -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve V8Path --- -function Find-ProjectV8Path { - $dir = (Get-Location).Path - while ($dir) { - $pf = Join-Path $dir ".v8-project.json" - if (Test-Path $pf) { - try { - $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json - if ($j.v8path) { return [string]$j.v8path } - } catch {} - return $null - } - $parent = Split-Path $dir -Parent - if (-not $parent -or $parent -eq $dir) { break } - $dir = $parent - } - return $null -} - -if (-not $V8Path) { - $V8Path = Find-ProjectV8Path -} -if (-not $V8Path) { - $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | - Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | - Select-Object -First 1 - if ($found) { - $V8Path = $found.FullName - Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow - } else { - Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red - exit 1 - } -} -if (Test-Path $V8Path -PathType Container) { - $V8Path = Join-Path $V8Path "1cv8.exe" -} - -if (-not (Test-Path $V8Path)) { - Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red - exit 1 -} - -# --- Validate database connection --- -if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red - Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow - exit 1 -} - -# --- Detect engine (ibcmd vs 1cv8) by exe name --- -$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } -if ($engine -eq "ibcmd") { - if (-not $InfoBasePath) { - Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red - exit 1 - } - if ($Format -eq "Plain") { - Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red - exit 1 - } -} - -# --- Validate input file --- -if (-not (Test-Path $InputFile)) { - Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red - exit 1 -} - -# --- Ensure output directory exists --- -if (-not (Test-Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null -} - -# --- Temp dir --- -$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - -try { - if ($engine -eq "ibcmd") { - # --- ibcmd branch: dump EPF/ERF via config export --file --- - $arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath") - if ($UserName) { $arguments += "--user=$UserName" } - if ($Password) { $arguments += "--password=$Password" } - $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" - $output = & $V8Path @arguments 2>&1 - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { - Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green - } else { - Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red - } - if ($output) { Write-Host ($output | Out-String) } - exit $exitCode - } - - # --- 1cv8 branch --- - # --- Build arguments --- - $arguments = @("DESIGNER") - - if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" - } else { - $arguments += "/F", "`"$InfoBasePath`"" - } - - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } - - $arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" - $arguments += "-Format", $Format - - # --- Output --- - $outFile = Join-Path $tempDir "dump_log.txt" - $arguments += "/Out", "`"$outFile`"" - $arguments += "/DisableStartupDialogs" - - # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" - $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru - $exitCode = $process.ExitCode - - # --- Result --- - if ($exitCode -eq 0) { - Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green - } else { - Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red - } - - if (Test-Path $outFile) { - $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue - if ($logContent) { - Write-Host "--- Log ---" - Write-Host $logContent - Write-Host "--- End ---" - } - } - - exit $exitCode - -} finally { - if (Test-Path $tempDir) { - Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} +# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Разборка внешней обработки/отчёта 1С в XML-исходники + +.DESCRIPTION + Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. + Общий скрипт для epf-dump и erf-dump. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER InputFile + Путь к EPF/ERF-файлу + +.PARAMETER OutputDir + Каталог для выгрузки исходников + +.PARAMETER Format + Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" + +.EXAMPLE + .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$InputFile, + + [Parameter(Mandatory=$true)] + [string]$OutputDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical" +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate database connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red + Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow + exit 1 +} + +# --- Detect engine (ibcmd vs 1cv8) by exe name --- +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +if ($engine -eq "ibcmd") { + if (-not $InfoBasePath) { + Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red + exit 1 + } + if ($Format -eq "Plain") { + Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red + exit 1 + } +} + +# --- Validate input file --- +if (-not (Test-Path $InputFile)) { + Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + if ($engine -eq "ibcmd") { + # --- ibcmd branch: dump EPF/ERF via config export --file --- + $arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath") + if ($UserName) { $arguments += "--user=$UserName" } + if ($Password) { $arguments += "--password=$Password" } + $arguments += "--data=$tempDir" + Write-Host "Running: ibcmd $($arguments -join ' ')" + $output = & $V8Path @arguments 2>&1 + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green + } else { + Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red + } + if ($output) { Write-Host ($output | Out-String) } + exit $exitCode + } + + # --- 1cv8 branch --- + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" + $arguments += "-Format", $Format + + # --- Output --- + $outFile = Join-Path $tempDir "dump_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green + } else { + Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/epf-init/SKILL.md b/.claude/skills/epf-init/SKILL.md index b1f54521..85572a69 100644 --- a/.claude/skills/epf-init/SKILL.md +++ b/.claude/skills/epf-init/SKILL.md @@ -1,41 +1,41 @@ ---- -name: epf-init -description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля -argument-hint: [Synonym] -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /epf-init — Создание новой обработки - -Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки. - -## Usage - -``` -/epf-init [Synonym] [SrcDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|-----------|:------------:|--------------|-------------------------------------| -| Name | да | — | Имя обработки (латиница/кириллица) | -| Synonym | нет | = Name | Синоним (отображаемое имя) | -| SrcDir | нет | `src` | Каталог исходников относительно CWD | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] -``` - -## Дальнейшие шаги - -- Добавить форму: `/form-add` -- Добавить макет: `/template-add` -- Добавить справку: `/help-add` -- Собрать EPF: `/epf-build` +--- +name: epf-init +description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля +argument-hint: [Synonym] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /epf-init — Создание новой обработки + +Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки. + +## Usage + +``` +/epf-init [Synonym] [SrcDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|-------------------------------------| +| Name | да | — | Имя обработки (латиница/кириллица) | +| Synonym | нет | = Name | Синоним (отображаемое имя) | +| SrcDir | нет | `src` | Каталог исходников относительно CWD | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] +``` + +## Дальнейшие шаги + +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Добавить справку: `/help-add` +- Собрать EPF: `/epf-build` diff --git a/.claude/skills/epf-init/scripts/init.ps1 b/.claude/skills/epf-init/scripts/init.ps1 index 966caa3a..611659b4 100644 --- a/.claude/skills/epf-init/scripts/init.ps1 +++ b/.claude/skills/epf-init/scripts/init.ps1 @@ -1,90 +1,90 @@ -# epf-init v1.1 — Init 1C external data processor scaffold -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$Name, - - [string]$Synonym = $Name, - - [string]$SrcDir = "src" -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 - -$uuid1 = [guid]::NewGuid().ToString() -$uuid2 = [guid]::NewGuid().ToString() -$uuid3 = [guid]::NewGuid().ToString() -$uuid4 = [guid]::NewGuid().ToString() - -$xml = @" - - - - - - c3831ec8-d8d5-4f93-8a22-f9bfae07327f - $uuid2 - - - $uuid3 - $uuid4 - - - - $Name - - - ru - $Synonym - - - - - - - - - -"@ - -$rootFile = Join-Path $SrcDir "$Name.xml" -$processorDir = Join-Path $SrcDir $Name - -if (Test-Path $rootFile) { - Write-Error "Файл уже существует: $rootFile" - exit 1 -} - -if (-not (Test-Path $SrcDir)) { - New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null -} -$extDir = Join-Path $processorDir "Ext" -New-Item -ItemType Directory -Path $extDir -Force | Out-Null - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) - -# --- Модуль объекта --- - -$moduleBsl = @" -#Область ОписаниеПеременных - -#КонецОбласти - -#Область ПрограммныйИнтерфейс - -#КонецОбласти - -#Область СлужебныеПроцедурыИФункции - -#КонецОбласти -"@ - -$modulePath = Join-Path $extDir "ObjectModule.bsl" -[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) - -Write-Host "[OK] Создана обработка: $rootFile" -Write-Host " Каталог: $processorDir" -Write-Host " Модуль: $modulePath" +# epf-init v1.1 — Init 1C external data processor scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + + [string]$Synonym = $Name, + + [string]$SrcDir = "src" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +$uuid1 = [guid]::NewGuid().ToString() +$uuid2 = [guid]::NewGuid().ToString() +$uuid3 = [guid]::NewGuid().ToString() +$uuid4 = [guid]::NewGuid().ToString() + +$xml = @" + + + + + + c3831ec8-d8d5-4f93-8a22-f9bfae07327f + $uuid2 + + + $uuid3 + $uuid4 + + + + $Name + + + ru + $Synonym + + + + + + + + + +"@ + +$rootFile = Join-Path $SrcDir "$Name.xml" +$processorDir = Join-Path $SrcDir $Name + +if (Test-Path $rootFile) { + Write-Error "Файл уже существует: $rootFile" + exit 1 +} + +if (-not (Test-Path $SrcDir)) { + New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null +} +$extDir = Join-Path $processorDir "Ext" +New-Item -ItemType Directory -Path $extDir -Force | Out-Null + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) + +# --- Модуль объекта --- + +$moduleBsl = @" +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +"@ + +$modulePath = Join-Path $extDir "ObjectModule.bsl" +[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) + +Write-Host "[OK] Создана обработка: $rootFile" +Write-Host " Каталог: $processorDir" +Write-Host " Модуль: $modulePath" diff --git a/.claude/skills/epf-validate/SKILL.md b/.claude/skills/epf-validate/SKILL.md index f51623ba..234407be 100644 --- a/.claude/skills/epf-validate/SKILL.md +++ b/.claude/skills/epf-validate/SKILL.md @@ -1,30 +1,30 @@ ---- -name: epf-validate -description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /epf-validate — валидация внешней обработки (EPF) - -Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|------------|:-----:|---------|-------------------------------------------------| -| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" -``` - +--- +name: epf-validate +description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /epf-validate — валидация внешней обработки (EPF) + +Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" +``` + diff --git a/.claude/skills/epf-validate/scripts/epf-validate.ps1 b/.claude/skills/epf-validate/scripts/epf-validate.ps1 index 000219c5..62d928e4 100644 --- a/.claude/skills/epf-validate/scripts/epf-validate.ps1 +++ b/.claude/skills/epf-validate/scripts/epf-validate.ps1 @@ -1,842 +1,842 @@ -# epf-validate v1.2 — Validate 1C external data processor / report structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$ObjectPath, - - [switch]$Detailed, - - [int]$MaxErrors = 30, - - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- - -if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { - $ObjectPath = Join-Path (Get-Location).Path $ObjectPath -} - -if (Test-Path $ObjectPath -PathType Container) { - $dirName = Split-Path $ObjectPath -Leaf - $candidate = Join-Path $ObjectPath "$dirName.xml" - $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" - if (Test-Path $candidate) { - $ObjectPath = $candidate - } elseif (Test-Path $sibling) { - $ObjectPath = $sibling - } else { - $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) - if ($xmlFiles.Count -gt 0) { - $ObjectPath = $xmlFiles[0].FullName - } else { - Write-Host "[ERROR] No XML file found in directory: $ObjectPath" - exit 1 - } - } -} - -# File not found — check Dir/Name/Name.xml → Dir/Name.xml -if (-not (Test-Path $ObjectPath)) { - $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) - $parentDir = Split-Path $ObjectPath - $parentDirName = Split-Path $parentDir -Leaf - if ($fileName -eq $parentDirName) { - $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" - if (Test-Path $candidate) { $ObjectPath = $candidate } - } -} -if (-not (Test-Path $ObjectPath)) { - Write-Host "[ERROR] File not found: $ObjectPath" - exit 1 -} - -$resolvedPath = (Resolve-Path $ObjectPath).Path -$srcDir = Split-Path $resolvedPath -Parent - -# --- Output infrastructure --- - -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$finalize = { - $checks = $script:okCount + $script:errors + $script:warnings - if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: $shortType.$objName ($checks checks) ===" - } else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() - } - Write-Host $result - - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" - } -} - -# --- Reference tables --- - -$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' - -$classIds = @{ - "ExternalDataProcessor" = "c3831ec8-d8d5-4f93-8a22-f9bfae07327f" - "ExternalReport" = "e41aff26-25cf-4bb6-b6c1-3f478a75f374" -} - -$allowedChildTypes = @("Attribute","TabularSection","Form","Template","Command") - -# Expected order of child types in ChildObjects -$childTypeOrder = @{ - "Attribute" = 0 - "TabularSection" = 1 - "Form" = 2 - "Template" = 3 - "Command" = 4 -} - -$validPropertyValues = @{ - "FillChecking" = @("DontCheck","ShowError","ShowWarning") - "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") -} - -# --- 1. Parse XML --- - -Out-Line "" - -$xmlDoc = $null -try { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $false - $xmlDoc.Load($resolvedPath) -} catch { - Out-Line "=== Validation: (parse failed) ===" - Out-Line "" - Report-Error "1. XML parse failed: $($_.Exception.Message)" - & $finalize - exit 1 -} - -# --- Register namespaces --- - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") - -$root = $xmlDoc.DocumentElement - -# --- Check 1: Root structure --- - -$check1Ok = $true - -if ($root.LocalName -ne "MetaDataObject") { - Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" - & $finalize - exit 1 -} - -$expectedNs = "http://v8.1c.ru/8.3/MDClasses" -if ($root.NamespaceURI -ne $expectedNs) { - Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" - $check1Ok = $false -} - -$version = $root.GetAttribute("version") -if (-not $version) { - Report-Warn "1. Missing version attribute on MetaDataObject" -} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { - Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" -} - -# Detect type: ExternalDataProcessor or ExternalReport -$typeNode = $null -$mdType = "" -$childElements = @() -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { - $childElements += $child - } -} - -if ($childElements.Count -eq 0) { - Report-Error "1. No metadata type element found inside MetaDataObject" - & $finalize - exit 1 -} elseif ($childElements.Count -gt 1) { - Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" - $check1Ok = $false -} - -$typeNode = $childElements[0] -$mdType = $typeNode.LocalName - -if ($mdType -ne "ExternalDataProcessor" -and $mdType -ne "ExternalReport") { - Report-Error "1. Unexpected type '$mdType' (expected ExternalDataProcessor or ExternalReport)" - & $finalize - exit 1 -} - -$typeUuid = $typeNode.GetAttribute("uuid") -if (-not $typeUuid) { - Report-Error "1. Missing uuid on <$mdType>" - $check1Ok = $false -} elseif ($typeUuid -notmatch $guidPattern) { - Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" - $check1Ok = $false -} - -# Get object name -$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) -$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } -$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } - -$shortType = if ($mdType -eq "ExternalDataProcessor") { "EPF" } else { "ERF" } -$script:output.Insert(0, "=== Validation: $shortType.$objName ===$([Environment]::NewLine)") | Out-Null - -if ($check1Ok) { - Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 2: InternalInfo --- - -$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) - -if (-not $internalInfo) { - Report-Error "2. InternalInfo block missing" -} else { - $check2Ok = $true - - # ContainedObject / ClassId - $containedObj = $internalInfo.SelectSingleNode("xr:ContainedObject", $ns) - if (-not $containedObj) { - Report-Error "2. InternalInfo: missing xr:ContainedObject" - $check2Ok = $false - } else { - $classIdNode = $containedObj.SelectSingleNode("xr:ClassId", $ns) - $objectIdNode = $containedObj.SelectSingleNode("xr:ObjectId", $ns) - - $expectedClassId = $classIds[$mdType] - if (-not $classIdNode -or -not $classIdNode.InnerText) { - Report-Error "2. Missing ClassId in ContainedObject" - $check2Ok = $false - } elseif ($classIdNode.InnerText -ne $expectedClassId) { - Report-Error "2. ClassId is '$($classIdNode.InnerText)', expected '$expectedClassId' for $mdType" - $check2Ok = $false - } - - if ($objectIdNode -and $objectIdNode.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid ObjectId UUID" - $check2Ok = $false - } - } - - # GeneratedType — expect exactly 1 with category "Object" - $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) - if ($genTypes.Count -eq 0) { - Report-Error "2. No GeneratedType entries found" - $check2Ok = $false - } else { - foreach ($gt in $genTypes) { - $gtName = $gt.GetAttribute("name") - $gtCategory = $gt.GetAttribute("category") - - if ($gtCategory -ne "Object") { - Report-Warn "2. Unexpected GeneratedType category '$gtCategory' (expected 'Object')" - } - - # Name format: ExternalDataProcessorObject.Name or ExternalReportObject.Name - $expectedPrefix = "${mdType}Object." - if ($gtName -and $objName -ne "(unknown)" -and -not $gtName.StartsWith($expectedPrefix)) { - Report-Warn "2. GeneratedType name '$gtName' does not start with '$expectedPrefix'" - } - - $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) - $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) - if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid TypeId UUID in GeneratedType" - $check2Ok = $false - } - if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid ValueId UUID in GeneratedType" - $check2Ok = $false - } - } - } - - if ($check2Ok) { - Report-OK "2. InternalInfo: ClassId correct, $($genTypes.Count) GeneratedType" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 3: Properties --- - -if (-not $propsNode) { - Report-Error "3. Properties block missing" -} else { - $check3Ok = $true - - # Name - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "3. Properties: Name is missing or empty" - $check3Ok = $false - } else { - $nameVal = $nameNode.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" - $check3Ok = $false - } - if ($nameVal.Length -gt 80) { - Report-Warn "3. Properties: Name '$nameVal' exceeds 80 characters ($($nameVal.Length))" - } - } - - # Synonym - $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) - $synPresent = $false - if ($synNode) { - $synItem = $synNode.SelectSingleNode("v8:item", $ns) - if ($synItem) { - $synContent = $synItem.SelectSingleNode("v8:content", $ns) - if ($synContent -and $synContent.InnerText) { - $synPresent = $true - } - } - } - - # DefaultForm cross-reference (collected now, checked after ChildObjects) - $defaultFormNode = $propsNode.SelectSingleNode("md:DefaultForm", $ns) - $defaultFormVal = if ($defaultFormNode -and $defaultFormNode.InnerText.Trim()) { $defaultFormNode.InnerText.Trim() } else { "" } - - # AuxiliaryForm cross-reference - $auxFormNode = $propsNode.SelectSingleNode("md:AuxiliaryForm", $ns) - $auxFormVal = if ($auxFormNode -and $auxFormNode.InnerText.Trim()) { $auxFormNode.InnerText.Trim() } else { "" } - - # ERF-specific: MainDataCompositionSchema - $mainDCSVal = "" - if ($mdType -eq "ExternalReport") { - $mainDCSNode = $propsNode.SelectSingleNode("md:MainDataCompositionSchema", $ns) - $mainDCSVal = if ($mainDCSNode -and $mainDCSNode.InnerText.Trim()) { $mainDCSNode.InnerText.Trim() } else { "" } - } - - if ($check3Ok) { - $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } - $extras = "" - if ($defaultFormVal) { $extras += ", DefaultForm set" } - if ($mainDCSVal) { $extras += ", MainDCS set" } - Report-OK "3. Properties: Name=`"$objName`", $synInfo$extras" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 4: ChildObjects — allowed types and ordering --- - -$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) -$formNames = @() -$templateNames = @() -$allChildNames = @{} - -if ($childObjNode) { - $check4Ok = $true - $childCounts = @{} - $lastOrder = -1 - $orderOk = $true - - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childTag = $child.LocalName - - if ($allowedChildTypes -notcontains $childTag) { - Report-Error "4. ChildObjects: disallowed element '$childTag'" - $check4Ok = $false - continue - } - - if (-not $childCounts.ContainsKey($childTag)) { - $childCounts[$childTag] = 0 - } - $childCounts[$childTag]++ - - # Check ordering - $thisOrder = $childTypeOrder[$childTag] - if ($thisOrder -lt $lastOrder -and $orderOk) { - Report-Warn "4. ChildObjects: '$childTag' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)" - $orderOk = $false - } - $lastOrder = $thisOrder - - # Collect Form and Template names (simple text content) - if ($childTag -eq "Form") { - $formNames += $child.InnerText.Trim() - } elseif ($childTag -eq "Template") { - $templateNames += $child.InnerText.Trim() - } - } - - if ($check4Ok) { - $summary = ($childCounts.GetEnumerator() | Sort-Object { $childTypeOrder[$_.Name] } | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " - if ($summary) { - Report-OK "4. ChildObjects: $summary" - } else { - Report-OK "4. ChildObjects: empty" - } - } -} else { - Report-OK "4. ChildObjects: absent" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 5: DefaultForm / MainDCS cross-references --- - -$check5Ok = $true - -if ($defaultFormVal) { - # Format: ExternalDataProcessor.Name.Form.FormName or ExternalReport.Name.Form.FormName - $expectedPrefix = "$mdType.$objName.Form." - if ($defaultFormVal.StartsWith($expectedPrefix)) { - $refFormName = $defaultFormVal.Substring($expectedPrefix.Length) - if ($formNames -notcontains $refFormName) { - Report-Error "5. DefaultForm references '$refFormName', but no such Form in ChildObjects" - $check5Ok = $false - } - } else { - Report-Warn "5. DefaultForm value '$defaultFormVal' has unexpected prefix (expected '$expectedPrefix...')" - } -} - -if ($auxFormVal) { - $expectedPrefix = "$mdType.$objName.Form." - if ($auxFormVal.StartsWith($expectedPrefix)) { - $refFormName = $auxFormVal.Substring($expectedPrefix.Length) - if ($formNames -notcontains $refFormName) { - Report-Error "5. AuxiliaryForm references '$refFormName', but no such Form in ChildObjects" - $check5Ok = $false - } - } -} - -if ($mainDCSVal -and $mdType -eq "ExternalReport") { - $expectedPrefix = "ExternalReport.$objName.Template." - if ($mainDCSVal.StartsWith($expectedPrefix)) { - $refTplName = $mainDCSVal.Substring($expectedPrefix.Length) - if ($templateNames -notcontains $refTplName) { - Report-Error "5. MainDataCompositionSchema references '$refTplName', but no such Template in ChildObjects" - $check5Ok = $false - } - } else { - Report-Warn "5. MainDataCompositionSchema value '$mainDCSVal' has unexpected prefix" - } -} - -if ($check5Ok) { - $refs = @() - if ($defaultFormVal) { $refs += "DefaultForm" } - if ($auxFormVal) { $refs += "AuxiliaryForm" } - if ($mainDCSVal) { $refs += "MainDCS" } - if ($refs.Count -gt 0) { - Report-OK "5. Cross-references: $($refs -join ', ') valid" - } else { - Report-OK "5. Cross-references: none to check" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 6: Attributes — UUID, Name, Type --- - -function Check-Attribute { - param( - [System.Xml.XmlNode]$node, - [string]$context - ) - - $uuid = $node.GetAttribute("uuid") - if (-not $uuid) { - Report-Error "6. $context Attribute missing uuid" - return $false - } elseif ($uuid -notmatch $guidPattern) { - Report-Error "6. $context Attribute has invalid uuid '$uuid'" - return $false - } - - $elProps = $node.SelectSingleNode("md:Properties", $ns) - if (-not $elProps) { - Report-Error "6. $context Attribute (uuid=$uuid) missing Properties" - return $false - } - - $elName = $elProps.SelectSingleNode("md:Name", $ns) - if (-not $elName -or -not $elName.InnerText) { - Report-Error "6. $context Attribute (uuid=$uuid) missing or empty Name" - return $false - } - - $nameVal = $elName.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "6. $context Attribute '$nameVal' has invalid identifier" - return $false - } - - $typeEl = $elProps.SelectSingleNode("md:Type", $ns) - if (-not $typeEl) { - Report-Error "6. $context Attribute '$nameVal' missing Type block" - return $false - } - $v8Types = $typeEl.SelectNodes("v8:Type", $ns) - $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) - if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { - Report-Error "6. $context Attribute '$nameVal' Type block has no v8:Type or v8:TypeSet" - return $false - } - - return $true -} - -if ($childObjNode) { - $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) - $check6Ok = $true - $attrCount = 0 - - foreach ($attr in $attrs) { - if ($script:stopped) { break } - $ok = Check-Attribute -node $attr -context "" - if (-not $ok) { $check6Ok = $false } - $attrCount++ - - # Collect name for uniqueness - $ap = $attr.SelectSingleNode("md:Properties/md:Name", $ns) - if ($ap -and $ap.InnerText) { - $allChildNames["Attr:$($ap.InnerText)"] = $ap.InnerText - } - } - - if ($attrCount -gt 0) { - if ($check6Ok) { - Report-OK "6. Attributes: $attrCount checked (UUID, Name, Type)" - } - } else { - Report-OK "6. Attributes: none" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 7: TabularSections --- - -if ($childObjNode) { - $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) - if ($tsSections.Count -gt 0) { - $check7Ok = $true - $tsCount = 0 - $tsAttrTotal = 0 - - foreach ($ts in $tsSections) { - if ($script:stopped) { break } - $tsCount++ - - $tsUuid = $ts.GetAttribute("uuid") - if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { - Report-Error "7. TabularSection #${tsCount}: invalid or missing uuid" - $check7Ok = $false - } - - $tsProps = $ts.SelectSingleNode("md:Properties", $ns) - $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } - $tsName = if ($tsNameNode -and $tsNameNode.InnerText) { $tsNameNode.InnerText } else { "(unnamed)" } - - if (-not $tsNameNode -or -not $tsNameNode.InnerText) { - Report-Error "7. TabularSection #${tsCount}: missing or empty Name" - $check7Ok = $false - } elseif ($tsName -notmatch $identPattern) { - Report-Error "7. TabularSection '$tsName': invalid identifier" - $check7Ok = $false - } - - $allChildNames["TS:$tsName"] = $tsName - - # InternalInfo — expect 2 GeneratedType - $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) - if ($tsIntInfo) { - $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) - if ($tsGens.Count -lt 2) { - Report-Warn "7. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" - } - } - - # Inner attributes - $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) - if ($tsChildObj) { - $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) - $tsAttrNames = @{} - foreach ($ta in $tsAttrs) { - $taOk = Check-Attribute -node $ta -context "TabularSection '$tsName'." - if (-not $taOk) { $check7Ok = $false } - $tsAttrTotal++ - - $taProps = $ta.SelectSingleNode("md:Properties/md:Name", $ns) - if ($taProps -and $taProps.InnerText) { - if ($tsAttrNames.ContainsKey($taProps.InnerText)) { - Report-Error "7. Duplicate attribute '$($taProps.InnerText)' in TabularSection '$tsName'" - $check7Ok = $false - } else { - $tsAttrNames[$taProps.InnerText] = $true - } - } - } - } - } - - if ($check7Ok) { - Report-OK "7. TabularSections: $tsCount sections, $tsAttrTotal inner attributes" - } - } else { - Report-OK "7. TabularSections: none" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 8: Name uniqueness --- - -$check8Ok = $true - -# Collect all names: attributes + tabular sections + forms + templates + commands -$allNames = @{} - -if ($childObjNode) { - $nameKinds = @( - @{ XPath = "md:Attribute"; Kind = "Attribute" }, - @{ XPath = "md:TabularSection"; Kind = "TabularSection" }, - @{ XPath = "md:Command"; Kind = "Command" } - ) - - foreach ($nk in $nameKinds) { - $nodes = $childObjNode.SelectNodes($nk.XPath, $ns) - foreach ($node in $nodes) { - $np = $node.SelectSingleNode("md:Properties/md:Name", $ns) - if ($np -and $np.InnerText) { - $nameVal = $np.InnerText - $key = "$($nk.Kind):$nameVal" - if ($allNames.ContainsKey($nameVal)) { - Report-Error "8. Duplicate name '$nameVal' ($($nk.Kind) conflicts with $($allNames[$nameVal]))" - $check8Ok = $false - } else { - $allNames[$nameVal] = $nk.Kind - } - } - } - } - - # Forms and Templates are simple text nodes - foreach ($fn in $formNames) { - if ($allNames.ContainsKey($fn)) { - Report-Error "8. Duplicate name '$fn' (Form conflicts with $($allNames[$fn]))" - $check8Ok = $false - } else { - $allNames[$fn] = "Form" - } - } - foreach ($tn in $templateNames) { - if ($allNames.ContainsKey($tn)) { - Report-Error "8. Duplicate name '$tn' (Template conflicts with $($allNames[$tn]))" - $check8Ok = $false - } else { - $allNames[$tn] = "Template" - } - } -} - -if ($check8Ok) { - Report-OK "8. Name uniqueness: $($allNames.Count) names, all unique" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 9: File existence (forms and templates on disk) --- - -$check9Ok = $true -$filesChecked = 0 - -# Object directory: same level as root XML, named after the object -$objDir = Join-Path $srcDir $objName - -foreach ($fn in $formNames) { - # FormName.xml — form descriptor - $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" - if (-not (Test-Path $formMetaXml)) { - Report-Error "9. Missing form descriptor: Forms/$fn.xml" - $check9Ok = $false - } else { - $filesChecked++ - } - - # FormName/Ext/Form.xml — form layout - $formXml = Join-Path (Join-Path (Join-Path (Join-Path $objDir "Forms") $fn) "Ext") "Form.xml" - if (-not (Test-Path $formXml)) { - Report-Error "9. Missing form layout: Forms/$fn/Ext/Form.xml" - $check9Ok = $false - } else { - $filesChecked++ - } -} - -foreach ($tn in $templateNames) { - # TemplateName.xml — template descriptor - $tplMetaXml = Join-Path (Join-Path $objDir "Templates") "$tn.xml" - if (-not (Test-Path $tplMetaXml)) { - Report-Error "9. Missing template descriptor: Templates/$tn.xml" - $check9Ok = $false - } else { - $filesChecked++ - } - - # TemplateName/Ext/Template.* — template content (extension varies) - $tplExtDir = Join-Path (Join-Path (Join-Path $objDir "Templates") $tn) "Ext" - if (Test-Path $tplExtDir) { - $tplFiles = @(Get-ChildItem $tplExtDir -Filter "Template.*" -File) - if ($tplFiles.Count -eq 0) { - Report-Error "9. Missing template content: Templates/$tn/Ext/Template.*" - $check9Ok = $false - } else { - $filesChecked++ - } - } else { - Report-Error "9. Missing template Ext directory: Templates/$tn/Ext/" - $check9Ok = $false - } -} - -# ObjectModule.bsl -$objModule = Join-Path (Join-Path $objDir "Ext") "ObjectModule.bsl" -if (Test-Path $objModule) { - $filesChecked++ -} - -if ($check9Ok) { - if ($filesChecked -gt 0) { - Report-OK "9. File existence: $filesChecked files verified" - } else { - Report-OK "9. File existence: no forms/templates to check" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 10: Form descriptors structure --- - -$check10Ok = $true -$formsChecked = 0 - -foreach ($fn in $formNames) { - $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" - if (-not (Test-Path $formMetaXml)) { continue } - - try { - $fDoc = New-Object System.Xml.XmlDocument - $fDoc.PreserveWhitespace = $false - $fDoc.Load($formMetaXml) - $fRoot = $fDoc.DocumentElement - - if ($fRoot.LocalName -ne "MetaDataObject") { - Report-Error "10. Form '$fn': root element is '$($fRoot.LocalName)', expected 'MetaDataObject'" - $check10Ok = $false - continue - } - - $fTypeNode = $fRoot.SelectSingleNode("md:Form", $ns) - if (-not $fTypeNode) { - Report-Error "10. Form '$fn': missing
element" - $check10Ok = $false - continue - } - - $fUuid = $fTypeNode.GetAttribute("uuid") - if (-not $fUuid -or $fUuid -notmatch $guidPattern) { - Report-Error "10. Form '$fn': invalid or missing uuid" - $check10Ok = $false - } - - $fProps = $fTypeNode.SelectSingleNode("md:Properties", $ns) - if ($fProps) { - $fName = $fProps.SelectSingleNode("md:Name", $ns) - if ($fName -and $fName.InnerText -ne $fn) { - Report-Error "10. Form '$fn': Name in descriptor is '$($fName.InnerText)', expected '$fn'" - $check10Ok = $false - } - - # FormType should be Managed - $fType = $fProps.SelectSingleNode("md:FormType", $ns) - if ($fType -and $fType.InnerText -ne "Managed") { - Report-Warn "10. Form '$fn': FormType is '$($fType.InnerText)' (expected 'Managed')" - } - } - - $formsChecked++ - } catch { - Report-Error "10. Form '$fn': XML parse error: $($_.Exception.Message)" - $check10Ok = $false - } -} - -if ($check10Ok) { - if ($formsChecked -gt 0) { - Report-OK "10. Form descriptors: $formsChecked checked" - } else { - Report-OK "10. Form descriptors: none to check" - } -} - -# --- Final output --- - -& $finalize - -if ($script:errors -gt 0) { - exit 1 -} -exit 0 +# epf-validate v1.2 — Validate 1C external data processor / report structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ObjectPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } elseif (Test-Path $sibling) { + $ObjectPath = $sibling + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $ObjectPath)) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) + $parentDir = Split-Path $ObjectPath + $parentDirName = Split-Path $parentDir -Leaf + if ($fileName -eq $parentDirName) { + $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ObjectPath).Path +$srcDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: $shortType.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- + +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +$classIds = @{ + "ExternalDataProcessor" = "c3831ec8-d8d5-4f93-8a22-f9bfae07327f" + "ExternalReport" = "e41aff26-25cf-4bb6-b6c1-3f478a75f374" +} + +$allowedChildTypes = @("Attribute","TabularSection","Form","Template","Command") + +# Expected order of child types in ChildObjects +$childTypeOrder = @{ + "Attribute" = 0 + "TabularSection" = 1 + "Form" = 2 + "Template" = 3 + "Command" = 4 +} + +$validPropertyValues = @{ + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") +} + +# --- 1. Parse XML --- + +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- + +$check1Ok = $true + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Detect type: ExternalDataProcessor or ExternalReport +$typeNode = $null +$mdType = "" +$childElements = @() +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { + $childElements += $child + } +} + +if ($childElements.Count -eq 0) { + Report-Error "1. No metadata type element found inside MetaDataObject" + & $finalize + exit 1 +} elseif ($childElements.Count -gt 1) { + Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" + $check1Ok = $false +} + +$typeNode = $childElements[0] +$mdType = $typeNode.LocalName + +if ($mdType -ne "ExternalDataProcessor" -and $mdType -ne "ExternalReport") { + Report-Error "1. Unexpected type '$mdType' (expected ExternalDataProcessor or ExternalReport)" + & $finalize + exit 1 +} + +$typeUuid = $typeNode.GetAttribute("uuid") +if (-not $typeUuid) { + Report-Error "1. Missing uuid on <$mdType>" + $check1Ok = $false +} elseif ($typeUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" + $check1Ok = $false +} + +# Get object name +$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$shortType = if ($mdType -eq "ExternalDataProcessor") { "EPF" } else { "ERF" } +$script:output.Insert(0, "=== Validation: $shortType.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- + +$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) + +if (-not $internalInfo) { + Report-Error "2. InternalInfo block missing" +} else { + $check2Ok = $true + + # ContainedObject / ClassId + $containedObj = $internalInfo.SelectSingleNode("xr:ContainedObject", $ns) + if (-not $containedObj) { + Report-Error "2. InternalInfo: missing xr:ContainedObject" + $check2Ok = $false + } else { + $classIdNode = $containedObj.SelectSingleNode("xr:ClassId", $ns) + $objectIdNode = $containedObj.SelectSingleNode("xr:ObjectId", $ns) + + $expectedClassId = $classIds[$mdType] + if (-not $classIdNode -or -not $classIdNode.InnerText) { + Report-Error "2. Missing ClassId in ContainedObject" + $check2Ok = $false + } elseif ($classIdNode.InnerText -ne $expectedClassId) { + Report-Error "2. ClassId is '$($classIdNode.InnerText)', expected '$expectedClassId' for $mdType" + $check2Ok = $false + } + + if ($objectIdNode -and $objectIdNode.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId UUID" + $check2Ok = $false + } + } + + # GeneratedType — expect exactly 1 with category "Object" + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + if ($genTypes.Count -eq 0) { + Report-Error "2. No GeneratedType entries found" + $check2Ok = $false + } else { + foreach ($gt in $genTypes) { + $gtName = $gt.GetAttribute("name") + $gtCategory = $gt.GetAttribute("category") + + if ($gtCategory -ne "Object") { + Report-Warn "2. Unexpected GeneratedType category '$gtCategory' (expected 'Object')" + } + + # Name format: ExternalDataProcessorObject.Name or ExternalReportObject.Name + $expectedPrefix = "${mdType}Object." + if ($gtName -and $objName -ne "(unknown)" -and -not $gtName.StartsWith($expectedPrefix)) { + Report-Warn "2. GeneratedType name '$gtName' does not start with '$expectedPrefix'" + } + + $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) + $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) + if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid TypeId UUID in GeneratedType" + $check2Ok = $false + } + if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ValueId UUID in GeneratedType" + $check2Ok = $false + } + } + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: ClassId correct, $($genTypes.Count) GeneratedType" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties --- + +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + if ($nameVal.Length -gt 80) { + Report-Warn "3. Properties: Name '$nameVal' exceeds 80 characters ($($nameVal.Length))" + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { + $synPresent = $true + } + } + } + + # DefaultForm cross-reference (collected now, checked after ChildObjects) + $defaultFormNode = $propsNode.SelectSingleNode("md:DefaultForm", $ns) + $defaultFormVal = if ($defaultFormNode -and $defaultFormNode.InnerText.Trim()) { $defaultFormNode.InnerText.Trim() } else { "" } + + # AuxiliaryForm cross-reference + $auxFormNode = $propsNode.SelectSingleNode("md:AuxiliaryForm", $ns) + $auxFormVal = if ($auxFormNode -and $auxFormNode.InnerText.Trim()) { $auxFormNode.InnerText.Trim() } else { "" } + + # ERF-specific: MainDataCompositionSchema + $mainDCSVal = "" + if ($mdType -eq "ExternalReport") { + $mainDCSNode = $propsNode.SelectSingleNode("md:MainDataCompositionSchema", $ns) + $mainDCSVal = if ($mainDCSNode -and $mainDCSNode.InnerText.Trim()) { $mainDCSNode.InnerText.Trim() } else { "" } + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + $extras = "" + if ($defaultFormVal) { $extras += ", DefaultForm set" } + if ($mainDCSVal) { $extras += ", MainDCS set" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo$extras" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: ChildObjects — allowed types and ordering --- + +$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$formNames = @() +$templateNames = @() +$allChildNames = @{} + +if ($childObjNode) { + $check4Ok = $true + $childCounts = @{} + $lastOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTag = $child.LocalName + + if ($allowedChildTypes -notcontains $childTag) { + Report-Error "4. ChildObjects: disallowed element '$childTag'" + $check4Ok = $false + continue + } + + if (-not $childCounts.ContainsKey($childTag)) { + $childCounts[$childTag] = 0 + } + $childCounts[$childTag]++ + + # Check ordering + $thisOrder = $childTypeOrder[$childTag] + if ($thisOrder -lt $lastOrder -and $orderOk) { + Report-Warn "4. ChildObjects: '$childTag' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)" + $orderOk = $false + } + $lastOrder = $thisOrder + + # Collect Form and Template names (simple text content) + if ($childTag -eq "Form") { + $formNames += $child.InnerText.Trim() + } elseif ($childTag -eq "Template") { + $templateNames += $child.InnerText.Trim() + } + } + + if ($check4Ok) { + $summary = ($childCounts.GetEnumerator() | Sort-Object { $childTypeOrder[$_.Name] } | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " + if ($summary) { + Report-OK "4. ChildObjects: $summary" + } else { + Report-OK "4. ChildObjects: empty" + } + } +} else { + Report-OK "4. ChildObjects: absent" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: DefaultForm / MainDCS cross-references --- + +$check5Ok = $true + +if ($defaultFormVal) { + # Format: ExternalDataProcessor.Name.Form.FormName or ExternalReport.Name.Form.FormName + $expectedPrefix = "$mdType.$objName.Form." + if ($defaultFormVal.StartsWith($expectedPrefix)) { + $refFormName = $defaultFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. DefaultForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. DefaultForm value '$defaultFormVal' has unexpected prefix (expected '$expectedPrefix...')" + } +} + +if ($auxFormVal) { + $expectedPrefix = "$mdType.$objName.Form." + if ($auxFormVal.StartsWith($expectedPrefix)) { + $refFormName = $auxFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. AuxiliaryForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } +} + +if ($mainDCSVal -and $mdType -eq "ExternalReport") { + $expectedPrefix = "ExternalReport.$objName.Template." + if ($mainDCSVal.StartsWith($expectedPrefix)) { + $refTplName = $mainDCSVal.Substring($expectedPrefix.Length) + if ($templateNames -notcontains $refTplName) { + Report-Error "5. MainDataCompositionSchema references '$refTplName', but no such Template in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. MainDataCompositionSchema value '$mainDCSVal' has unexpected prefix" + } +} + +if ($check5Ok) { + $refs = @() + if ($defaultFormVal) { $refs += "DefaultForm" } + if ($auxFormVal) { $refs += "AuxiliaryForm" } + if ($mainDCSVal) { $refs += "MainDCS" } + if ($refs.Count -gt 0) { + Report-OK "5. Cross-references: $($refs -join ', ') valid" + } else { + Report-OK "5. Cross-references: none to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: Attributes — UUID, Name, Type --- + +function Check-Attribute { + param( + [System.Xml.XmlNode]$node, + [string]$context + ) + + $uuid = $node.GetAttribute("uuid") + if (-not $uuid) { + Report-Error "6. $context Attribute missing uuid" + return $false + } elseif ($uuid -notmatch $guidPattern) { + Report-Error "6. $context Attribute has invalid uuid '$uuid'" + return $false + } + + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { + Report-Error "6. $context Attribute (uuid=$uuid) missing Properties" + return $false + } + + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { + Report-Error "6. $context Attribute (uuid=$uuid) missing or empty Name" + return $false + } + + $nameVal = $elName.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "6. $context Attribute '$nameVal' has invalid identifier" + return $false + } + + $typeEl = $elProps.SelectSingleNode("md:Type", $ns) + if (-not $typeEl) { + Report-Error "6. $context Attribute '$nameVal' missing Type block" + return $false + } + $v8Types = $typeEl.SelectNodes("v8:Type", $ns) + $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) + if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { + Report-Error "6. $context Attribute '$nameVal' Type block has no v8:Type or v8:TypeSet" + return $false + } + + return $true +} + +if ($childObjNode) { + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) + $check6Ok = $true + $attrCount = 0 + + foreach ($attr in $attrs) { + if ($script:stopped) { break } + $ok = Check-Attribute -node $attr -context "" + if (-not $ok) { $check6Ok = $false } + $attrCount++ + + # Collect name for uniqueness + $ap = $attr.SelectSingleNode("md:Properties/md:Name", $ns) + if ($ap -and $ap.InnerText) { + $allChildNames["Attr:$($ap.InnerText)"] = $ap.InnerText + } + } + + if ($attrCount -gt 0) { + if ($check6Ok) { + Report-OK "6. Attributes: $attrCount checked (UUID, Name, Type)" + } + } else { + Report-OK "6. Attributes: none" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: TabularSections --- + +if ($childObjNode) { + $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tsSections.Count -gt 0) { + $check7Ok = $true + $tsCount = 0 + $tsAttrTotal = 0 + + foreach ($ts in $tsSections) { + if ($script:stopped) { break } + $tsCount++ + + $tsUuid = $ts.GetAttribute("uuid") + if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { + Report-Error "7. TabularSection #${tsCount}: invalid or missing uuid" + $check7Ok = $false + } + + $tsProps = $ts.SelectSingleNode("md:Properties", $ns) + $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } + $tsName = if ($tsNameNode -and $tsNameNode.InnerText) { $tsNameNode.InnerText } else { "(unnamed)" } + + if (-not $tsNameNode -or -not $tsNameNode.InnerText) { + Report-Error "7. TabularSection #${tsCount}: missing or empty Name" + $check7Ok = $false + } elseif ($tsName -notmatch $identPattern) { + Report-Error "7. TabularSection '$tsName': invalid identifier" + $check7Ok = $false + } + + $allChildNames["TS:$tsName"] = $tsName + + # InternalInfo — expect 2 GeneratedType + $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) + if ($tsIntInfo) { + $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) + if ($tsGens.Count -lt 2) { + Report-Warn "7. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" + } + } + + # Inner attributes + $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) + if ($tsChildObj) { + $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) + $tsAttrNames = @{} + foreach ($ta in $tsAttrs) { + $taOk = Check-Attribute -node $ta -context "TabularSection '$tsName'." + if (-not $taOk) { $check7Ok = $false } + $tsAttrTotal++ + + $taProps = $ta.SelectSingleNode("md:Properties/md:Name", $ns) + if ($taProps -and $taProps.InnerText) { + if ($tsAttrNames.ContainsKey($taProps.InnerText)) { + Report-Error "7. Duplicate attribute '$($taProps.InnerText)' in TabularSection '$tsName'" + $check7Ok = $false + } else { + $tsAttrNames[$taProps.InnerText] = $true + } + } + } + } + } + + if ($check7Ok) { + Report-OK "7. TabularSections: $tsCount sections, $tsAttrTotal inner attributes" + } + } else { + Report-OK "7. TabularSections: none" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Name uniqueness --- + +$check8Ok = $true + +# Collect all names: attributes + tabular sections + forms + templates + commands +$allNames = @{} + +if ($childObjNode) { + $nameKinds = @( + @{ XPath = "md:Attribute"; Kind = "Attribute" }, + @{ XPath = "md:TabularSection"; Kind = "TabularSection" }, + @{ XPath = "md:Command"; Kind = "Command" } + ) + + foreach ($nk in $nameKinds) { + $nodes = $childObjNode.SelectNodes($nk.XPath, $ns) + foreach ($node in $nodes) { + $np = $node.SelectSingleNode("md:Properties/md:Name", $ns) + if ($np -and $np.InnerText) { + $nameVal = $np.InnerText + $key = "$($nk.Kind):$nameVal" + if ($allNames.ContainsKey($nameVal)) { + Report-Error "8. Duplicate name '$nameVal' ($($nk.Kind) conflicts with $($allNames[$nameVal]))" + $check8Ok = $false + } else { + $allNames[$nameVal] = $nk.Kind + } + } + } + } + + # Forms and Templates are simple text nodes + foreach ($fn in $formNames) { + if ($allNames.ContainsKey($fn)) { + Report-Error "8. Duplicate name '$fn' (Form conflicts with $($allNames[$fn]))" + $check8Ok = $false + } else { + $allNames[$fn] = "Form" + } + } + foreach ($tn in $templateNames) { + if ($allNames.ContainsKey($tn)) { + Report-Error "8. Duplicate name '$tn' (Template conflicts with $($allNames[$tn]))" + $check8Ok = $false + } else { + $allNames[$tn] = "Template" + } + } +} + +if ($check8Ok) { + Report-OK "8. Name uniqueness: $($allNames.Count) names, all unique" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: File existence (forms and templates on disk) --- + +$check9Ok = $true +$filesChecked = 0 + +# Object directory: same level as root XML, named after the object +$objDir = Join-Path $srcDir $objName + +foreach ($fn in $formNames) { + # FormName.xml — form descriptor + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { + Report-Error "9. Missing form descriptor: Forms/$fn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # FormName/Ext/Form.xml — form layout + $formXml = Join-Path (Join-Path (Join-Path (Join-Path $objDir "Forms") $fn) "Ext") "Form.xml" + if (-not (Test-Path $formXml)) { + Report-Error "9. Missing form layout: Forms/$fn/Ext/Form.xml" + $check9Ok = $false + } else { + $filesChecked++ + } +} + +foreach ($tn in $templateNames) { + # TemplateName.xml — template descriptor + $tplMetaXml = Join-Path (Join-Path $objDir "Templates") "$tn.xml" + if (-not (Test-Path $tplMetaXml)) { + Report-Error "9. Missing template descriptor: Templates/$tn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # TemplateName/Ext/Template.* — template content (extension varies) + $tplExtDir = Join-Path (Join-Path (Join-Path $objDir "Templates") $tn) "Ext" + if (Test-Path $tplExtDir) { + $tplFiles = @(Get-ChildItem $tplExtDir -Filter "Template.*" -File) + if ($tplFiles.Count -eq 0) { + Report-Error "9. Missing template content: Templates/$tn/Ext/Template.*" + $check9Ok = $false + } else { + $filesChecked++ + } + } else { + Report-Error "9. Missing template Ext directory: Templates/$tn/Ext/" + $check9Ok = $false + } +} + +# ObjectModule.bsl +$objModule = Join-Path (Join-Path $objDir "Ext") "ObjectModule.bsl" +if (Test-Path $objModule) { + $filesChecked++ +} + +if ($check9Ok) { + if ($filesChecked -gt 0) { + Report-OK "9. File existence: $filesChecked files verified" + } else { + Report-OK "9. File existence: no forms/templates to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 10: Form descriptors structure --- + +$check10Ok = $true +$formsChecked = 0 + +foreach ($fn in $formNames) { + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { continue } + + try { + $fDoc = New-Object System.Xml.XmlDocument + $fDoc.PreserveWhitespace = $false + $fDoc.Load($formMetaXml) + $fRoot = $fDoc.DocumentElement + + if ($fRoot.LocalName -ne "MetaDataObject") { + Report-Error "10. Form '$fn': root element is '$($fRoot.LocalName)', expected 'MetaDataObject'" + $check10Ok = $false + continue + } + + $fTypeNode = $fRoot.SelectSingleNode("md:Form", $ns) + if (-not $fTypeNode) { + Report-Error "10. Form '$fn': missing element" + $check10Ok = $false + continue + } + + $fUuid = $fTypeNode.GetAttribute("uuid") + if (-not $fUuid -or $fUuid -notmatch $guidPattern) { + Report-Error "10. Form '$fn': invalid or missing uuid" + $check10Ok = $false + } + + $fProps = $fTypeNode.SelectSingleNode("md:Properties", $ns) + if ($fProps) { + $fName = $fProps.SelectSingleNode("md:Name", $ns) + if ($fName -and $fName.InnerText -ne $fn) { + Report-Error "10. Form '$fn': Name in descriptor is '$($fName.InnerText)', expected '$fn'" + $check10Ok = $false + } + + # FormType should be Managed + $fType = $fProps.SelectSingleNode("md:FormType", $ns) + if ($fType -and $fType.InnerText -ne "Managed") { + Report-Warn "10. Form '$fn': FormType is '$($fType.InnerText)' (expected 'Managed')" + } + } + + $formsChecked++ + } catch { + Report-Error "10. Form '$fn': XML parse error: $($_.Exception.Message)" + $check10Ok = $false + } +} + +if ($check10Ok) { + if ($formsChecked -gt 0) { + Report-OK "10. Form descriptors: $formsChecked checked" + } else { + Report-OK "10. Form descriptors: none to check" + } +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/erf-build/SKILL.md b/.claude/skills/erf-build/SKILL.md index 860505b1..4f717694 100644 --- a/.claude/skills/erf-build/SKILL.md +++ b/.claude/skills/erf-build/SKILL.md @@ -1,71 +1,71 @@ ---- -name: erf-build -description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников -argument-hint: -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# /erf-build — Сборка отчёта - -## Usage - -``` -/erf-build [SrcDir] [OutDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|------------|:------------:|--------------|--------------------------------------| -| ReportName | да | — | Имя отчёта (имя корневого XML) | -| SrcDir | нет | `src` | Каталог исходников | -| OutDir | нет | `build` | Каталог для результата | - -## Параметры подключения (опционально) - -Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. - -1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: -2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -5. Если ветка не совпала — используй `default` -6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. - -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -Используй общий скрипт из epf-build: - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | -| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | - -> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных - -## Примеры - -```powershell -# Сборка отчёта (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" -``` +--- +name: erf-build +description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /erf-build — Сборка отчёта + +## Usage + +``` +/erf-build [SrcDir] [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|--------------------------------------| +| ReportName | да | — | Имя отчёта (имя корневого XML) | +| SrcDir | нет | `src` | Каталог исходников | +| OutDir | нет | `build` | Каталог для результата | + +## Параметры подключения (опционально) + +Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. + +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +Используй общий скрипт из epf-build: + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | +| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | + +> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных + +## Примеры + +```powershell +# Сборка отчёта (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" +``` diff --git a/.claude/skills/erf-dump/SKILL.md b/.claude/skills/erf-dump/SKILL.md index d79a66b7..f8f25ece 100644 --- a/.claude/skills/erf-dump/SKILL.md +++ b/.claude/skills/erf-dump/SKILL.md @@ -1,71 +1,71 @@ ---- -name: erf-dump -description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла -argument-hint: -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# /erf-dump — Разборка отчёта - -## Usage - -``` -/erf-dump [OutDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|----------|:------------:|--------------|-------------------------------------| -| ErfFile | да | — | Путь к ERF-файлу | -| OutDir | нет | `src` | Каталог для выгрузки исходников | - -## Параметры подключения (обязательно) - -Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. - -1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: -2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую -3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` -4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` -5. Если ветка не совпала — используй `default` -6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. - -Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). -Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. - -## Команда - -Используй общий скрипт из epf-dump: - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> -``` - -### Параметры скрипта - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | -| `-InfoBasePath <путь>` | * | Файловая база | -| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | -| `-InfoBaseRef <имя>` | * | Имя базы на сервере | -| `-UserName <имя>` | нет | Имя пользователя | -| `-Password <пароль>` | нет | Пароль | -| `-InputFile <путь>` | да | Путь к ERF-файлу | -| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | -| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | - -> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) - -## Примеры - -```powershell -# Разборка отчёта (файловая база) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" - -# Серверная база -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" -``` +--- +name: erf-dump +description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /erf-dump — Разборка отчёта + +## Usage + +``` +/erf-dump [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|----------|:------------:|--------------|-------------------------------------| +| ErfFile | да | — | Путь к ERF-файлу | +| OutDir | нет | `src` | Каталог для выгрузки исходников | + +## Параметры подключения (обязательно) + +Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. + +Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +Используй общий скрипт из epf-dump: + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к ERF-файлу | +| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) + +## Примеры + +```powershell +# Разборка отчёта (файловая база) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" + +# Серверная база +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" +``` diff --git a/.claude/skills/erf-init/SKILL.md b/.claude/skills/erf-init/SKILL.md index c57afbb3..b527b571 100644 --- a/.claude/skills/erf-init/SKILL.md +++ b/.claude/skills/erf-init/SKILL.md @@ -1,42 +1,42 @@ ---- -name: erf-init -description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля -argument-hint: [Synonym] [--with-skd] -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /erf-init — Создание нового отчёта - -Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта. - -## Usage - -``` -/erf-init [Synonym] [SrcDir] [--with-skd] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|-----------|:------------:|--------------|---------------------------------------| -| Name | да | — | Имя отчёта (латиница/кириллица) | -| Synonym | нет | = Name | Синоним (отображаемое имя) | -| SrcDir | нет | `src` | Каталог исходников относительно CWD | -| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] [-WithSKD] -``` - -## Дальнейшие шаги - -- Добавить форму: `/form-add` -- Добавить макет: `/template-add` -- Добавить справку: `/help-add` -- Собрать ERF: `/erf-build` +--- +name: erf-init +description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля +argument-hint: [Synonym] [--with-skd] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /erf-init — Создание нового отчёта + +Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта. + +## Usage + +``` +/erf-init [Synonym] [SrcDir] [--with-skd] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|---------------------------------------| +| Name | да | — | Имя отчёта (латиница/кириллица) | +| Synonym | нет | = Name | Синоним (отображаемое имя) | +| SrcDir | нет | `src` | Каталог исходников относительно CWD | +| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] [-WithSKD] +``` + +## Дальнейшие шаги + +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Добавить справку: `/help-add` +- Собрать ERF: `/erf-build` diff --git a/.claude/skills/erf-init/scripts/init.ps1 b/.claude/skills/erf-init/scripts/init.ps1 index 9cd22885..572e940f 100644 --- a/.claude/skills/erf-init/scripts/init.ps1 +++ b/.claude/skills/erf-init/scripts/init.ps1 @@ -1,180 +1,180 @@ -# erf-init v1.1 — Init 1C external report scaffold -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$Name, - - [string]$Synonym = $Name, - - [string]$SrcDir = "src", - - [switch]$WithSKD -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 - -$uuid1 = [guid]::NewGuid().ToString() -$uuid2 = [guid]::NewGuid().ToString() -$uuid3 = [guid]::NewGuid().ToString() -$uuid4 = [guid]::NewGuid().ToString() - -# --- Формируем Properties --- - -$mainDCSValue = "" -$childObjectsContent = "" - -if ($WithSKD) { - $mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных" - $childObjectsContent = @" - - - -"@ -} - -$mainDCSElement = if ($mainDCSValue) { - "$mainDCSValue" -} else { - "" -} - -$childObjectsXml = if ($childObjectsContent) { - "$childObjectsContent" -} else { - "" -} - -$xml = @" - - - - - - e41aff26-25cf-4bb6-b6c1-3f478a75f374 - $uuid2 - - - $uuid3 - $uuid4 - - - - $Name - - - ru - $Synonym - - - - - - $mainDCSElement - - - - - - - $childObjectsXml - - -"@ - -$rootFile = Join-Path $SrcDir "$Name.xml" -$reportDir = Join-Path $SrcDir $Name - -if (Test-Path $rootFile) { - Write-Error "Файл уже существует: $rootFile" - exit 1 -} - -if (-not (Test-Path $SrcDir)) { - New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null -} -$extDir = Join-Path $reportDir "Ext" -New-Item -ItemType Directory -Path $extDir -Force | Out-Null - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) - -# --- Модуль объекта --- - -$moduleBsl = @" -#Область ОписаниеПеременных - -#КонецОбласти - -#Область ПрограммныйИнтерфейс - -#КонецОбласти - -#Область СлужебныеПроцедурыИФункции - -#КонецОбласти -"@ - -$modulePath = Join-Path $extDir "ObjectModule.bsl" -[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) - -Write-Host "[OK] Создан отчёт: $rootFile" -Write-Host " Каталог: $reportDir" -Write-Host " Модуль: $modulePath" - -# --- СКД-макет (если --WithSKD) --- - -if ($WithSKD) { - $templatesDir = Join-Path $reportDir "Templates" - $skdName = "ОсновнаяСхемаКомпоновкиДанных" - $skdMetaPath = Join-Path $templatesDir "$skdName.xml" - $skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext" - New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null - - $skdUuid = [guid]::NewGuid().ToString() - - $skdMetaXml = @" - - - - -"@ - - [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) - - $skdContent = @" - - - - ИсточникДанных1 - Local - - -"@ - - $skdFilePath = Join-Path $skdExtDir "Template.xml" - [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) - - Write-Host " СКД: $skdMetaPath" - Write-Host " Тело: $skdFilePath" -} +# erf-init v1.1 — Init 1C external report scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + + [string]$Synonym = $Name, + + [string]$SrcDir = "src", + + [switch]$WithSKD +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +$uuid1 = [guid]::NewGuid().ToString() +$uuid2 = [guid]::NewGuid().ToString() +$uuid3 = [guid]::NewGuid().ToString() +$uuid4 = [guid]::NewGuid().ToString() + +# --- Формируем Properties --- + +$mainDCSValue = "" +$childObjectsContent = "" + +if ($WithSKD) { + $mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных" + $childObjectsContent = @" + + + +"@ +} + +$mainDCSElement = if ($mainDCSValue) { + "$mainDCSValue" +} else { + "" +} + +$childObjectsXml = if ($childObjectsContent) { + "$childObjectsContent" +} else { + "" +} + +$xml = @" + + + + + + e41aff26-25cf-4bb6-b6c1-3f478a75f374 + $uuid2 + + + $uuid3 + $uuid4 + + + + $Name + + + ru + $Synonym + + + + + + $mainDCSElement + + + + + + + $childObjectsXml + + +"@ + +$rootFile = Join-Path $SrcDir "$Name.xml" +$reportDir = Join-Path $SrcDir $Name + +if (Test-Path $rootFile) { + Write-Error "Файл уже существует: $rootFile" + exit 1 +} + +if (-not (Test-Path $SrcDir)) { + New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null +} +$extDir = Join-Path $reportDir "Ext" +New-Item -ItemType Directory -Path $extDir -Force | Out-Null + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) + +# --- Модуль объекта --- + +$moduleBsl = @" +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +"@ + +$modulePath = Join-Path $extDir "ObjectModule.bsl" +[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) + +Write-Host "[OK] Создан отчёт: $rootFile" +Write-Host " Каталог: $reportDir" +Write-Host " Модуль: $modulePath" + +# --- СКД-макет (если --WithSKD) --- + +if ($WithSKD) { + $templatesDir = Join-Path $reportDir "Templates" + $skdName = "ОсновнаяСхемаКомпоновкиДанных" + $skdMetaPath = Join-Path $templatesDir "$skdName.xml" + $skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext" + New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null + + $skdUuid = [guid]::NewGuid().ToString() + + $skdMetaXml = @" + + + + +"@ + + [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) + + $skdContent = @" + + + + ИсточникДанных1 + Local + + +"@ + + $skdFilePath = Join-Path $skdExtDir "Template.xml" + [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) + + Write-Host " СКД: $skdMetaPath" + Write-Host " Тело: $skdFilePath" +} diff --git a/.claude/skills/erf-validate/SKILL.md b/.claude/skills/erf-validate/SKILL.md index ea98d71f..355c4298 100644 --- a/.claude/skills/erf-validate/SKILL.md +++ b/.claude/skills/erf-validate/SKILL.md @@ -1,32 +1,32 @@ ---- -name: erf-validate -description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /erf-validate — валидация внешнего отчёта (ERF) - -Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. - -Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|------------|:-----:|---------|-------------------------------------------------| -| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" -``` - +--- +name: erf-validate +description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /erf-validate — валидация внешнего отчёта (ERF) + +Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. + +Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" +``` + diff --git a/.claude/skills/form-add/SKILL.md b/.claude/skills/form-add/SKILL.md index 78521f92..18d10bfc 100644 --- a/.claude/skills/form-add/SKILL.md +++ b/.claude/skills/form-add/SKILL.md @@ -1,71 +1,71 @@ ---- -name: form-add -description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму -argument-hint: [Purpose] [--set-default] -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /form-add — Добавление формы к объекту конфигурации - -Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.). - -## Usage - -``` -/form-add [Purpose] [Synonym] [--set-default] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|-------------|:------------:|--------------|----------------------------------------------| -| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) | -| FormName | да | — | Имя формы (ФормаДокумента) | -| Purpose | нет | Object | Назначение: Object, List, Choice, Record | -| Synonym | нет | = FormName | Синоним формы | -| --set-default | нет | авто | Установить как форму по умолчанию | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "" -FormName "" [-Purpose ""] [-Synonym ""] [-SetDefault] -``` - -## Purpose — назначение формы - -| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство | -|---------|-------------------------|-------------------|---------------------| -| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) | -| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm | -| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm | -| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm | - -## Примеры - -``` -# Форма документа -/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object - -# Форма списка каталога -/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List - -# Форма записи регистра сведений -/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record - -# Форма выбора с синонимом -/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры" - -# Установить как форму по умолчанию -/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default -``` - -## Workflow - -1. `/form-add` — создать каркас формы -2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами -3. `/form-validate` — проверить корректность -4. `/form-info` — проанализировать результат +--- +name: form-add +description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму +argument-hint: [Purpose] [--set-default] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /form-add — Добавление формы к объекту конфигурации + +Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.). + +## Usage + +``` +/form-add [Purpose] [Synonym] [--set-default] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-------------|:------------:|--------------|----------------------------------------------| +| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) | +| FormName | да | — | Имя формы (ФормаДокумента) | +| Purpose | нет | Object | Назначение: Object, List, Choice, Record | +| Synonym | нет | = FormName | Синоним формы | +| --set-default | нет | авто | Установить как форму по умолчанию | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "" -FormName "" [-Purpose ""] [-Synonym ""] [-SetDefault] +``` + +## Purpose — назначение формы + +| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство | +|---------|-------------------------|-------------------|---------------------| +| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) | +| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm | +| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm | +| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm | + +## Примеры + +``` +# Форма документа +/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object + +# Форма списка каталога +/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List + +# Форма записи регистра сведений +/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record + +# Форма выбора с синонимом +/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры" + +# Установить как форму по умолчанию +/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default +``` + +## Workflow + +1. `/form-add` — создать каркас формы +2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами +3. `/form-validate` — проверить корректность +4. `/form-info` — проанализировать результат diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 5a5fbdcf..2d09af1c 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -1,567 +1,567 @@ ---- -name: form-compile -description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму -argument-hint: | -FromObject -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /form-compile — Генерация Form.xml - -Два режима: -1. **JSON DSL** — из JSON-определения формы -2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP - -> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно. - -## Параметры - -| Параметр | Обязательный | Описание | -|------------|:------------:|---------------------------------| -| JsonPath | режим 1 | Путь к JSON-определению формы | -| OutputPath | да | Путь к выходному Form.xml | -| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта | - -## Команда - -```powershell -# Режим JSON DSL -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "" -OutputPath "" - -# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>" -``` - -## JSON DSL — справка - -### Структура верхнего уровня - -```json -{ - "title": "Заголовок формы", - "properties": { "autoTitle": false, ... }, - "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, - "excludedCommands": ["Reread"], - "elements": [ ... ], - "attributes": [ ... ], - "commands": [ ... ], - "parameters": [ ... ] -} -``` - -- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне -- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др. -- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры) -- `excludedCommands` — исключённые стандартные команды - -### Элементы (ключ определяет тип) - -| DSL ключ | XML элемент | Значение ключа | -|--------------|-------------------|---------------------------------------------------| -| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) | -| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы | -| `"input"` | InputField | имя элемента | -| `"check"` | CheckBoxField | имя | -| `"radio"` | RadioButtonField | имя | -| `"label"` | LabelDecoration | имя (текст задаётся через `title`) | -| `"labelField"` | LabelField | имя | -| `"table"` | Table | имя | -| `"pages"` | Pages | имя | -| `"page"` | Page | имя | -| `"button"` | Button | имя | -| `"picture"` | PictureDecoration | имя | -| `"picField"` | PictureField | имя | -| `"calendar"` | CalendarField | имя | -| `"cmdBar"` | CommandBar | имя | -| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `` | -| `"popup"` | Popup | имя | - -### Общие свойства (все типы элементов) - -| Ключ | Описание | -|------|----------| -| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) | -| `title` | Заголовок элемента | -| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) | -| `visible: false` | Скрыть (синоним: `hidden: true`) | -| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) | -| `readOnly: true` | Только чтение | -| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически | - -### Допустимые имена событий (`events`) - -Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано. - -**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening` - -**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange` - -**check / radio**: `OnChange` - -**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd` - -**label / picture**: `Click`, `URLProcessing` - -**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing` - -**button**: `Click` - -**pages**: `OnCurrentPageChange` - -### Поле ввода (input) - -| Ключ | Описание | Пример | -|------|----------|--------| -| `path` | DataPath — привязка к данным | `"Объект.Организация"` | -| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` | -| `multiLine: true` | Многострочное поле | текстовое поле, комментарий | -| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля | -| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле | -| `clearButton: true` | Кнопка очистки ("X") | | -| `spinButton: true` | Кнопка прокрутки | числовые поля | -| `dropListButton: true` | Кнопка выпадающего списка | | -| `markIncomplete: true` | Пометка незаполненного | обязательные поля | -| `skipOnInput: true` | Пропускать при обходе Tab | | -| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` | -| `width` / `height` | Размер | числа | -| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | | -| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` | -| `horizontalStretch: true` | Растягивать по ширине | | - -### Чекбокс (check) - -| Ключ | Описание | -|------|----------| -| `path` | DataPath | -| `titleLocation` | Размещение заголовка | - -### Поле переключателя (radio) - -Радиокнопки или тумблер для выбора одного значения из списка. - -| Ключ | Описание | Пример | -|------|----------|--------| -| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` | -| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` | -| `columnsCount` | Число колонок раскладки | `1`, `2`, ... | -| `titleLocation` | Размещение заголовка | по умолчанию `"none"` | -| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже | - -`choiceList[*]`: - -| Ключ | Описание | -|------|----------| -| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` | -| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности | - -```json -{ - "radio": "СпособКурса", - "path": "Объект.СпособУстановкиКурса", - "radioButtonType": "Auto", - "choiceList": [ - { "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } }, - { "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" } - ] -} -``` - -### Надпись-декорация (label) - -| Ключ | Описание | -|------|----------| -| `title` | Текст надписи (обязательно) | -| `hyperlink: true` | Сделать ссылкой | -| `width` / `height` | Размер | - -### Группа (group) - -Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`. - -| Ключ | Описание | -|------|----------| -| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная | -| `showTitle: true` | Показывать заголовок группы | -| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) | -| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой | -| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | -| `children: [...]` | Вложенные элементы | - -### Таблица (table) - -**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки"). - -| Ключ | Описание | -|------|----------| -| `path` | DataPath (привязка к реквизиту-таблице) | -| `columns: [...]` | Колонки — массив элементов (обычно `input`) | -| `changeRowSet: true` | Разрешить добавление/удаление строк | -| `changeRowOrder: true` | Разрешить перемещение строк | -| `height` | Высота в строках таблицы | -| `header: false` | Скрыть шапку | -| `footer: true` | Показать подвал | -| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` | -| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` | -| `choiceMode: true` | Режим выбора (для форм выбора) | -| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) | -| `enableDrag: true` | Разрешить перетаскивание | -| `enableStartDrag: true` | Разрешить начало перетаскивания | -| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) | -| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar | - -Колонки можно группировать через `columnGroup` (см. ниже). - -### Группа колонок (columnGroup) - -Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`. - -| Ключ | Описание | -|------|----------| -| `name` | Имя элемента (рекомендуется задавать явно) | -| `title` | Заголовок группы | -| `showTitle: false` | Скрыть заголовок | -| `showInHeader: true/false` | Показывать ли группу в шапке таблицы | -| `width` | Ширина | -| `horizontalStretch: false` | Растягивание | -| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) | - -```json -{ "table": "Список", "path": "Список", "columns": [ - { "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [ - { "input": "СрокИсполнения", "path": "Список.СрокИсполнения" }, - { "labelField": "Просрочено", "path": "Список.Просрочено" } - ]}, - { "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [ - { "input": "Исполнитель", "path": "Список.Исполнитель" } - ]}, - { "input": "Комментарий", "path": "Список.Комментарий" } -]} -``` - -### Картинка-поле (picField) - -PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`: - -| Ключ | Описание | -|------|----------| -| `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` | -| `loadTransparent: true` | Скрыть кадр «нет значения» | - -### Страницы (pages + page) - -| Ключ (pages) | Описание | -|------|----------| -| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. | -| `children: [...]` | Массив `page` | - -| Ключ (page) | Описание | -|------|----------| -| `title` | Заголовок вкладки | -| `group` | Ориентация внутри страницы | -| `children: [...]` | Содержимое страницы | - -### Кнопка (button) - -| Ключ | Описание | -|------|----------| -| `command` | Имя команды формы → `Form.Command.Имя` | -| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` | -| `defaultButton: true` | Кнопка по умолчанию | -| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту | -| `picture` | Картинка кнопки | -| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` | -| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` | - -### Командная панель (cmdBar) - -Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы. - -| Ключ | Описание | -|------|----------| -| `autofill: true` | Автозаполнение стандартными командами | -| `children: [...]` | Кнопки панели | - -### Главная автокомандная панель формы (autoCmdBar) - -Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением. - -| Ключ | Описание | -|------|----------| -| `autofill: true/false` | Автозаполнение стандартными командами | -| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` | -| `children: [...]` | Кнопки/popup | - -```json -{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [ - { "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные", - "locationInCommandBar": "InAdditionalSubmenu" } -]} -``` - -Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе. - -### Выпадающее меню (popup) - -| Ключ | Описание | -|------|----------| -| `title` | Заголовок подменю | -| `children: [...]` | Кнопки подменю | - -Используется внутри `cmdBar` для группировки кнопок в подменю: -```json -{ "cmdBar": "Панель", "children": [ - { "popup": "Добавить", "title": "Добавить", "children": [ - { "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" }, - { "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" } - ]} -]} -``` - -### Реквизиты (attributes) - -```json -{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } -{ "name": "Список", "type": "DynamicList", "main": true, "settings": { - "mainTable": "Catalog.Номенклатура", "dynamicDataRead": true -}} -{ "name": "Итого", "type": "decimal(15,2)" } -{ "name": "Таблица", "type": "ValueTable", "columns": [ - { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, - { "name": "Количество", "type": "decimal(10,3)" } -]} -``` - -- `savedData: true` — сохраняемые данные -- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`) - -### Команды (commands) - -```json -{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } -``` - -- `title` — заголовок (если отличается от name) -- `picture` — картинка команды - -### Система типов - -**Примитивные:** - -| DSL | XML | -|------------------------|----------------------------------------| -| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers | -| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers | -| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative | -| `"boolean"` | `xs:boolean` | -| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions | - -**Ссылочные и объектные (`cfg:Prefix.Name`):** - -| DSL | Описание | -|-----|----------| -| `"CatalogRef.XXX"` / `"CatalogObject.XXX"` | Справочник | -| `"DocumentRef.XXX"` / `"DocumentObject.XXX"` | Документ | -| `"EnumRef.XXX"` | Перечисление | -| `"DataProcessorObject.XXX"` / `"ReportObject.XXX"` | Обработка / Отчёт | -| `"InformationRegisterRecordSet.XXX"` | Набор записей регистра сведений | -| `"AccumulationRegisterRecordSet.XXX"` | Набор записей регистра накопления | -| `"DynamicList"` | Динамический список | - -Также допустимы: `ChartOfAccountsRef/Object`, `ChartOfCharacteristicTypesRef/Object`, `ChartOfCalculationTypesRef/Object`, `ExchangePlanRef/Object`, `BusinessProcessRef/Object`, `TaskRef/Object`, `AccountingRegisterRecordSet`, `InformationRegisterRecordManager`, `ConstantsSet`. - -**Платформенные:** - -| DSL | XML | -|-----|-----| -| `"ValueTable"` | `v8:ValueTable` | -| `"ValueTree"` | `v8:ValueTree` | -| `"ValueList"` | `v8:ValueListType` | -| `"TypeDescription"` | `v8:TypeDescription` | -| `"UUID"` | `v8:UUID` | -| `"FormattedString"` | `v8ui:FormattedString` | -| `"Picture"` / `"Color"` / `"Font"` | `v8ui:*` | -| `"DataCompositionSettings"` | `dcsset:DataCompositionSettings` | -| `"Type1 \| Type2"` | составной тип (несколько ``) | - -**Недопустимые типы (XDTO-ошибка при загрузке):** - -> `FormDataStructure`, `FormDataCollection`, `FormDataTree` — runtime-типы 1С, не существуют в XML-схеме. Вместо них используйте `CatalogObject.XXX`, `DocumentObject.XXX`, `DataProcessorObject.XXX`, `ValueTable`, `ValueTree`. - -## Связки: элемент + реквизит - -Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`. - -**Таблица** — элемент `table` + реквизит `ValueTable`: -```json -{ - "elements": [ - { "table": "Товары", "path": "Объект.Товары", "columns": [ - { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } - ]} - ], - "attributes": [ - { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true, - "columns": [ - { "name": "Товары", "type": "ValueTable", "columns": [ - { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" } - ]} - ] - } - ] -} -``` - -Или, если таблица привязана к реквизиту формы (не к Объект): -```json -{ - "elements": [ - { "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [ - { "input": "Наименование", "path": "ТаблицаДанных.Наименование" } - ]} - ], - "attributes": [ - { "name": "ТаблицаДанных", "type": "ValueTable", "columns": [ - { "name": "Наименование", "type": "string(150)" } - ]} - ] -} -``` - -## Паттерны - -### Диалог загрузки файла - -```json -{ - "title": "Загрузка из файла", - "properties": { "autoTitle": false }, - "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, - "elements": [ - { "group": "horizontal", "name": "ГруппаФайл", "children": [ - { "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } }, - { "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" } - ]}, - { "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" }, - { "autoCmdBar": "ФормаКоманднаяПанель", "children": [ - { "button": "Загрузить", "command": "Загрузить", "defaultButton": true }, - { "button": "Закрыть", "stdCommand": "Close" } - ]} - ], - "attributes": [ - { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true }, - { "name": "ИмяФайла", "type": "string" }, - { "name": "ПерваяСтрокаЗаголовок", "type": "boolean" }, - { "name": "Результат", "type": "string" } - ], - "commands": [ - { "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } - ] -} -``` - -### Мастер (wizard) с шагами - -```json -{ - "title": "Мастер настройки", - "properties": { "autoTitle": false }, - "elements": [ - { "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [ - { "page": "Шаг1", "title": "Параметры", "children": [ - { "input": "Параметр1", "path": "Параметр1" } - ]}, - { "page": "Шаг2", "title": "Результат", "children": [ - { "input": "Итог", "path": "Итог", "readOnly": true } - ]} - ]}, - { "group": "horizontal", "name": "Навигация", "children": [ - { "button": "Назад", "command": "Назад", "title": "< Назад" }, - { "button": "Далее", "command": "Далее", "title": "Далее >" } - ]} - ], - "attributes": [ - { "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true }, - { "name": "Параметр1", "type": "string" }, - { "name": "Итог", "type": "string" } - ], - "commands": [ - { "name": "Назад", "action": "НазадОбработка" }, - { "name": "Далее", "action": "ДалееОбработка" } - ] -} -``` - -### Список с фильтром и таблицей - -```json -{ - "title": "Просмотр данных", - "elements": [ - { "group": "horizontal", "name": "Фильтр", "children": [ - { "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } }, - { "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } } - ]}, - { "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [ - { "input": "Дата", "path": "Данные.Дата" }, - { "input": "Сумма", "path": "Данные.Сумма" }, - { "input": "Комментарий", "path": "Данные.Комментарий" } - ]} - ], - "attributes": [ - { "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true }, - { "name": "Период", "type": "date" }, - { "name": "Организация", "type": "string" }, - { "name": "Данные", "type": "ValueTable", "columns": [ - { "name": "Дата", "type": "date" }, - { "name": "Сумма", "type": "decimal(15,2)" }, - { "name": "Комментарий", "type": "string(200)" } - ]} - ] -} -``` - -## Продвинутые конструкции (по необходимости) - -Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`: - -- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса -- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки) -- `choice-params.md` — параметры и связи выбора у полей ввода -- `command-interface.md` — командный интерфейс формы -- `roles-access.md` — пользовательская видимость и доступ по ролям -- `companion-panels.md` — контент расширенной подсказки и контекстного меню -- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок) -- `charts.md` — диаграммы и планировщик -- `report-form.md` — свойства формы отчёта -- `type-system-advanced.md` — наборы и составные типы -- `table-advanced.md` — расширенные свойства таблиц -- `layout-advanced.md` — тонкая компоновка и геометрия - -## Автогенерация - -- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически -- **Namespace**: все 17 namespace-деклараций -- **ID**: последовательная нумерация, AutoCommandBar = id="-1" -- **Unknown keys**: выводится предупреждение о нераспознанных ключах - -## Workflow - -1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`). -2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml. -3. **Проверка**: `/form-validate`, `/form-info`. - -## Верификация - -``` -/form-validate — проверка корректности XML -/form-info — визуальная сводка структуры -``` - -## Особенности для внешних обработок (EPF) - -- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) -- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных -- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`) +--- +name: form-compile +description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму +argument-hint: | -FromObject +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /form-compile — Генерация Form.xml + +Два режима: +1. **JSON DSL** — из JSON-определения формы +2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP + +> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно. + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|---------------------------------| +| JsonPath | режим 1 | Путь к JSON-определению формы | +| OutputPath | да | Путь к выходному Form.xml | +| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта | + +## Команда + +```powershell +# Режим JSON DSL +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "" -OutputPath "" + +# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>" +``` + +## JSON DSL — справка + +### Структура верхнего уровня + +```json +{ + "title": "Заголовок формы", + "properties": { "autoTitle": false, ... }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "excludedCommands": ["Reread"], + "elements": [ ... ], + "attributes": [ ... ], + "commands": [ ... ], + "parameters": [ ... ] +} +``` + +- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне +- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др. +- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры) +- `excludedCommands` — исключённые стандартные команды + +### Элементы (ключ определяет тип) + +| DSL ключ | XML элемент | Значение ключа | +|--------------|-------------------|---------------------------------------------------| +| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) | +| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы | +| `"input"` | InputField | имя элемента | +| `"check"` | CheckBoxField | имя | +| `"radio"` | RadioButtonField | имя | +| `"label"` | LabelDecoration | имя (текст задаётся через `title`) | +| `"labelField"` | LabelField | имя | +| `"table"` | Table | имя | +| `"pages"` | Pages | имя | +| `"page"` | Page | имя | +| `"button"` | Button | имя | +| `"picture"` | PictureDecoration | имя | +| `"picField"` | PictureField | имя | +| `"calendar"` | CalendarField | имя | +| `"cmdBar"` | CommandBar | имя | +| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `` | +| `"popup"` | Popup | имя | + +### Общие свойства (все типы элементов) + +| Ключ | Описание | +|------|----------| +| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) | +| `title` | Заголовок элемента | +| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) | +| `visible: false` | Скрыть (синоним: `hidden: true`) | +| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) | +| `readOnly: true` | Только чтение | +| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически | + +### Допустимые имена событий (`events`) + +Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано. + +**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening` + +**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange` + +**check / radio**: `OnChange` + +**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd` + +**label / picture**: `Click`, `URLProcessing` + +**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing` + +**button**: `Click` + +**pages**: `OnCurrentPageChange` + +### Поле ввода (input) + +| Ключ | Описание | Пример | +|------|----------|--------| +| `path` | DataPath — привязка к данным | `"Объект.Организация"` | +| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` | +| `multiLine: true` | Многострочное поле | текстовое поле, комментарий | +| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля | +| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле | +| `clearButton: true` | Кнопка очистки ("X") | | +| `spinButton: true` | Кнопка прокрутки | числовые поля | +| `dropListButton: true` | Кнопка выпадающего списка | | +| `markIncomplete: true` | Пометка незаполненного | обязательные поля | +| `skipOnInput: true` | Пропускать при обходе Tab | | +| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` | +| `width` / `height` | Размер | числа | +| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | | +| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` | +| `horizontalStretch: true` | Растягивать по ширине | | + +### Чекбокс (check) + +| Ключ | Описание | +|------|----------| +| `path` | DataPath | +| `titleLocation` | Размещение заголовка | + +### Поле переключателя (radio) + +Радиокнопки или тумблер для выбора одного значения из списка. + +| Ключ | Описание | Пример | +|------|----------|--------| +| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` | +| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` | +| `columnsCount` | Число колонок раскладки | `1`, `2`, ... | +| `titleLocation` | Размещение заголовка | по умолчанию `"none"` | +| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже | + +`choiceList[*]`: + +| Ключ | Описание | +|------|----------| +| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` | +| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности | + +```json +{ + "radio": "СпособКурса", + "path": "Объект.СпособУстановкиКурса", + "radioButtonType": "Auto", + "choiceList": [ + { "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } }, + { "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" } + ] +} +``` + +### Надпись-декорация (label) + +| Ключ | Описание | +|------|----------| +| `title` | Текст надписи (обязательно) | +| `hyperlink: true` | Сделать ссылкой | +| `width` / `height` | Размер | + +### Группа (group) + +Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`. + +| Ключ | Описание | +|------|----------| +| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная | +| `showTitle: true` | Показывать заголовок группы | +| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) | +| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой | +| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | +| `children: [...]` | Вложенные элементы | + +### Таблица (table) + +**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки"). + +| Ключ | Описание | +|------|----------| +| `path` | DataPath (привязка к реквизиту-таблице) | +| `columns: [...]` | Колонки — массив элементов (обычно `input`) | +| `changeRowSet: true` | Разрешить добавление/удаление строк | +| `changeRowOrder: true` | Разрешить перемещение строк | +| `height` | Высота в строках таблицы | +| `header: false` | Скрыть шапку | +| `footer: true` | Показать подвал | +| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` | +| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` | +| `choiceMode: true` | Режим выбора (для форм выбора) | +| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) | +| `enableDrag: true` | Разрешить перетаскивание | +| `enableStartDrag: true` | Разрешить начало перетаскивания | +| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) | +| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar | + +Колонки можно группировать через `columnGroup` (см. ниже). + +### Группа колонок (columnGroup) + +Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`. + +| Ключ | Описание | +|------|----------| +| `name` | Имя элемента (рекомендуется задавать явно) | +| `title` | Заголовок группы | +| `showTitle: false` | Скрыть заголовок | +| `showInHeader: true/false` | Показывать ли группу в шапке таблицы | +| `width` | Ширина | +| `horizontalStretch: false` | Растягивание | +| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) | + +```json +{ "table": "Список", "path": "Список", "columns": [ + { "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [ + { "input": "СрокИсполнения", "path": "Список.СрокИсполнения" }, + { "labelField": "Просрочено", "path": "Список.Просрочено" } + ]}, + { "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [ + { "input": "Исполнитель", "path": "Список.Исполнитель" } + ]}, + { "input": "Комментарий", "path": "Список.Комментарий" } +]} +``` + +### Картинка-поле (picField) + +PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`: + +| Ключ | Описание | +|------|----------| +| `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` | +| `loadTransparent: true` | Скрыть кадр «нет значения» | + +### Страницы (pages + page) + +| Ключ (pages) | Описание | +|------|----------| +| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. | +| `children: [...]` | Массив `page` | + +| Ключ (page) | Описание | +|------|----------| +| `title` | Заголовок вкладки | +| `group` | Ориентация внутри страницы | +| `children: [...]` | Содержимое страницы | + +### Кнопка (button) + +| Ключ | Описание | +|------|----------| +| `command` | Имя команды формы → `Form.Command.Имя` | +| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` | +| `defaultButton: true` | Кнопка по умолчанию | +| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту | +| `picture` | Картинка кнопки | +| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` | +| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` | + +### Командная панель (cmdBar) + +Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы. + +| Ключ | Описание | +|------|----------| +| `autofill: true` | Автозаполнение стандартными командами | +| `children: [...]` | Кнопки панели | + +### Главная автокомандная панель формы (autoCmdBar) + +Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением. + +| Ключ | Описание | +|------|----------| +| `autofill: true/false` | Автозаполнение стандартными командами | +| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` | +| `children: [...]` | Кнопки/popup | + +```json +{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [ + { "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные", + "locationInCommandBar": "InAdditionalSubmenu" } +]} +``` + +Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе. + +### Выпадающее меню (popup) + +| Ключ | Описание | +|------|----------| +| `title` | Заголовок подменю | +| `children: [...]` | Кнопки подменю | + +Используется внутри `cmdBar` для группировки кнопок в подменю: +```json +{ "cmdBar": "Панель", "children": [ + { "popup": "Добавить", "title": "Добавить", "children": [ + { "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" }, + { "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" } + ]} +]} +``` + +### Реквизиты (attributes) + +```json +{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } +{ "name": "Список", "type": "DynamicList", "main": true, "settings": { + "mainTable": "Catalog.Номенклатура", "dynamicDataRead": true +}} +{ "name": "Итого", "type": "decimal(15,2)" } +{ "name": "Таблица", "type": "ValueTable", "columns": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, + { "name": "Количество", "type": "decimal(10,3)" } +]} +``` + +- `savedData: true` — сохраняемые данные +- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`) + +### Команды (commands) + +```json +{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } +``` + +- `title` — заголовок (если отличается от name) +- `picture` — картинка команды + +### Система типов + +**Примитивные:** + +| DSL | XML | +|------------------------|----------------------------------------| +| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers | +| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers | +| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative | +| `"boolean"` | `xs:boolean` | +| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions | + +**Ссылочные и объектные (`cfg:Prefix.Name`):** + +| DSL | Описание | +|-----|----------| +| `"CatalogRef.XXX"` / `"CatalogObject.XXX"` | Справочник | +| `"DocumentRef.XXX"` / `"DocumentObject.XXX"` | Документ | +| `"EnumRef.XXX"` | Перечисление | +| `"DataProcessorObject.XXX"` / `"ReportObject.XXX"` | Обработка / Отчёт | +| `"InformationRegisterRecordSet.XXX"` | Набор записей регистра сведений | +| `"AccumulationRegisterRecordSet.XXX"` | Набор записей регистра накопления | +| `"DynamicList"` | Динамический список | + +Также допустимы: `ChartOfAccountsRef/Object`, `ChartOfCharacteristicTypesRef/Object`, `ChartOfCalculationTypesRef/Object`, `ExchangePlanRef/Object`, `BusinessProcessRef/Object`, `TaskRef/Object`, `AccountingRegisterRecordSet`, `InformationRegisterRecordManager`, `ConstantsSet`. + +**Платформенные:** + +| DSL | XML | +|-----|-----| +| `"ValueTable"` | `v8:ValueTable` | +| `"ValueTree"` | `v8:ValueTree` | +| `"ValueList"` | `v8:ValueListType` | +| `"TypeDescription"` | `v8:TypeDescription` | +| `"UUID"` | `v8:UUID` | +| `"FormattedString"` | `v8ui:FormattedString` | +| `"Picture"` / `"Color"` / `"Font"` | `v8ui:*` | +| `"DataCompositionSettings"` | `dcsset:DataCompositionSettings` | +| `"Type1 \| Type2"` | составной тип (несколько ``) | + +**Недопустимые типы (XDTO-ошибка при загрузке):** + +> `FormDataStructure`, `FormDataCollection`, `FormDataTree` — runtime-типы 1С, не существуют в XML-схеме. Вместо них используйте `CatalogObject.XXX`, `DocumentObject.XXX`, `DataProcessorObject.XXX`, `ValueTable`, `ValueTree`. + +## Связки: элемент + реквизит + +Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`. + +**Таблица** — элемент `table` + реквизит `ValueTable`: +```json +{ + "elements": [ + { "table": "Товары", "path": "Объект.Товары", "columns": [ + { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true, + "columns": [ + { "name": "Товары", "type": "ValueTable", "columns": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" } + ]} + ] + } + ] +} +``` + +Или, если таблица привязана к реквизиту формы (не к Объект): +```json +{ + "elements": [ + { "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [ + { "input": "Наименование", "path": "ТаблицаДанных.Наименование" } + ]} + ], + "attributes": [ + { "name": "ТаблицаДанных", "type": "ValueTable", "columns": [ + { "name": "Наименование", "type": "string(150)" } + ]} + ] +} +``` + +## Паттерны + +### Диалог загрузки файла + +```json +{ + "title": "Загрузка из файла", + "properties": { "autoTitle": false }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "horizontal", "name": "ГруппаФайл", "children": [ + { "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } }, + { "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" } + ]}, + { "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" }, + { "autoCmdBar": "ФормаКоманднаяПанель", "children": [ + { "button": "Загрузить", "command": "Загрузить", "defaultButton": true }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true }, + { "name": "ИмяФайла", "type": "string" }, + { "name": "ПерваяСтрокаЗаголовок", "type": "boolean" }, + { "name": "Результат", "type": "string" } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } + ] +} +``` + +### Мастер (wizard) с шагами + +```json +{ + "title": "Мастер настройки", + "properties": { "autoTitle": false }, + "elements": [ + { "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [ + { "page": "Шаг1", "title": "Параметры", "children": [ + { "input": "Параметр1", "path": "Параметр1" } + ]}, + { "page": "Шаг2", "title": "Результат", "children": [ + { "input": "Итог", "path": "Итог", "readOnly": true } + ]} + ]}, + { "group": "horizontal", "name": "Навигация", "children": [ + { "button": "Назад", "command": "Назад", "title": "< Назад" }, + { "button": "Далее", "command": "Далее", "title": "Далее >" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true }, + { "name": "Параметр1", "type": "string" }, + { "name": "Итог", "type": "string" } + ], + "commands": [ + { "name": "Назад", "action": "НазадОбработка" }, + { "name": "Далее", "action": "ДалееОбработка" } + ] +} +``` + +### Список с фильтром и таблицей + +```json +{ + "title": "Просмотр данных", + "elements": [ + { "group": "horizontal", "name": "Фильтр", "children": [ + { "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } }, + { "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } } + ]}, + { "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [ + { "input": "Дата", "path": "Данные.Дата" }, + { "input": "Сумма", "path": "Данные.Сумма" }, + { "input": "Комментарий", "path": "Данные.Комментарий" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true }, + { "name": "Период", "type": "date" }, + { "name": "Организация", "type": "string" }, + { "name": "Данные", "type": "ValueTable", "columns": [ + { "name": "Дата", "type": "date" }, + { "name": "Сумма", "type": "decimal(15,2)" }, + { "name": "Комментарий", "type": "string(200)" } + ]} + ] +} +``` + +## Продвинутые конструкции (по необходимости) + +Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`: + +- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса +- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки) +- `choice-params.md` — параметры и связи выбора у полей ввода +- `command-interface.md` — командный интерфейс формы +- `roles-access.md` — пользовательская видимость и доступ по ролям +- `companion-panels.md` — контент расширенной подсказки и контекстного меню +- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок) +- `charts.md` — диаграммы и планировщик +- `report-form.md` — свойства формы отчёта +- `type-system-advanced.md` — наборы и составные типы +- `table-advanced.md` — расширенные свойства таблиц +- `layout-advanced.md` — тонкая компоновка и геометрия + +## Автогенерация + +- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически +- **Namespace**: все 17 namespace-деклараций +- **ID**: последовательная нумерация, AutoCommandBar = id="-1" +- **Unknown keys**: выводится предупреждение о нераспознанных ключах + +## Workflow + +1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`). +2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml. +3. **Проверка**: `/form-validate`, `/form-info`. + +## Верификация + +``` +/form-validate — проверка корректности XML +/form-info — визуальная сводка структуры +``` + +## Особенности для внешних обработок (EPF) + +- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) +- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных +- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 38fb12fe..7c7a0a6d 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,6683 +1,6683 @@ -# form-compile v1.174 — Compile 1C managed form from JSON or object metadata -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [string]$JsonPath, - - [Parameter(Mandatory)] - [string]$OutputPath, - - [switch]$FromObject, - [string]$ObjectPath, - [string]$Purpose, - [string]$Preset = "erp-standard", - [string]$EmitDsl -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# ═══════════════════════════════════════════════════════════════════════════ -# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation -# ═══════════════════════════════════════════════════════════════════════════ - -function Parse-ObjectMeta([string]$ObjectPath) { - $doc = New-Object System.Xml.XmlDocument - $doc.PreserveWhitespace = $false - $doc.Load($ObjectPath) - - $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) - $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - - # Detect object type from root child - $metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns) - if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 } - $typeNode = $metaRoot.FirstChild - $objType = $typeNode.LocalName # "Document", "Catalog", etc. - - $propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) - $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) - - # Name - $objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText - - # Synonym (Russian) - $synonym = $objName - $synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - if ($synNode) { $synonym = $synNode.InnerText } - - # Helper: extract type string from md:Type - $extractType = { - param($typeParent) - if (-not $typeParent) { return "string" } - $types = @() - foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) { - $types += $t.InnerText - } - if ($types.Count -eq 0) { return "string" } - return ($types -join " | ") - } - - # Helper: check if type is a reference - $isRefType = { - param([string]$t) - return ($t -match 'Ref\.' -or $t -match 'ссылка\.') - } - - # Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) - $extractFields = { - param($parentNode, [string]$tagName) - $result = @() - if (-not $parentNode) { return $result } - foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { - $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) - $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText - $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } - $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) - $fType = & $extractType $fTypeNode - $result += @{ - Name = $fName - Synonym = $fSyn - Type = $fType - IsRef = (& $isRefType $fType) - } - } - return $result - } - - # Attributes - $attributes = @(& $extractFields $childObjs "Attribute") - - # Tabular sections - $tabularSections = @() - if ($childObjs) { - foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) { - $tsp = $tsNode.SelectSingleNode("md:Properties", $ns) - $tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText - $tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - $tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName } - $tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns) - $tsCols = @(& $extractFields $tsCo "Attribute") - $tabularSections += @{ - Name = $tsName - Synonym = $tsSyn - Columns = $tsCols - } - } - } - - $meta = @{ - Type = $objType - Name = $objName - Synonym = $synonym - Attributes = $attributes - TabularSections = $tabularSections - } - - # Type-specific properties - switch ($objType) { - "Document" { - $ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns) - $meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" } - } - "Catalog" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) - $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - # Owners - $owners = @() - foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { - $owners += $ow.InnerText - } - $meta.Owners = $owners - } - "InformationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) - $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } - $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) - $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } - } - "AccumulationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) - $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } - } - "ChartOfCharacteristicTypes" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) - $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - $owners = @() - foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { - $owners += $ow.InnerText - } - $meta.Owners = $owners - $meta.HasValueType = $true - } - "ExchangePlan" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $meta.Hierarchical = $false - $meta.HierarchyType = $null - $meta.Owners = @() - } - "ChartOfAccounts" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $meta.Hierarchical = $true - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - $meta.Owners = @() - $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) - $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } - $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") - $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") - } - } - - return $meta -} - -function Load-Preset([string]$PresetName, [string]$ScriptDir) { - # Hardcoded defaults (ERP-oriented) - $defaults = @{ - "document.item" = @{ - header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" } - footer = @{ fields = @("Комментарий"); position = "insidePage" } - tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true } - additional = @{ position = "page"; layout = "2col"; bspGroup = $true } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - commandBar = "auto" - properties = @{ autoTitle = $false } - } - "document.list" = @{ - columns = "all"; columnType = "labelField"; hiddenRef = $true - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "document.choice" = @{ - basedOn = "document.list" - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "catalog.item" = @{ - header = @{ layout = "1col"; distribute = "left" } - codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" } - parent = @{ title = "Входит в группу"; position = "afterCodeDescription" } - owner = @{ readOnly = $true; position = "first" } - tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true } - footer = @{ fields = @(); position = "none" } - additional = @{ position = "none"; bspGroup = $true } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - commandBar = "auto" - properties = @{} - } - "catalog.folder" = @{ - parent = @{ title = "Входит в группу" } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "catalog.list" = @{ - columns = "all"; columnType = "labelField"; hiddenRef = $true - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "catalog.choice" = @{ - basedOn = "catalog.list"; choiceMode = $true - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - # ─── Register defaults ─── - "informationRegister.record" = @{ - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "informationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "accumulationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - # ─── Catalog-like type defaults ─── - "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } - "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } - "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } - "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } - "exchangePlan.item" = @{ basedOn = "catalog.item" } - "exchangePlan.list" = @{ basedOn = "catalog.list" } - "exchangePlan.choice" = @{ basedOn = "catalog.choice" } - # ─── ChartOfAccounts defaults ─── - "chartOfAccounts.item" = @{ - parent = @{ title = "Подчинен счету" } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{} - } - "chartOfAccounts.folder" = @{ - parent = @{ title = "Подчинен счету" } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "chartOfAccounts.list" = @{ basedOn = "catalog.list" } - "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } - } - - # Deep merge helper - $deepMerge = { - param($base, $overlay) - if (-not $overlay) { return $base } - if (-not $base) { return $overlay } - $result = @{} - foreach ($k in $base.Keys) { $result[$k] = $base[$k] } - foreach ($k in $overlay.Keys) { - if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { - $result[$k] = & $deepMerge $result[$k] $overlay[$k] - } else { - $result[$k] = $overlay[$k] - } - } - return $result - } - - # Try built-in preset - $presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets" - $builtInPath = Join-Path $presetDir "$PresetName.json" - if (Test-Path $builtInPath) { - $presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json - # Convert PSCustomObject to hashtable recursively - $toHash = { - param($obj) - if ($obj -is [System.Management.Automation.PSCustomObject]) { - $h = @{} - foreach ($p in $obj.PSObject.Properties) { - $h[$p.Name] = & $toHash $p.Value - } - return $h - } - if ($obj -is [System.Object[]]) { - return @($obj | ForEach-Object { & $toHash $_ }) - } - return $obj - } - $presetHash = & $toHash $presetJson - foreach ($k in @($presetHash.Keys)) { - $defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k] - } - } - - # Try project-level preset (scan up from output path) - $scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved) - while ($scanDir) { - $projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json" - if (Test-Path $projPreset) { - $projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json - $projHash = & $toHash $projJson - foreach ($k in @($projHash.Keys)) { - $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] - } - break - } - $parentDir = Split-Path $scanDir -Parent - if ($parentDir -eq $scanDir) { break } - $scanDir = $parentDir - } - - # Resolve basedOn references - foreach ($k in @($defaults.Keys)) { - $sect = $defaults[$k] - if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) { - $baseName = $sect["basedOn"] - if ($defaults.ContainsKey($baseName)) { - $merged = & $deepMerge $defaults[$baseName] $sect - $merged.Remove("basedOn") - $defaults[$k] = $merged - } - } - } - - return $defaults -} - -# --- Helper: build a field element DSL entry --- -# Non-displayable types — cannot be bound to form elements -$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') - -function Test-DisplayableType([string]$typeStr) { - foreach ($nd in $script:nonDisplayableTypes) { - if ($typeStr -match [regex]::Escape($nd)) { return $false } - } - return $true -} - -function New-FieldElement { - param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps) - - $isRef = ($attrType -match 'Ref\.') - $isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean') - - # Determine element type - $elType = "input" - if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") { - $elType = "check" - } - - $el = [ordered]@{ $elType = $attrName; path = $dataPath } - - # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. - # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы - # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) - - # Extra props - if ($extraProps) { - foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] } - } - - return $el -} - -# --- Catalog DSL generators --- -function Generate-CatalogDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - - $purposeKey = "catalog.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } - $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } - - switch ($purpose) { - "Folder" { return Generate-CatalogFolderDSL $meta $p } - "List" { return Generate-CatalogListDSL $meta $p } - "Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData } - "Item" { return Generate-CatalogItemDSL $meta $p $fd } - } -} - -function Generate-CatalogFolderDSL($meta, [hashtable]$p) { - $elements = @() - # Code (if CodeLength > 0) - if ($meta.CodeLength -gt 0) { - $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - # Description - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - # Parent - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } - $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } - if ($parentTitle) { $parentEl["title"] = $parentTitle } - $elements += $parentEl - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - $formProps = [ordered]@{ useForFoldersAndItems = "Folders" } - foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $elements - attributes = @( - [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } - ) - } -} - -function Generate-CatalogListDSL($meta, [hashtable]$p) { - # Columns - $columns = @() - # Description always first - $columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" } - # Code if present - if ($meta.CodeLength -gt 0) { - $columns += [ordered]@{ labelField = "Код"; path = "Список.Code" } - } - # Custom attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } - } - # Hidden ref - if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { - $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - # Hierarchical properties - if ($meta.Hierarchical) { - $tableEl["initialTreeView"] = "ExpandTopLevel" - $tableEl["enableStartDrag"] = $true - $tableEl["enableDrag"] = $true - } - - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = @($tableEl) - attributes = @( - [ordered]@{ - name = "Список"; type = "DynamicList"; main = $true - settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true } - } - ) - } -} - -function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { - # Start from list - $listKey = "catalog.list" - $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } - $dsl = Generate-CatalogListDSL $meta $lp - - # Add choice-specific properties - $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" - if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } - - # Set ChoiceMode on table - $dsl.elements[0]["choiceMode"] = $true - - return $dsl -} - -function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) { - $headerChildren = @() - - # Owner (if subordinate) - if ($meta.Owners -and $meta.Owners.Count -gt 0) { - $ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true } - $headerChildren += $ownerEl - } - - # Code + Description - $cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" } - $cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" } - $hasCode = ($meta.CodeLength -gt 0) - - if ($cdLayout -eq "horizontal" -and $hasCode) { - $cdChildren = @() - $descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" } - $codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" } - if ($cdOrder -eq "descriptionFirst") { - $cdChildren = @($descEl, $codeEl) - } else { - $cdChildren = @($codeEl, $descEl) - } - $headerChildren += [ordered]@{ - group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false - representation = "none"; children = $cdChildren - } - } else { - # Vertical or no code - $headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - if ($hasCode) { - $headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - } - - # Parent (for hierarchical catalogs) - $parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" } - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } - if ($meta.Hierarchical) { - $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } - if ($parentTitle) { $parentEl["title"] = $parentTitle } - if ($parentPos -eq "beforeCodeDescription") { - # Insert before Code/Description (after Owner if present) - $insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 } - $newChildren = @() - for ($i = 0; $i -lt $headerChildren.Count; $i++) { - if ($i -eq $insertIdx) { $newChildren += $parentEl } - $newChildren += $headerChildren[$i] - } - $headerChildren = $newChildren - } else { - # afterCodeDescription (default) - $headerChildren += $parentEl - } - } - - # Custom attributes → header - $footerFieldNames = @() - if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) } - - foreach ($attr in $meta.Attributes) { - if ($footerFieldNames -contains $attr.Name) { continue } - if (-not (Test-DisplayableType $attr.Type)) { continue } - $headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Build root elements - $rootElements = @() - - # ГруппаШапка - $rootElements += [ordered]@{ - group = "vertical"; name = "ГруппаШапка"; showTitle = $false - representation = "none"; children = $headerChildren - } - - # Tabular sections - $tsExclude = @("ДополнительныеРеквизиты", "Представления") - if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } - $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } - $tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" } - - $visibleTS = @() - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $visibleTS += $ts - } - - foreach ($ts in $visibleTS) { - $tsCols = @() - if ($tsLineNumber) { - $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } - } - foreach ($col in $ts.Columns) { - $colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{} - $tsCols += $colEl - } - $tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - $rootElements += $tableEl - } - - # Footer fields - foreach ($fn in $footerFieldNames) { - $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } - if ($fAttr) { - $rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - - # BSP group - $bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } - if ($bspGroup) { - $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - - # Properties - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - # UseForFoldersAndItems - if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") { - $formProps["useForFoldersAndItems"] = "Items" - } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $rootElements - attributes = @( - [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } - ) - } -} - -# --- Document DSL generators --- -function Generate-DocumentDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - - $purposeKey = "document.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } - $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } - - switch ($purpose) { - "List" { return Generate-DocumentListDSL $meta $p } - "Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData } - "Item" { return Generate-DocumentItemDSL $meta $p $fd } - } -} - -function Generate-DocumentListDSL($meta, [hashtable]$p) { - $columns = @() - # Standard columns: Number + Date - $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } - $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } - # All custom attributes as labelField - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } - } - # Hidden ref - if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { - $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = @($tableEl) - attributes = @( - [ordered]@{ - name = "Список"; type = "DynamicList"; main = $true - settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true } - } - ) - } -} - -function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { - $listKey = "document.list" - $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } - $dsl = Generate-DocumentListDSL $meta $lp - - $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" - if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } - - return $dsl -} - -function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) { - $headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" } - $headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" } - $headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" } - $dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" } - - $footerFields = @() - if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) } - $footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" } - - $addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" } - $addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" } - $addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } - $addLeft = @(); $addRight = @() - if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) } - if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) } - - $headerRight = @() - if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) } - - $tsExclude = @("ДополнительныеРеквизиты") - if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } - $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } - - # Classify attributes - $claimed = @{} - foreach ($fn in $footerFields) { $claimed[$fn] = "footer" } - foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" } - foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" } - foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" } - - $unclaimed = @() - foreach ($attr in $meta.Attributes) { - if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr } - } - - # Distribute unclaimed - $leftAttrs = @(); $rightExtraAttrs = @() - switch ($headerDistribute) { - "left" { $leftAttrs = $unclaimed } - "right" { $rightExtraAttrs = $unclaimed } - default { # "even" - $half = [Math]::Ceiling($unclaimed.Count / 2) - for ($i = 0; $i -lt $unclaimed.Count; $i++) { - if ($i -lt $half) { $leftAttrs += $unclaimed[$i] } - else { $rightExtraAttrs += $unclaimed[$i] } - } - } - } - - # Build ГруппаНомерДата - $numDateChildren = @( - [ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 } - [ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle } - ) - $numDateGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren - } - - # Build left column - $leftChildren = @($numDateGroup) - foreach ($attr in $leftAttrs) { - $leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Build right column - $rightChildren = @() - foreach ($rn in $headerRight) { - $rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn } - if ($rAttr) { - $rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - foreach ($attr in $rightExtraAttrs) { - $rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Header group - $headerGroup = $null - if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) { - $headerGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren } - [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren } - ) - } - } else { - # 1col or no right items - $allHeaderFields = $leftChildren + $rightChildren - $headerGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields } - ) - } - } - - # Footer elements - $footerElements = @() - foreach ($fn in $footerFields) { - $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } - if ($fAttr -and (Test-DisplayableType $fAttr.Type)) { - $footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - - # Visible tabular sections - $visibleTS = @() - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $visibleTS += $ts - } - - # Additional page content - $additionalPage = $null - if ($addPos -eq "page") { - $addLeftEls = @(); $addRightEls = @() - foreach ($aln in $addLeft) { - $alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln } - if ($alAttr) { - $addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - foreach ($arn in $addRight) { - $arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn } - if ($arAttr) { - $addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - $addPageChildren = @() - if ($addLayout -eq "2col") { - $addPageChildren += [ordered]@{ - group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls } - [ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls } - ) - } - } else { - $addPageChildren += @($addLeftEls + $addRightEls) - } - if ($addBspGroup) { - $addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - $additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren } - } - - # Build TS page elements - $tsPages = @() - foreach ($ts in $visibleTS) { - $tsCols = @() - if ($tsLineNumber) { - $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } - } - foreach ($col in $ts.Columns) { - $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}) - } - $tsPages += [ordered]@{ - page = "Группа$($ts.Name)"; title = $ts.Synonym - children = @( - [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - ) - } - } - - # Assemble root elements - $rootElements = @() - - if ($visibleTS.Count -eq 0) { - # Simple form — no Pages - $rootElements += $headerGroup - if ($footerElements.Count -gt 0) { $rootElements += $footerElements } - if ($addBspGroup -and $addPos -ne "none") { - $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - } else { - # Pages form - if ($headerPos -eq "abovePages") { - $rootElements += $headerGroup - $pagesChildren = @() - $pagesChildren += $tsPages - if ($additionalPage) { $pagesChildren += $additionalPage } - $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } - } else { - # insidePage (default) - $osnovnoeChildren = @($headerGroup) - if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) { - $osnovnoeChildren += $footerElements - } - $pagesChildren = @() - $pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren } - $pagesChildren += $tsPages - if ($additionalPage) { $pagesChildren += $additionalPage } - $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } - } - - # Footer below pages - if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) { - $rootElements += $footerElements - } - } - - # Properties - $formProps = [ordered]@{ autoTitle = $false } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $rootElements - attributes = @( - [ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true } - ) - } -} - -# ─── InformationRegister ────────────────────────────────────────────────── - -function Generate-InformationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "informationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } - "List" { return Generate-InformationRegisterListDSL $meta $p } - } -} - -function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { - $elements = @() - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - - # Period first (if periodic) - if ($isPeriodic) { - $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" - - $columns = @() - # Period - if ($isPeriodic) { - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - } - # Recorder/LineNumber for subordinate registers - if ($isRecorderSubordinate) { - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── AccumulationRegister ───────────────────────────────────────────────── - -function Generate-AccumulationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "accumulationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - switch ($purpose) { - "List" { return Generate-AccumulationRegisterListDSL $meta $p } - } -} - -function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { - $columns = @() - # AccumulationRegisters always have Period, Recorder, LineNumber - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── - -function Generate-ChartOfCharacteristicTypesDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - $catObjType = "CatalogObject.$($meta.Name)" - $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $ccoctListType - } - } - - # For Item forms: inject ValueType field after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $vtEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $vtEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── - -function Generate-ExchangePlanDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # ExchangePlans are not hierarchical and have no Folder form - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ExchangePlan types - $catObjType = "CatalogObject.$($meta.Name)" - $epObjType = "ExchangePlanObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $epListType = "ExchangePlan.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $epObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $epListType - } - } - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } - $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $sentEl - $newElements += $recvEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ChartOfAccounts ────────────────────────────────────────────────────── - -function Generate-ChartOfAccountsDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "chartOfAccounts.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } - "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } - "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } - "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } - } -} - -function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { - $elements = @() - - # Header: Code + Parent - $headerLeftChildren = @() - if ($meta.CodeLength -gt 0) { - $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - $headerRightChildren = @() - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - if ($headerRightChildren.Count -gt 0) { - $elements += [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } - [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } - ) - } - } elseif ($headerLeftChildren.Count -gt 0) { - $elements += $headerLeftChildren - } - - # Description - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - - # OffBalance - $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } - - # AccountingFlags as checkboxes - if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { - $flagChildren = @() - foreach ($flag in $meta.AccountingFlags) { - $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } - } - $elements += [ordered]@{ - group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" - children = $flagChildren - } - } - - # ExtDimensionTypes table - if ($meta.MaxExtDimensionCount -gt 0) { - # Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С), - # иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта. - $edTable = "ВидыСубконто" - $edCols = @() - $edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } - $edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } - if ($meta.ExtDimensionAccountingFlags) { - foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { - $edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } - } - } - $elements += [ordered]@{ - table = $edTable - path = "Объект.ExtDimensionTypes" - columns = $edCols - } - } - - # Custom attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - # Tabular sections - $tsExclude = @("ДополнительныеРеквизиты","Представления") - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $tsCols = @() - foreach ($col in $ts.Columns) { - if (-not (Test-DisplayableType $col.Type)) { continue } - $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) - } - $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { - $elements = @() - if ($meta.CodeLength -gt 0) { - $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - useForFoldersAndItems = "Folders" - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { - # Delegate to Catalog List and patch types - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - -# --- Detect XML format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } -Assert-EditAllowed $script:outPathResolved 'editable' -$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) - -# --- 0. Path normalization and mode dispatch --- - -# Form name → purpose mapping -$script:formNameToPurpose = @{ - "ФормаДокумента" = "Item" - "ФормаЭлемента" = "Item" - "ФормаСписка" = "List" - "ФормаВыбора" = "Choice" - "ФормаГруппы" = "Folder" - "ФормаЗаписи" = "Record" - "ФормаСчета" = "Item" - "ФормаУзла" = "Item" -} - -if ($FromObject -and $JsonPath) { - Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode." - exit 1 -} -if (-not $FromObject -and -not $JsonPath) { - Write-Error "Either -JsonPath or -FromObject is required." - exit 1 -} - -if ($FromObject) { - # Normalize OutputPath: append /Ext/Form.xml if missing - $outNorm = $OutputPath -replace '[\\/]$', '' - if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') { - if ($outNorm -match '[/\\]Ext$') { - $OutputPath = "$outNorm/Form.xml" - } else { - $OutputPath = "$outNorm/Ext/Form.xml" - } - Write-Host "[resolved] OutputPath -> $OutputPath" - } - - # Resolve object path and purpose from OutputPath convention: - # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } - $pathParts = $outAbs -split '[/\\]' - # Find "Forms" segment - $formsIdx = -1 - for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { - if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break } - } - - $resolvedObjectPath = $null - $resolvedPurpose = $null - - if ($formsIdx -ge 2) { - $formName = $pathParts[$formsIdx + 1] - $objectName = $pathParts[$formsIdx - 1] - $typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar - - # Derive purpose from form name - if ($script:formNameToPurpose.ContainsKey($formName)) { - $resolvedPurpose = $script:formNameToPurpose[$formName] - } - - # Derive object XML path - $candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml" - if (Test-Path $candidateObjPath) { - $resolvedObjectPath = $candidateObjPath - } - } - - # Apply: explicit -ObjectPath / -Purpose override resolved values - $fromObjPath = $null - if ($ObjectPath) { - $fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath } - # Append .xml if missing - if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" } - } elseif ($resolvedObjectPath) { - $fromObjPath = $resolvedObjectPath - Write-Host "[resolved] ObjectPath -> $fromObjPath" - } else { - Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly." - exit 1 - } - - if (-not (Test-Path $fromObjPath)) { - Write-Error "Object file not found: $fromObjPath" - exit 1 - } - - $effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" } - if ($resolvedPurpose -and -not $Purpose) { - Write-Host "[resolved] Purpose -> $effectivePurpose" - } - - $meta = Parse-ObjectMeta $fromObjPath - Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)" - - $presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot - - $supportedPurposes = switch ($meta.Type) { - "Document" { @("Item","List","Choice") } - "Catalog" { @("Item","Folder","List","Choice") } - "InformationRegister" { @("Record","List") } - "AccumulationRegister" { @("List") } - "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } - "ExchangePlan" { @("Item","List","Choice") } - "ChartOfAccounts" { @("Item","Folder","List","Choice") } - default { @() } - } - if ($supportedPurposes.Count -eq 0) { - Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." - exit 1 - } - if ($supportedPurposes -notcontains $effectivePurpose) { - Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')" - exit 1 - } - - # Generate DSL - $dsl = switch ($meta.Type) { - "Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfAccounts" { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - } - - # Emit DSL if requested - if ($EmitDsl) { - $dslJson = $dsl | ConvertTo-Json -Depth 20 - $dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl } - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($dslPath, $dslJson, $enc) - Write-Host "[from-object] DSL saved: $dslPath" - } - - # Feed DSL into existing compiler - $dslJson = $dsl | ConvertTo-Json -Depth 20 - $def = $dslJson | ConvertFrom-Json -} else { - # --- 1. Load and validate JSON (original mode) --- - - if (-not (Test-Path $JsonPath)) { - Write-Error "File not found: $JsonPath" - exit 1 - } - - $json = Get-Content -Raw -Encoding UTF8 $JsonPath - $def = $json | ConvertFrom-Json -} - -# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile) -$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path } -function Resolve-QueryValue { - param([string]$val, [string]$baseDir) - if (-not $val.StartsWith("@")) { return $val } - $filePath = $val.Substring(1) - if ([System.IO.Path]::IsPathRooted($filePath)) { - $candidates = @($filePath) - } else { - $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath)) - } - foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" - exit 1 -} - -# --- 2. ID allocator --- - -$script:nextId = 1 -function New-Id { - $id = $script:nextId - $script:nextId++ - return $id -} - -# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё -# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. -function Assert-UniqueName { - param([string]$name, [hashtable]$seen, [string]$kind) - if ($seen.ContainsKey($name)) { - Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')" - exit 1 - } - $seen[$name] = $true -} - -# --- 3. XML helper --- - -$script:xml = New-Object System.Text.StringBuilder 8192 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - # Экранирование ТЕКСТА элемента (, ): только & < > . - # Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально); - # " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов. - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>') -} - -# --- 4. Multilang helper --- - -# Эмитит для значения: строка → один ru-элемент; объект {lang:text} → по элементу на язык. -function Emit-MLItems { - param($val, [string]$indent) - if ($val -is [System.Collections.IDictionary]) { - foreach ($k in $val.Keys) { - X "$indent"; X "$indent`t$k"; X "$indent`t$(Esc-Xml "$($val[$k])")"; X "$indent" - } - } elseif ($val -is [System.Management.Automation.PSCustomObject]) { - foreach ($p in $val.PSObject.Properties) { - X "$indent"; X "$indent`t$($p.Name)"; X "$indent`t$(Esc-Xml "$($p.Value)")"; X "$indent" - } - } else { - X "$indent"; X "$indent`tru"; X "$indent`t$(Esc-Xml "$val")"; X "$indent" - } -} - -function Emit-MLText { - param([string]$tag, $text, [string]$indent, [string]$xsiType) - $attr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } - X "$indent<$tag$attr>" - Emit-MLItems -val $text -indent "$indent`t" - X "$indent" -} - -# и подобные DCS-подписи: платформа пишет плоскую строку как -# xsi:type="xs:string" (скаляр; корпус 26), мультиязычный текст — как xsi:type="v8:LocalStringType" -# (7). Декомпилятор различает (Get-PresText: строка ИЛИ объект {ru,en}). -function Emit-USPresentation { - param($val, [string]$tag, [string]$indent) - if ($null -eq $val) { return } - if ($val -is [string]) { - X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)" - } else { - Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType" - } -} - -# Детектор «настоящей» inline-разметки форматированного текста (1С: ///… -# и закрывающий ). Плейсхолдеры вида <не заполнен> НЕ срабатывают (нет известного тега/). -# ВАЖНО: regex должен быть идентичен в form-decompile (иначе гибрид-раундтрип поедет). -$script:fmtMarkupRe = '|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' -function Test-HasRealMarkup { - param($text) - if ($null -eq $text) { return $false } - $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } - elseif ($text -is [System.Management.Automation.PSCustomObject]) { @($text.PSObject.Properties.Value) } - else { @("$text") } - foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } - return $false -} -# DSL-значение ML-поля → @{ text; formatted }. Форма {text, formatted} = явный override; -# строка/мапа → авто-детект formatted по разметке. -function Resolve-MLFormatted { - param($val) - $hasText = $false - if ($val -is [System.Management.Automation.PSCustomObject]) { $hasText = [bool]$val.PSObject.Properties['text'] } - elseif ($val -is [System.Collections.IDictionary]) { $hasText = $val.Contains('text') } - if ($hasText) { - $t = if ($val -is [System.Collections.IDictionary]) { $val['text'] } else { $val.text } - $f = if ($val -is [System.Collections.IDictionary]) { $val['formatted'] } else { $val.formatted } - return @{ text = $t; formatted = [bool]$f } - } - return @{ text = $val; formatted = (Test-HasRealMarkup $val) } -} - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип -# (harness нормализует GUID для хвоста с иными идентификаторами). -$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - -# ───────────────────────────────────────────────────────────────────────────── -# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. -# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). -# ───────────────────────────────────────────────────────────────────────────── -function New-Guid-String { return [System.Guid]::NewGuid().ToString() } - -$script:comparisonTypes = @{ - "=" = "Equal"; "<>" = "NotEqual" - ">" = "Greater"; ">=" = "GreaterOrEqual" - "<" = "Less"; "<=" = "LessOrEqual" - "in" = "InList"; "notIn" = "NotInList" - "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" - "contains" = "Contains"; "notContains" = "NotContains" - "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" - "like" = "Like"; "notLike" = "NotLike" - "подобно" = "Like"; "неподобно" = "NotLike" # рус. синоним (хэш регистронезависим: ПОДОБНО=подобно) - "filled" = "Filled"; "notFilled" = "NotFilled" -} - -function Parse-FilterShorthand { - param([string]$s) - $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } - if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } - if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } - if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } - if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } - if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' } - $s = $s.Trim() - $opPatterns = @('<>', '>=', '<=', '=', '>', '<', - 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', - 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', - 'notLike\b', 'like\b', 'неподобно\b', 'подобно\b', - 'notFilled\b', 'filled\b') - $opJoined = $opPatterns -join '|' - if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { - $result.field = $Matches[1].Trim() - $result.op = $Matches[2].Trim() - $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } - if ($valPart -and $valPart -ne "_") { - if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" } - elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart } # дата без valueType → Emit-FilterItem выведет StandardBeginningDate Custom (дефолт даты в фильтре) - elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" } - elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" } - else { $result.value = $valPart; $result["valueType"] = "xs:string" } - } - } else { $result.field = $s } - return $result -} - -# Значение типа v8:Type (напр. тип «Неопределено» = :Undefined) ссылается на тип -# платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО -# на теге значения (префикс авто-назначаемый: d6p1/d8p1/dN…). Без объявления QName битый. -# Возвращает строку-атрибут ' xmlns:="…"' (перед xsi:type), либо "". -function Get-ValueTypeNsAttr { - param([string]$valueType, [string]$value) - if ($valueType -eq 'v8:Type' -and "$value" -match '^([A-Za-z]\w*):') { - $pref = $Matches[1] - if ($pref -notin @('xs','cfg','v8','v8ui','ent','dcscor','dcsset','dcssch')) { - return " xmlns:$pref=`"http://v8.1c.ru/8.2/data/types`"" - } - } - return "" -} - -function Emit-FilterItem { - param($item, [string]$indent) - if ($item.group) { - $groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } } - X "$indent" - if ($item.use -eq $false) { X "$indent`tfalse" } # группа отключена (перед groupType, порядок исходника) - X "$indent`t$groupType" - if ($item.items) { - foreach ($sub in $item.items) { - if ($sub -is [string]) { - $parsed = Parse-FilterShorthand $sub - $obj = @{ field = $parsed.field; op = $parsed.op } - if ($parsed.use -eq $false) { $obj.use = $false } - if ($null -ne $parsed.value) { $obj.value = $parsed.value } - if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } - if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } - if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } - $sub = [pscustomobject]$obj - } - Emit-FilterItem -item $sub -indent "$indent`t" - } - } - if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } - if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } - if ($item.userSettingID) { - $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t$(Esc-Xml $guid)" - } - if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" - return - } - X "$indent" - if ($item.use -eq $false) { X "$indent`tfalse" } - X "$indent`t$(Esc-Xml "$($item.field)")" - $compType = $script:comparisonTypes["$($item.op)"] - if (-not $compType) { $compType = "$($item.op)" } - X "$indent`t$(Esc-Xml $compType)" - $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) - if ($valIsArray) { - if (@($item.value).Count -eq 0) { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t-1" - X "$indent`t" - } else { - foreach ($v in $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - if ($v -is [bool]) { $vt = 'xs:boolean' } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } - elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } - elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } - elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } - else { $vt = 'xs:string' } - } - $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } - $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v" - X "$indent`t$vStr" - } - } - } elseif ($null -ne $item.value -and ( - "$($item.valueType)" -match 'Standard(Beginning|End)Date$' -or - (-not $item.valueType -and "$($item.value)" -match '^\d{4}-\d{2}-\d{2}T'))) { - # Стандартная дата начала/окончания. Формы значения: - # объект {variant, date?} — полная (Custom несёт ); - # строка-вариант "BeginningOfThisDay" — именованный вариант без даты; - # голая ISO-дата без valueType — шорткат для Custom+date (дата в фильтре платформой - # почти всегда хранится как StandardBeginningDate Custom, корпус 268 vs 2 xs:dateTime; - # явный valueType="xs:dateTime" → плоская дата, ветка ниже). - $sdType = if ($item.valueType) { "$($item.valueType)" -replace '^v8:','' } else { 'StandardBeginningDate' } - $sv = $item.value - if (($sv -is [PSCustomObject]) -or ($sv -is [System.Collections.IDictionary])) { - $variant = if ($sv -is [PSCustomObject]) { "$($sv.variant)" } else { "$($sv['variant'])" } - $hasDate = if ($sv -is [PSCustomObject]) { [bool]$sv.PSObject.Properties['date'] } else { $sv.Contains('date') } - $dateV = if ($hasDate) { if ($sv -is [PSCustomObject]) { "$($sv.date)" } else { "$($sv['date'])" } } else { $null } - } elseif ("$sv" -match '^\d{4}-\d{2}-\d{2}T') { - $variant = 'Custom'; $hasDate = $true; $dateV = "$sv" - } else { - $variant = "$sv"; $hasDate = $false; $dateV = $null - } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $variant)" - if ($hasDate) { X "$indent`t`t$(Esc-Xml $dateV)" } - X "$indent`t" - } elseif ("$($item.value)" -eq '_') { - # "_" — маркер пустого значения: платформа эмитит пустой self-closing - # (напр. — сравнение с незаданным полем). - $vt = if ($item.valueType) { "$($item.valueType)" } else { 'xs:string' } - X "$indent`t" - } elseif ($null -ne $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - $v = $item.value - if ($v -is [bool]) { $vt = "xs:boolean" } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" } - elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" } - elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" } - elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" } - else { $vt = "xs:string" } - } - $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } - $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)" - X "$indent`t$vStr" - } - if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } - if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } - if ($item.userSettingID) { - $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t$(Esc-Xml $uid)" - } - if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" -} - -function Emit-Filter { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent" - foreach ($item in $items) { - if ($item -is [string]) { - $parsed = Parse-FilterShorthand $item - $obj = @{ field = $parsed.field; op = $parsed.op } - if ($parsed.use -eq $false) { $obj.use = $false } - if ($null -ne $parsed.value) { $obj.value = $parsed.value } - if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } - if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } - if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } - Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t" - } else { Emit-FilterItem -item $item -indent "$indent`t" } - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" -} - -function Emit-Order { - param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent" - foreach ($item in $items) { - if ($item -is [string]) { - if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t" } } - else { - $parts = $item -split '\s+' - $field = $parts[0] - $dir = "Asc" - if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" } - elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $field)" - X "$indent`t`t$dir" - X "$indent`t" - } - } else { - if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t" }; continue } - $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } - if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" } - X "$indent`t" - if ($item.use -eq $false) { X "$indent`t`tfalse" } - X "$indent`t`t$(Esc-Xml "$($item.field)")" - X "$indent`t`t$dir" - if ($item.viewMode) { X "$indent`t`t$(Esc-Xml "$($item.viewMode)")" } - X "$indent`t" - } - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" -} - -function Emit-AppearanceValue { - param([string]$key, $val, [string]$indent) - X "$indent" - function _HasKey { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } - if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } - return $false - } - function _Get { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return $o.$k } - if ($o -is [System.Collections.IDictionary]) { return $o[$k] } - return $null - } - $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') - $useWrapper = $false - $innerVal = $val - $nestedItems = $null - if ($isTopLevelLine) { - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { - $innerVal = (_Get $val 'value') - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } - if ($useWrapper) { X "$indent`tfalse" } - X "$indent`t$(Esc-Xml $key)" - $isFontDict = $false - if ($innerVal -is [PSCustomObject]) { - $tProp = $innerVal.PSObject.Properties['@type'] - if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } - } elseif ($innerVal -is [System.Collections.IDictionary]) { - if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } - } - $isLineDict = $false - if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } - $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) - if ($isLineDict) { - $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } - $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } - $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $ls)" - X "$indent`t" - } elseif ($isFontDict) { - $attrParts = @() - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $null - if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } } - else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } } - if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } - } - X "$indent`t" - } elseif ($isDict -and (_HasKey $innerVal 'field')) { - # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки - X "$indent`t$(Esc-Xml "$(_Get $innerVal 'field')")" - } elseif ($isDict) { - # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value - Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType" - } else { - $actualVal = "$innerVal" - $keyTypeMap = @{ - 'Размещение' = 'dcscor:DataCompositionTextPlacementType' - 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' - 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' - 'ОриентацияТекста' = 'xs:decimal' - 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' - 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' - } - $keyType = $keyTypeMap[$key] - if ($keyType) { X "$indent`t$(Esc-Xml $actualVal)" } - elseif ($actualVal -match '^(style|web|win):') { X "$indent`t$(Esc-Xml $actualVal)" } - elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t$actualVal" } - elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { - # Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит - # нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше). - # Пустая строка → самозакрывающийся тег (как у платформы). - if ($actualVal -eq '') { X "$indent`t" } - else { X "$indent`t$(Esc-Xml $actualVal)" } - } - elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t$actualVal" } - elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t$(Esc-Xml $actualVal)" } - else { X "$indent`t$(Esc-Xml $actualVal)" } - } - if ($nestedItems) { - $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } - if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } } - elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } } - } - X "$indent" -} - -function Emit-ConditionalAppearance { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance', $blockUserSettingPresentation = $null) - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent<$wrapTag>" - foreach ($ca in $items) { - X "$indent`t" - if ($ca.use -eq $false) { X "$indent`t`tfalse" } - if ($ca.selection -and $ca.selection.Count -gt 0) { - X "$indent`t`t" - foreach ($sel in $ca.selection) { - X "$indent`t`t`t" - X "$indent`t`t`t`t$(Esc-Xml "$sel")" - X "$indent`t`t`t" - } - X "$indent`t`t" - } else { X "$indent`t`t" } - if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" } - else { X "$indent`t`t" } - if ($ca.appearance) { - X "$indent`t`t" - foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" } - X "$indent`t`t" - } - if ($ca.presentation) { - if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { - # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) - X "$indent`t`t" - Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t" - X "$indent`t`t" - } - else { X "$indent`t`t$(Esc-Xml "$($ca.presentation)")" } - } - if ($ca.viewMode) { X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")" } - if ($ca.userSettingID) { - $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } - X "$indent`t`t$(Esc-Xml $uid)" - } - if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } - if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { - $useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') - $set = @{} - foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } - foreach ($n in $useInOrder) { - if ($set.ContainsKey($n)) { - $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) - X "$indent`t`tDontUse" - } - } - } - X "$indent`t" - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" -} - -# === Группировка строк динамического списка (DCS-структура ListSettings) === -# Линейная цепочка (каждый уровень = одно поле в groupItems; -# вложенность — через дочерний ). Зеркало skd Emit-GroupItems/Emit-StructureItem, -# но плоская модель уровней (список всегда линеен, без selection/order/children). -function Get-ListGroupingValue { - param($st) - foreach ($k in 'grouping','structure','группировка') { - if ($st.PSObject.Properties[$k] -and $st.$k) { return $st.$k } - } - return $null -} - -function Parse-ListGrouping { - param($grouping) - # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. - # Unary comma: иначе PS разворачивает одноэлементный массив при return → строка → индексация даёт char. - if (-not $grouping) { return ,@() } - if ($grouping -is [string]) { return ,@($grouping -split '\s*>\s*' | Where-Object { "$_" -ne '' }) } - return ,@($grouping) -} - -function Emit-GroupItemField { - param($level, [string]$indent) - if ($level -is [string]) { - $field = $level; $gt = 'Items'; $pat = 'None'; $pab = '0001-01-01T00:00:00'; $pae = '0001-01-01T00:00:00' - } else { - $field = "$($level.field)" - $gt = if ($level.groupType) { "$($level.groupType)" } else { 'Items' } - $pat = if ($level.periodAdditionType) { "$($level.periodAdditionType)" } else { 'None' } - $pab = if ($level.periodAdditionBegin) { "$($level.periodAdditionBegin)" } else { '0001-01-01T00:00:00' } - $pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } - } - X "$indent" - X "$indent`t$(Esc-Xml $field)" - X "$indent`t$(Esc-Xml $gt)" - X "$indent`t$(Esc-Xml $pat)" - # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. - $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } - $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } - X "$indent`t$(Esc-Xml $pab)" - X "$indent`t$(Esc-Xml $pae)" - X "$indent" -} - -function Emit-ListGroupingLevels { - param($levels, [int]$i, [string]$indent) - X "$indent" - X "$indent`t" - Emit-GroupItemField $levels[$i] "$indent`t`t" - X "$indent`t" - if ($i -lt $levels.Count - 1) { Emit-ListGroupingLevels $levels ($i + 1) "$indent`t" } - X "$indent" -} - -function Emit-ListGrouping { - param($grouping, [string]$indent) - $levels = Parse-ListGrouping $grouping - if ($levels.Count -eq 0) { return } - Emit-ListGroupingLevels $levels 0 $indent -} - -# === Вычисляемые поля DataSet динамического списка () === -# Зеркало skd: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder" -# или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression (dcscommon ns). -$script:calcRestrictMap = @{ 'noField'='field'; 'noFilter'='condition'; 'noCondition'='condition'; 'noGroup'='group'; 'noOrder'='order' } -$script:dcsCommonNs = 'http://v8.1c.ru/8.1/data-composition-system/common' - -function Parse-CalcShorthand { - param([string]$s) - $restrict = @() - foreach ($m in [regex]::Matches($s, '#(noField|noFilter|noCondition|noGroup|noOrder)\b')) { $restrict += $m.Groups[1].Value } - $s = [regex]::Replace($s, '\s*#(noField|noFilter|noCondition|noGroup|noOrder)\b', '') - $eq = $s.IndexOf('=') - if ($eq -gt 0) { $lhs = $s.Substring(0, $eq); $rhs = $s.Substring($eq + 1).Trim() } else { $lhs = $s; $rhs = '' } - $title = '' - if ($lhs -match '\[([^\]]+)\]') { $title = $Matches[1]; $lhs = $lhs -replace '\s*\[[^\]]+\]', '' } - $lhs = $lhs.Trim() - $type = ''; $dataPath = $lhs - if ($lhs.Contains(':')) { $parts = $lhs -split ':', 2; $dataPath = $parts[0].Trim(); $type = Resolve-TypeStr ($parts[1].Trim()) } - return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict } -} - -function Emit-CalcFields { - param($calcFields, [string]$indent) - if (-not $calcFields) { return } - foreach ($cf in $calcFields) { - $pres = $null; $orderExpr = $null; $restrict = @() - if ($cf -is [string]) { - $p = Parse-CalcShorthand $cf - $dataPath = "$($p.dataPath)"; $expression = "$($p.expression)"; $title = $p.title; $typeStr = "$($p.type)" - foreach ($r in $p.restrict) { if ($script:calcRestrictMap[$r]) { $restrict += $script:calcRestrictMap[$r] } } - } else { - $dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } elseif ($cf.field) { "$($cf.field)" } else { "$($cf.name)" } - $expression = "$($cf.expression)" - $title = $cf.title - $typeStr = if ($cf.valueType) { "$($cf.valueType)" } elseif ($cf.type) { "$($cf.type)" } else { '' } - $ur = if ($cf.useRestriction) { $cf.useRestriction } elseif ($cf.restrict) { $cf.restrict } else { $null } - if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) { - foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $restrict += $k } } - } elseif ($ur -is [string]) { - foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $restrict += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } } - } elseif ($ur) { - foreach ($r in $ur) { $rr = "$r"; $restrict += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) } - } - $pres = $cf.presentationExpression - $orderExpr = $cf.orderExpression - } - $ci = "$indent`t" - X "$indent" - X "$ci$(Esc-Xml $dataPath)" - X "$ci$(Esc-Xml $expression)" - if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' } - if ($restrict.Count -gt 0) { - X "$ci" - foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`ttrue" } } - X "$ci" - } - if ($pres) { X "$ci$(Esc-Xml "$pres")" } - if ($orderExpr) { - $oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) } - foreach ($oe in $oeList) { - if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' } - else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } } - X "$ci" - X "$ci`t$(Esc-Xml $exprV)" - X "$ci`t$oType" - X "$ci`t$auto" - X "$ci" - } - } - if ($typeStr) { Emit-DLValueType -typeStr $typeStr -indent $ci } - X "$indent" - } -} - -# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction). -# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField #noFilter #noGroup #noOrder" | массив. -function Get-RestrictList { - param($ur) - $out = @() - if (-not $ur) { return ,$out } - if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) { - foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $out += $k } } - } elseif ($ur -is [string]) { - foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $out += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } } - } else { - foreach ($r in $ur) { $rr = "$r"; $out += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) } - } - return ,$out -} - -function Emit-RestrictBlock { - param([string]$tag, $ur, [string]$indent) - $r = Get-RestrictList $ur - if ($r.Count -eq 0) { return } - X "$indent" - foreach ($k in @('field','condition','group','order')) { if ($r -contains $k) { X "$indent`ttrue" } } - X "$indent" -} - -# --- 5. Type emitter --- - -$script:formTypeSynonyms = New-Object System.Collections.Hashtable -$script:formTypeSynonyms["строка"] = "string" -$script:formTypeSynonyms["число"] = "decimal" -$script:formTypeSynonyms["булево"] = "boolean" -$script:formTypeSynonyms["дата"] = "date" -$script:formTypeSynonyms["датавремя"]= "dateTime" -$script:formTypeSynonyms["number"] = "decimal" -$script:formTypeSynonyms["bool"] = "boolean" -$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" -$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" -$script:formTypeSynonyms["документссылка"] = "DocumentRef" -$script:formTypeSynonyms["документобъект"] = "DocumentObject" -$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" -$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:formTypeSynonyms["задачассылка"] = "TaskRef" -$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" -$script:formTypeSynonyms["характеристика"] = "Characteristic" -$script:formTypeSynonyms["любаяссылка"] = "AnyRef" -$script:formTypeSynonyms["любаяссылкаиб"] = "AnyIBRef" -# Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: (эмитим verbatim) -$script:formTypeSynonyms["standardperiod"] = "v8:StandardPeriod" -$script:formTypeSynonyms["стандартныйпериод"] = "v8:StandardPeriod" -$script:formTypeSynonyms["standardbeginningdate"] = "v8:StandardBeginningDate" -$script:formTypeSynonyms["стандартнаядатаначала"] = "v8:StandardBeginningDate" -$script:formTypeSynonyms["uuid"] = "v8:UUID" -$script:formTypeSynonyms["уникальныйидентификатор"] = "v8:UUID" -$script:formTypeSynonyms["списокзначений"] = "ValueList" - -# Known invalid types (runtime/UI types that don't exist in XDTO schema) -$script:knownInvalidTypes = @{ - "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" - "FormDataCollection" = "Runtime type. Use ValueTable" - "FormDataTree" = "Runtime type. Use ValueTree" - "FormDataTreeItem" = "Runtime type, not valid in XML" - "FormDataCollectionItem"= "Runtime type, not valid in XML" - "FormGroup" = "UI element type, not a data type" - "FormField" = "UI element type, not a data type" - "FormButton" = "UI element type, not a data type" - "FormDecoration" = "UI element type, not a data type" - "FormTable" = "UI element type, not a data type" -} - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $base = $Matches[1].Trim(); $params = $Matches[2] - $r = $script:formTypeSynonyms[$base.ToLower()] - if ($r) { return "$r($params)" } - return $typeStr - } - if ($typeStr.Contains('.')) { - $i = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) - $r = $script:formTypeSynonyms[$prefix.ToLower()] - if ($r) { return "$r$suffix" } - return $typeStr - } - $r = $script:formTypeSynonyms[$typeStr.ToLower()] - if ($r) { return $r } - return $typeStr -} - -function Emit-Type { - # $tag/$tagAttrs — обёртка (по умолчанию ); для уточнения типа значений ValueList - # вызывается с tag="Settings", tagAttrs=' xsi:type="v8:TypeDescription"'. - param($typeStr, [string]$indent, [string]$tag = "Type", [string]$tagAttrs = "") - - if (-not $typeStr) { - X "$indent<$tag$tagAttrs/>" - return - } - - $typeString = "$typeStr" - - # Composite type: "Type1 | Type2" or "Type1 + Type2" - $parts = $typeString -split '\s*[|+]\s*' - - X "$indent<$tag$tagAttrs>" - foreach ($part in $parts) { - $part = $part.Trim() - Emit-SingleType -typeStr $part -indent "$indent`t" - } - X "$indent" -} - -function Emit-SingleType { - param([string]$typeStr, [string]$indent) - - $typeStr = Resolve-TypeStr $typeStr - - # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так - # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID - # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. - if ($typeStr -match '^typeid:([0-9a-fA-F-]{36})$') { - X "$indent$($Matches[1])" - return - } - - # boolean - if ($typeStr -eq "boolean") { - X "$indentxs:boolean" - return - } - - # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) - if ($typeStr -match '^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - $al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' } - X "$indentxs:string" - X "$indent" - X "$indent`t$len" - X "$indent`t$al" - X "$indent" - return - } - - # decimal(D,F) or decimal(D,F,nonneg) - if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1] - $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - X "$indentxs:decimal" - X "$indent" - X "$indent`t$digits" - X "$indent`t$fraction" - X "$indent`t$sign" - X "$indent" - return - } - - # date / dateTime / time - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { - "date" { "Date" } - "dateTime" { "DateTime" } - "time" { "Time" } - } - X "$indentxs:dateTime" - X "$indent" - X "$indent`t$fractions" - X "$indent" - return - } - - # ValueTable, ValueTree, ValueList, etc. - $v8Types = @{ - "ValueTable" = "v8:ValueTable" - "ValueTree" = "v8:ValueTree" - "ValueList" = "v8:ValueListType" - "TypeDescription" = "v8:TypeDescription" - "Universal" = "v8:Universal" - "FixedArray" = "v8:FixedArray" - "FixedStructure" = "v8:FixedStructure" - } - if ($v8Types.ContainsKey($typeStr)) { - X "$indent$($v8Types[$typeStr])" - return - } - - # UI types - $uiTypes = @{ - "FormattedString" = "v8ui:FormattedString" - "Picture" = "v8ui:Picture" - "Color" = "v8ui:Color" - "Font" = "v8ui:Font" - } - if ($uiTypes.ContainsKey($typeStr)) { - X "$indent$($uiTypes[$typeStr])" - return - } - - # DCS types - if ($typeStr -match '^DataComposition') { - $dcsMap = @{ - "DataCompositionSettings" = "dcsset:DataCompositionSettings" - "DataCompositionSchema" = "dcssch:DataCompositionSchema" - "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" - } - if ($dcsMap.ContainsKey($typeStr)) { - X "$indent$($dcsMap[$typeStr])" - return - } - } - - # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. - # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. (Дотированные формы - # ConstantsSet.X / ReportObject.X ловит общий cfg:-regex ниже.) - if ($typeStr -in @("DynamicList","ConstantsSet","ReportObject")) { - X "$indentcfg:$typeStr" - return - } - - # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) - # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. - if ($typeStr -match '^(DefinedType|Characteristic)\.') { - X "$indentcfg:$typeStr" - return - } - if ($typeStr -match '^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$') { - X "$indentcfg:$typeStr" - return - } - - # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) - if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { - X "$indentcfg:$typeStr" - return - } - - # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). - # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. - # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, - # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. - $specialTypeNs = @{ - "mxl:SpreadsheetDocument" = "http://v8.1c.ru/8.2/data/spreadsheet" - "fd:FormattedDocument" = "http://v8.1c.ru/8.2/data/formatted-document" - "d5p1:TextDocument" = "http://v8.1c.ru/8.1/data/txtedt" - "d5p1:Chart" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:GanttChart" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:Dendrogram" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:FlowchartContextType" = "http://v8.1c.ru/8.2/data/graphscheme" - "d5p1:DataAnalysisTimeIntervalUnitType" = "http://v8.1c.ru/8.2/data/data-analysis" - "d5p1:GeographicalSchema" = "http://v8.1c.ru/8.2/data/geo" - "pdfdoc:PDFDocument" = "http://v8.1c.ru/8.3/data/pdf" - "pl:Planner" = "http://v8.1c.ru/8.3/data/planner" - } - if ($specialTypeNs.ContainsKey($typeStr)) { - $pref = $typeStr.Substring(0, $typeStr.IndexOf(':')) - X "$indent$typeStr" - return - } - - # Fallback with validation - if ($script:knownInvalidTypes.ContainsKey($typeStr)) { - throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" - } - # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod). - if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') { - X "$indent$typeStr" - } elseif ($typeStr.Contains('.')) { - X "$indentcfg:$typeStr" - } else { - Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" - X "$indent$typeStr" - } -} - -# --- 6. Event handler name generator --- - -$script:eventSuffixMap = @{ - "OnChange" = "ПриИзменении" - "StartChoice" = "НачалоВыбора" - "ChoiceProcessing" = "ОбработкаВыбора" - "AutoComplete" = "АвтоПодбор" - "Clearing" = "Очистка" - "Opening" = "Открытие" - "Click" = "Нажатие" - "OnActivateRow" = "ПриАктивизацииСтроки" - "BeforeAddRow" = "ПередНачаломДобавления" - "BeforeDeleteRow" = "ПередУдалением" - "BeforeRowChange" = "ПередНачаломИзменения" - "OnStartEdit" = "ПриНачалеРедактирования" - "OnEndEdit" = "ПриОкончанииРедактирования" - "Selection" = "ВыборСтроки" - "OnCurrentPageChange" = "ПриСменеСтраницы" - "TextEditEnd" = "ОкончаниеВводаТекста" - "URLProcessing" = "ОбработкаНавигационнойСсылки" - "DragStart" = "НачалоПеретаскивания" - "Drag" = "Перетаскивание" - "DragCheck" = "ПроверкаПеретаскивания" - "Drop" = "Помещение" - "AfterDeleteRow" = "ПослеУдаления" -} - -function Get-HandlerName { - param([string]$elementName, [string]$eventName) - $suffix = $script:eventSuffixMap[$eventName] - if ($suffix) { - return "$elementName$suffix" - } - return "$elementName$eventName" -} - -# --- 7. Element emitters --- - -function Get-ElementName { - param($el, [string]$typeKey) - if ($el.name) { return "$($el.name)" } - return "$($el.$typeKey)" -} - -$script:knownEvents = @{ - "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") - "check" = @("OnChange") - "radio" = @("OnChange") - "label" = @("Click","URLProcessing") - "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") - "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") - "pages" = @("OnCurrentPageChange") - "page" = @("OnCurrentPageChange") - "button" = @("Click") - "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") - "calendar" = @("OnChange","OnActivate") - "picture" = @("Click") - "cmdBar" = @() - "popup" = @() - "group" = @() -} -$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") - -# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. -# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции). -# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён). -function Get-EventPairs { - param($el, [string]$elementName) - $pairs = New-Object System.Collections.ArrayList - if ($el.events) { - foreach ($p in $el.events.PSObject.Properties) { - $h = "$($p.Value)" - if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name } - [void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h }) - } - } elseif ($el.on) { - foreach ($evt in $el.on) { - $evtName = "$evt" - $h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName } - [void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h }) - } - } - return $pairs -} - -# Проверить, подключено ли событие к элементу (в любом из форматов). -function Test-ElementEvent { - param($el, [string]$eventName) - if ($el.events) { - foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } } - } - if ($el.on -contains $eventName) { return $true } - return $false -} - -function Emit-Events { - param($el, [string]$elementName, [string]$indent, [string]$typeKey) - - $pairs = Get-EventPairs -el $el -elementName $elementName - if ($pairs.Count -eq 0) { return } - - # Validate event names - if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { - $allowed = $script:knownEvents[$typeKey] - foreach ($pr in $pairs) { - if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") { - Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')" - } - } - } - - X "$indent" - foreach ($pr in $pairs) { - X "$indent`t$($pr.handler)" - } - X "$indent" -} - -# ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/флаги/hyperlink) -# вместо/вместе с текстом. Признак структурированной формы: объект с любым НЕ-текстовым ключом -# ({text,formatted} и языковые ключи {ru,en} → обычная текст-форма). -$script:companionStructKeys = @( - 'width','autoMaxWidth','maxWidth','height','autoMaxHeight','maxHeight','verticalAlign','titleHeight', - 'horizontalStretch','verticalStretch','horizontalAlign','groupHorizontalAlign','groupVerticalAlign', - 'visible','hidden','enabled','disabled','hyperlink','events','tooltip', - 'textColor','backColor','borderColor','font','border','цветтекста','цветфона','цветрамки','шрифт','рамка' -) -function Test-CompanionStructured { - param($content) - if (-not (($content -is [System.Collections.IDictionary]) -or ($content -is [System.Management.Automation.PSCustomObject]))) { return $false } - foreach ($k in $script:companionStructKeys) { - $present = if ($content -is [System.Collections.IDictionary]) { $content.Contains($k) } else { [bool]$content.PSObject.Properties[$k] } - if ($present) { return $true } - } - return $false -} - -function Emit-CompanionTitle { - param($content, [string]$indent) - $r = Resolve-MLFormatted $content - $fmt = if ($r.formatted) { 'true' } else { 'false' } - X "$indent" - Emit-MLItems -val $r.text -indent "$indent`t" - X "$indent" -} - -# DisplayImportance — атрибут открывающего тега элемента (адаптивная важность: VeryHigh/High/Usual/Low/VeryLow). -# Возвращает ` DisplayImportance="X"` или "" (для companion-эмиттеров без $el → "" молча). -function DI-Attr { - param($el) - if ($null -ne $el -and $el.displayImportance) { return " DisplayImportance=`"$(Esc-Xml "$($el.displayImportance)")`"" } - return "" -} - -function Emit-Companion { - param([string]$tag, [string]$name, [string]$indent, $content = $null) - $id = New-Id - $hasContent = $null -ne $content -and -not ($content -is [string] -and "$content" -eq '') - if (-not $hasContent) { - X "$indent<$tag name=`"$name`" id=`"$id`"/>" - return - } - $inner = "$indent`t" - # DI-Attr берём от СОБСТВЕННОГО объекта компаньона ($content), НЕ от ambient $el родителя - # (PowerShell dynamic scope — иначе companion наследует DisplayImportance владельца: баг). - X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $content)>" - if (Test-CompanionStructured $content) { - # структурированная форма (own-content). Порядок как у платформы: own-content (флаги/hyperlink/ - # layout/оформление) ПЕРЕД Title (в корпусе layout-first 582 vs 10). - $txtPresent = if ($content -is [System.Collections.IDictionary]) { $content.Contains('text') } else { [bool]$content.PSObject.Properties['text'] } - Emit-CommonFlags -el $content -indent $inner - if ($content.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $content -indent $inner - Emit-Appearance -el $content -indent $inner -profile 'decoration' - if ($txtPresent) { Emit-CompanionTitle -content $content -indent $inner } - # ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration) - if ($content.tooltip) { Emit-MLText -tag "ToolTip" -text $content.tooltip -indent $inner } - # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) - Emit-Events -el $content -elementName $name -indent $inner -typeKey 'label' - } else { - Emit-CompanionTitle -content $content -indent $inner - } - X "$indent" -} - -# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] } -# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion). -# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar). -function Emit-CompanionPanel { - param([string]$tag, [string]$name, [string]$indent, $panel) - $id = New-Id - $autofill = $null - $children = $null - $halign = $null - if ($panel -is [array]) { - $children = $panel - } elseif ($null -ne $panel) { - if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill } - if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" } - $children = $panel.children - } - $hasChildren = $children -and @($children).Count -gt 0 - # Платформа пишет только при false; true = дефолт (тег опускается). - $emitAfFalse = ($autofill -eq $false) - if (-not $emitAfFalse -and -not $hasChildren -and -not $halign) { - X "$indent<$tag name=`"$name`" id=`"$id`"/>" - return - } - X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $panel)>" - if ($halign) { X "$indent`t$halign" } - if ($emitAfFalse) { X "$indent`tfalse" } - if ($hasChildren) { - X "$indent`t" - foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true } - X "$indent`t" - } - X "$indent" -} - -# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type. -$script:additionTypeMap = [ordered]@{ - 'searchString' = @{ Tag = 'SearchStringAddition'; Type = 'SearchStringRepresentation'; Suffix = 'СтрокаПоиска' } - 'viewStatus' = @{ Tag = 'ViewStatusAddition'; Type = 'ViewStatusRepresentation'; Suffix = 'СостояниеПросмотра' } - 'searchControl' = @{ Tag = 'SearchControlAddition'; Type = 'SearchControl'; Suffix = 'УправлениеПоиском' } -} -# Синонимы типа дополнения (для override-карты additions и резолва тип-ключа). -$script:additionKeySynonyms = @{ - 'searchString' = @('SearchStringAddition','SearchStringRepresentation','строкаПоиска','отображениеСтрокиПоиска') - 'viewStatus' = @('ViewStatusAddition','ViewStatusRepresentation','состояниеПросмотра') - 'searchControl' = @('SearchControlAddition','SearchControl','управлениеПоиском') -} - -# HorizontalLocation: auto (дефолт, тег опускаем) / left / right; forgiving + рус.синонимы. -function Get-HLocation { - param($el) - $v = if ($el -and $el.PSObject.Properties['horizontalLocation']) { $el.horizontalLocation } else { $null } - if (-not $v) { return $null } - switch -Regex ("$v".ToLower()) { - '^(auto|авто)$' { return $null } # дефолт — не эмитим - '^(left|слева|лево)$' { return 'Left' } - '^(right|справа|право)$' { return 'Right' } - '^(center|центр|по центру)$' { return 'Center' } - default { return "$v" } - } -} - -# Тело дополнения: AdditionSource + свойства (как у поля) + companions. $props может быть $null -# (стандартное дополнение без отклонений). Порядок 1С-толерантен (diff порядок-независим). -function Emit-AdditionBody { - param($props, [string]$source, [string]$srcType, [string]$addName, [string]$indent) - $inner = "$indent`t" - X "$inner" - X "$inner`t$source" - X "$inner`t$srcType" - X "$inner" - if ($props) { - if ($props.PSObject.Properties['title'] -and $props.title) { Emit-MLText -tag "Title" -text $props.title -indent $inner } - Emit-CommonFlags -el $props -indent $inner - if ($props.tooltip) { Emit-MLText -tag "ToolTip" -text $props.tooltip -indent $inner } - if ($props.tooltipRepresentation) { X "$inner$($props.tooltipRepresentation)" } - $hl = Get-HLocation $props; if ($hl) { X "$inner$hl" } - Emit-Layout -el $props -indent $inner - Emit-Appearance -el $props -indent $inner -profile 'field' - } - Emit-Companion -tag "ContextMenu" -name "${addName}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${addName}РасширеннаяПодсказка" -indent $inner -} - -# Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. -function Emit-Addition { - param($el, [string]$name, [int]$id, [string]$typeKey, [string]$indent) - $map = $script:additionTypeMap[$typeKey] - $source = if ($el.source) { "$($el.source)" } elseif ($script:currentTableName) { $script:currentTableName } else { '' } - X "$indent<$($map.Tag) name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - Emit-AdditionBody -props $el -source $source -srcType $map.Type -addName $name -indent $indent - X "$indent" -} - -# Стандартное табличное дополнение (авто-генерация на уровне таблицы). $override — объект отклонений -# из per-table карты additions (или $null = чистый дефолт). -function Emit-TableAddition { - param([string]$typeKey, [string]$tableName, [string]$indent, $override = $null) - $map = $script:additionTypeMap[$typeKey] - $addName = "$tableName$($map.Suffix)" - $id = New-Id - X "$indent<$($map.Tag) name=`"$addName`" id=`"$id`">" - Emit-AdditionBody -props $override -source $tableName -srcType $map.Type -addName $addName -indent $indent - X "$indent" -} - -# Прочитать override-объект для типа дополнения из per-table карты additions (с синонимами). -function Get-AdditionOverride { - param($additions, [string]$typeKey) - if ($null -eq $additions) { return $null } - foreach ($k in @($typeKey) + $script:additionKeySynonyms[$typeKey]) { - $p = $additions.PSObject.Properties[$k] - if ($p) { return $p.Value } - } - return $null -} - -function Emit-Element { - param($el, [string]$indent, [bool]$inCmdBar = $false) - - # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. - Normalize-PanelSynonyms $el - - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). - # Maps any synonym to canonical short DSL key. - # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя); - # объект/массив уже отнесён к панель-свойству выше. - $strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель') - $synonyms = @{ - "commandBar" = "cmdBar" - "autoCommandBar" = "autoCmdBar" - "КоманднаяПанель" = "cmdBar" - "InputField" = "input" - "ПолеВвода" = "input" - "CheckBoxField" = "check" - "ПолеФлажка" = "check" - "RadioButtonField" = "radio" - "ПолеПереключателя" = "radio" - "radioButton" = "radio" - "PictureField" = "picField" - "ПолеКартинки" = "picField" - "LabelField" = "labelField" - "ПолеНадписи" = "labelField" - "CalendarField" = "calendar" - "ПолеКалендаря" = "calendar" - "LabelDecoration" = "label" - "Надпись" = "label" - "PictureDecoration" = "picture" - "Картинка" = "picture" - "UsualGroup" = "group" - "Группа" = "group" - "ОбычнаяГруппа" = "group" - "ColumnGroup" = "columnGroup" - "ГруппаКолонок" = "columnGroup" - "Pages" = "pages" - "ГруппаСтраниц" = "pages" - "Page" = "page" - "Страница" = "page" - "Table" = "table" - "Таблица" = "table" - "Button" = "button" - "Кнопка" = "button" - "Popup" = "popup" - "ВсплывающееМеню" = "popup" - # Дополнения командной панели таблицы (тип-как-ключ) — forgiving: XML-тег/Type/рус.имя → канон - "SearchStringAddition" = "searchString" - "SearchStringRepresentation" = "searchString" - "строкаПоиска" = "searchString" - "отображениеСтрокиПоиска" = "searchString" - "Отображение строки поиска" = "searchString" - "ViewStatusAddition" = "viewStatus" - "ViewStatusRepresentation" = "viewStatus" - "состояниеПросмотра" = "viewStatus" - "Состояние просмотра" = "viewStatus" - "SearchControlAddition" = "searchControl" - "SearchControl" = "searchControl" - "управлениеПоиском" = "searchControl" - "Управление поиском" = "searchControl" - # Спец-поля (документ/датчик) — XML-имя/рус. → канон - "SpreadSheetDocumentField" = "spreadsheet" - "ПолеТабличногоДокумента" = "spreadsheet" - "HTMLDocumentField" = "html" - "ПолеHTMLДокумента" = "html" - "TextDocumentField" = "textDoc" - "ПолеТекстовогоДокумента" = "textDoc" - "FormattedDocumentField" = "formattedDoc" - "ПолеФорматированногоДокумента" = "formattedDoc" - "ProgressBarField" = "progressBar" - "ПолеИндикатора" = "progressBar" - "TrackBarField" = "trackBar" - "ПолеПолосыРегулирования" = "trackBar" - "ChartField" = "chart" - "ПолеДиаграммы" = "chart" - "GanttChartField" = "ganttChart" - "ПолеДиаграммыГанта" = "ganttChart" - "GraphicalSchemaField" = "graphicalSchema" - "ПолеГрафическойСхемы" = "graphicalSchema" - "PlannerField" = "planner" - "ПолеПланировщика" = "planner" - "PeriodField" = "periodField" - "ПолеПериода" = "periodField" - "DendrogramField" = "dendrogram" - "ПолеДендрограммы" = "dendrogram" - } - foreach ($pair in $synonyms.GetEnumerator()) { - if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { - if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue } - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - - # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. - # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. - foreach ($pn in @($el.PSObject.Properties.Name)) { - $norm = ($pn -replace '\s','').ToLower() - $canon = $script:propSynonyms[$norm] - if ($canon -and $pn -ne $canon) { - if ($null -eq $el.PSObject.Properties[$canon]) { - $val = $el.($pn) - $el | Add-Member -NotePropertyName $canon -NotePropertyValue $val -Force - } - $el.PSObject.Properties.Remove($pn) | Out-Null - } - } - - # Determine element type from key - $typeKey = $null - $xmlTag = $null - - # picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка - # у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. - # pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей - # (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. - foreach ($key in @("columnGroup","buttonGroup","pages","page","group","input","check","radio","label","labelField","table","button","calendar","cmdBar","popup","searchString","viewStatus","searchControl","picField","picture","spreadsheet","html","textDoc","formattedDoc","progressBar","trackBar","chart","ganttChart","graphicalSchema","planner","periodField","dendrogram")) { - if ($el.$key -ne $null) { - $typeKey = $key - break - } - } - - if (-not $typeKey) { - Write-Warning "Unknown element type, skipping" - return - } - - # Validate known keys — warn about typos and unknown properties - $knownKeys = @{ - # type keys - "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 - "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 - # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры - "spreadsheet"=1;"html"=1;"textDoc"=1;"formattedDoc"=1;"progressBar"=1;"trackBar"=1 - "chart"=1;"ganttChart"=1;"graphicalSchema"=1;"planner"=1;"periodField"=1;"dendrogram"=1;"ganttTable"=1 - "showPercent"=1;"largeStep"=1;"markingStep"=1;"step"=1 - "horizontalScrollBar"=1;"viewScalingMode"=1;"output"=1;"selectionShowMode"=1;"protection"=1 - "edit"=1;"showGrid"=1;"showGroups"=1;"showHeaders"=1;"showRowAndColumnNames"=1;"showCellNames"=1 - "pointerType"=1;"drawingSelectionShowMode"=1;"warningOnEditRepresentation"=1;"markingAppearance"=1 - # report-form контекст (generic-скаляры элементов) - "horizontalSpacing"=1;"representationInContextMenu"=1;"settingsNamedItemDetailedRepresentation"=1 - # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель - "itemHeight"=1;"dropListWidth"=1;"choiceButtonPicture"=1;"transparentPixel"=1 - # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы - "titleDataPath"=1;"extendedEdit"=1;"maxRowsCount"=1;"autoMaxRowsCount"=1;"heightControlVariant"=1 - "warningOnEdit"=1;"nonselectedPictureText"=1;"editTextUpdate"=1;"footerText"=1 - # columnGroup-specific - "showInHeader"=1 - # radio-specific - "radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1 - # naming & binding - "name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1 - # companion-панели (свойства): командная панель + контекстное меню - "commandBar"=1;"contextMenu"=1 - # источник команд группы/панели (ButtonGroup/CommandBar) - "commandSource"=1 - # visibility & state - "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 - # events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости) - "events"=1;"on"=1;"handlers"=1 - # layout - "titleLocation"=1;"representation"=1;"width"=1;"height"=1 - "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 - "maxWidth"=1;"maxHeight"=1 - "groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1 - # input-specific - "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 - "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 - "textEdit"=1 - "wrap"=1;"openButton"=1;"listChoiceMode"=1;"showInFooter"=1 - "extendedEditMultipleValues"=1;"chooseType"=1;"autoCellHeight"=1 - "choiceButtonRepresentation"=1;"footerHorizontalAlign"=1;"headerHorizontalAlign"=1 - "headerDataPath"=1;"headerFormat"=1;"currentRowUse"=1 - "format"=1;"editFormat"=1;"choiceParameters"=1;"choiceParameterLinks"=1;"typeLink"=1 - # label/hyperlink - "hyperlink"=1;"formatted"=1 - # group-specific - "collapsedTitle"=1;"showTitle"=1;"united"=1;"collapsed"=1;"behavior"=1 - # hierarchy - "children"=1;"columns"=1 - # table-specific - "changeRowSet"=1;"changeRowOrder"=1;"autoInsertNewRow"=1;"rowFilter"=1;"header"=1;"footer"=1 - "commandBarLocation"=1;"searchStringLocation"=1;"viewStatusLocation"=1;"searchControlLocation"=1 - "excludedCommands"=1 - "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 - "rowPictureDataPath"=1;"tableAutofill"=1;"heightInTableRows"=1 - "multipleChoice"=1;"searchOnInput"=1;"shortcut"=1 - "rowSelectionMode"=1;"verticalLines"=1;"horizontalLines"=1 - # dynamic-list table block - "defaultItem"=1;"useAlternationRowColor"=1;"fileDragMode"=1;"autoRefresh"=1 - "autoRefreshPeriod"=1;"choiceFoldersAndItems"=1;"restoreCurrentRow"=1;"showRoot"=1 - "allowRootChoice"=1;"updateOnDataChange"=1;"allowGettingCurrentRowURL"=1 - "userSettingsGroup"=1;"rowsPicture"=1 - # calendar-specific - "selectionMode"=1;"showCurrentDate"=1;"widthInMonths"=1;"heightInMonths"=1;"showMonthsPanel"=1 - # pages-specific - "pagesRepresentation"=1 - # button-specific - "type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"parameter"=1;"defaultButton"=1;"locationInCommandBar"=1;"displayImportance"=1 - # picture/decoration - "src"=1;"valuesPicture"=1;"loadTransparent"=1;"headerPicture"=1;"footerPicture"=1 - # cmdBar-specific - "autofill"=1 - # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице - "autoCmdBar"=1 - # дополнения командной панели таблицы (тип-ключи + свойства) - "searchString"=1;"viewStatus"=1;"searchControl"=1;"source"=1;"horizontalLocation"=1;"additions"=1 - # generic-скаляры (pass-through) + точечные - "verticalAlign"=1;"throughAlign"=1;"enableContentChange"=1;"pictureSize"=1;"titleHeight"=1 - "childItemsWidth"=1;"showLeftMargin"=1;"cellHyperlink"=1;"viewMode"=1;"verticalScrollBar"=1 - "rowInputMode"=1;"mask"=1;"createButton"=1;"fixingInTable"=1;"verticalSpacing"=1 - # InputField choice-скаляры - "choiceListButton"=1;"quickChoice"=1;"autoChoiceIncomplete"=1 - "choiceForm"=1;"choiceHistoryOnInput"=1;"footerDataPath"=1;"minValue"=1;"maxValue"=1 - # Button — пометка toggle-кнопки (ключ 'checked', не 'check' — во избежание конфликта с типом) - "checked"=1 - } - # Оформление (цвета/шрифты/граница) — авто-регистрация из самих структур, чтобы allowlist - # не дрейфовал при добавлении новых ключей/синонимов. Канонические + forgiving-синонимы. - foreach ($k in $script:appearanceSpec.Keys) { $knownKeys[$k] = 1 } - foreach ($k in $script:appearanceSynonyms.Keys) { $knownKeys[$k] = 1 } - foreach ($k in $script:propSynonyms.Keys) { $knownKeys[$k] = 1 } - foreach ($p in $el.PSObject.Properties) { - if ($p.Name -like '_*') { continue } # внутренние маркеры (напр. _dynList) - if (-not $knownKeys.ContainsKey($p.Name)) { - Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." - } - } - - $name = Get-ElementName -el $el -typeKey $typeKey - Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element' - $id = New-Id - - switch ($typeKey) { - "group" { Emit-Group -el $el -name $name -id $id -indent $indent } - "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } - "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } - "input" { Emit-Input -el $el -name $name -id $id -indent $indent } - "check" { Emit-Check -el $el -name $name -id $id -indent $indent } - "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } - "label" { Emit-Label -el $el -name $name -id $id -indent $indent } - "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } - "table" { Emit-Table -el $el -name $name -id $id -indent $indent } - "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } - "page" { Emit-Page -el $el -name $name -id $id -indent $indent } - "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } - "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } - "searchString" { Emit-Addition -el $el -name $name -typeKey "searchString" -id $id -indent $indent } - "viewStatus" { Emit-Addition -el $el -name $name -typeKey "viewStatus" -id $id -indent $indent } - "searchControl" { Emit-Addition -el $el -name $name -typeKey "searchControl" -id $id -indent $indent } - "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } - "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } - "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } - "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } - "spreadsheet" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "SpreadSheetDocumentField" -typeKey "spreadsheet" } - "html" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "HTMLDocumentField" -typeKey "html" } - "textDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TextDocumentField" -typeKey "textDoc" } - "formattedDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "FormattedDocumentField" -typeKey "formattedDoc" } - "progressBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ProgressBarField" -typeKey "progressBar" } - "trackBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TrackBarField" -typeKey "trackBar" } - "chart" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ChartField" -typeKey "chart" } - "graphicalSchema" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "GraphicalSchemaField" -typeKey "graphicalSchema" } - "planner" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PlannerField" -typeKey "planner" } - "periodField" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PeriodField" -typeKey "periodField" } - "dendrogram" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "DendrogramField" -typeKey "dendrogram" } - "ganttChart" { Emit-GanttChart -el $el -name $name -id $id -indent $indent } - } -} - -# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). -# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). -# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. -# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. -function Emit-XrFlag { - param([string]$tag, $val, [string]$indent) - if ($null -eq $val) { return } - if ($val -is [bool]) { - X "$indent<$tag>" - X "$indent`t$(if ($val){'true'}else{'false'})" - X "$indent" - return - } - # объектная форма { common, roles } - $common = if ($null -ne $val.common) { [bool]$val.common } else { $false } - X "$indent<$tag>" - X "$indent`t$(if ($common){'true'}else{'false'})" - if ($val.roles) { - foreach ($r in $val.roles.PSObject.Properties) { - # Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". - # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. - $rname = "$($r.Name)" -replace '^(Role|Роль)\.', '' - if ($rname -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $rname = "Role.$rname" } - $rval = if ([bool]$r.Value) { 'true' } else { 'false' } - X "$indent`t$rval" - } - } - X "$indent" -} - -function Emit-CommonFlags { - param($el, [string]$indent) - if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } - if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent } - if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } - if ($el.readOnly -eq $true) { X "$indenttrue" } -} - -# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с -# историческим выводом input/label, чтобы не сдвигать существующие снапшоты. -# -skipHeight: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). -# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. -# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. -function Emit-CommonElementProps { - param($el, [string]$indent) - if ($el.defaultItem -eq $true) { X "$indenttrue" } - if ($el.PSObject.Properties['skipOnInput'] -and $null -ne $el.skipOnInput) { - $siv = if ($el.skipOnInput -eq $true) { 'true' } else { 'false' } - X "$indent$siv" - } - # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - if ($null -ne $el.enableStartDrag) { X "$indent$(if ($el.enableStartDrag){'true'}else{'false'})" } - if ($el.fileDragMode) { X "$indent$($el.fileDragMode)" } - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - foreach ($p in @(@('showInHeader','ShowInHeader'), @('showInFooter','ShowInFooter'), @('autoCellHeight','AutoCellHeight'))) { - if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } - } - # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) - if ($el.headerDataPath) { X "$indent$(Esc-Xml "$($el.headerDataPath)")" } - if ($el.footerHorizontalAlign) { X "$indent$($el.footerHorizontalAlign)" } - if ($el.headerHorizontalAlign) { X "$indent$($el.headerHorizontalAlign)" } - # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) - if ($el.headerFormat) { Emit-MLText -tag "HeaderFormat" -text $el.headerFormat -indent $indent } -} - -# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). -# Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). -# Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. -# src с префиксом "abs:" → встроенная картинка ; иначе именованная/стилевая . -function Emit-PictureRef { - param($val, [string]$picTag, [string]$indent) - if (-not $val) { return } - $src = $null; $lt = $false; $tpx = $null - if ($val -is [string]) { $src = $val } - else { $src = $val.src; if ($val.loadTransparent -eq $true) { $lt = $true }; $tpx = $val.transparentPixel } - if (-not $src) { return } - $srcStr = "$src" - X "$indent<$picTag>" - if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } - else { X "$indent`t$(Esc-Xml $srcStr)" } - X "$indent`t$(if ($lt) { 'true' } else { 'false' })" - if ($tpx) { X "$indent`t" } - X "$indent" -} - -# Картинки заголовка/подвала колонки поля — по схеме сразу после , -# перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь). -function Emit-ColumnPics { - param($el, [string]$indent) - Emit-PictureRef -val $el.headerPicture -picTag 'HeaderPicture' -indent $indent - Emit-PictureRef -val $el.footerPicture -picTag 'FooterPicture' -indent $indent -} - -# кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false -# (обратная конвенция относительно header/values-картинок). Прощающий ввод: -# принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель -# опишет картинку объектно по аналогии с headerPicture. $elemLt — legacy -# элемент-уровневый ключ loadTransparent (используется, если в объекте флаг не задан). -function Emit-CommandPicture { - param($pic, $elemLt, [string]$indent) - if (-not $pic) { return } - $src = $null; $lt = $null; $tpx = $null - if ($pic -is [string]) { $src = $pic } - else { $src = $pic.src; if ($null -ne $pic.loadTransparent) { $lt = [bool]$pic.loadTransparent }; $tpx = $pic.transparentPixel } - if (-not $src) { return } - if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt } - $srcStr = "$src" - X "$indent" - if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } - else { X "$indent`t$(Esc-Xml $srcStr)" } - X "$indent`t$(if ($lt -eq $false) { 'false' } else { 'true' })" - if ($tpx) { X "$indent`t" } - X "$indent" -} - -# --- Оформление элемента: цвета / шрифты / граница --- -# Прямые свойства элемента (// + header/footer-варианты у полей). -# Ключи DSL — англ. camelCase 1:1 с тегами; принимаем рус. синонимы (forgiving). -# Значения: цвет — verbatim-строка (style:/web:/sys:/win:/#RRGGBB); шрифт — строка-ref или -# объект-атрибуты; граница — строка-ref или объект {width,style|ref}. Порядок тегов — XSD (профиль). -$script:appearanceSpec = @{ - titleTextColor = @{ tag='TitleTextColor'; kind='color' } - titleBackColor = @{ tag='TitleBackColor'; kind='color' } - titleFont = @{ tag='TitleFont'; kind='font' } - footerTextColor = @{ tag='FooterTextColor'; kind='color' } - footerBackColor = @{ tag='FooterBackColor'; kind='color' } - footerFont = @{ tag='FooterFont'; kind='font' } - textColor = @{ tag='TextColor'; kind='color' } - backColor = @{ tag='BackColor'; kind='color' } - borderColor = @{ tag='BorderColor'; kind='color' } - border = @{ tag='Border'; kind='border'} - font = @{ tag='Font'; kind='font' } -} -# Рус. синоним (lower) → canonical key -$script:appearanceSynonyms = @{ - 'цветтекста'='textColor'; 'цветфона'='backColor'; 'цветрамки'='borderColor' - 'цветтекстазаголовка'='titleTextColor'; 'цветфоназаголовка'='titleBackColor'; 'шрифтзаголовка'='titleFont' - 'цветтекстаподвала'='footerTextColor'; 'цветфонаподвала'='footerBackColor'; 'шрифтподвала'='footerFont' - 'шрифт'='font'; 'рамка'='border' -} -# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. -# Ключи карты нормализованы (lowercase, без пробелов); сопоставление в Normalize-PropSynonyms тоже. -# Прощающий ввод: модель может писать свойство по-русски. Англ. ключ работает всегда (это доп. слой). -# Видимость/Доступность НЕ включаем — наш hidden/disabled инвертирован, был бы баг семантики. -$script:propSynonyms = @{ - 'пометка'='checked' - 'кнопкавыбора'='choiceButton'; 'кнопкаочистки'='clearButton'; 'кнопкарегулирования'='spinButton' - 'кнопкавыпадающегосписка'='dropListButton'; 'кнопкасписковоговыбора'='choiceListButton' - 'кнопкаоткрытия'='openButton'; 'кнопкапоумолчанию'='defaultButton' - 'быстрыйвыбор'='quickChoice'; 'формавыбора'='choiceForm'; 'историявыборапривводе'='choiceHistoryOnInput' - 'выборгруппиэлементов'='choiceFoldersAndItems'; 'фиксациявтаблице'='fixingInTable' - 'путькданнымподвала'='footerDataPath'; 'автоотметканезаполненного'='markIncomplete' - 'многострочныйрежим'='multiLine'; 'режимпароля'='passwordMode'; 'переноспословам'='wrap' - 'расположениезаголовка'='titleLocation'; 'пропускатьпривводе'='skipOnInput' - 'заголовок'='title'; 'ширина'='width'; 'высота'='height'; 'подсказкаввода'='inputHint' -} -# Профили порядка тегов по базовым типам (XSD-последовательность) -$script:appOrderField = @('titleTextColor','titleBackColor','titleFont','footerTextColor','footerBackColor','footerFont','textColor','backColor','borderColor','border','font') -$script:appOrderDecoration = @('textColor','font','backColor','borderColor','border') -$script:appOrderButton = @('textColor','backColor','borderColor','font') - -# Простые скаляры элемента (pass-through: captured/emitted «как есть»). Только НЕ-перекрывающиеся -# теги (не обрабатываемые специфично где-то ещё). kind: bool → true/false; value → строка verbatim. -$script:genericScalars = @( - @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } - @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } - @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } - @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } - @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } - @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } - @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } - @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } - @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } - @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } - @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } - @{ Tag='Mask'; Key='mask'; Kind='value' } - @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } - @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } - @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } - # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through - @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } - @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } - @{ Tag='Output'; Key='output'; Kind='value' } - @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } - @{ Tag='PointerType'; Key='pointerType'; Kind='value' } - @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } - @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } - @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } - @{ Tag='Protection'; Key='protection'; Kind='bool' } - @{ Tag='Edit'; Key='edit'; Kind='bool' } - @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } - @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } - @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } - @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } - @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } - @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } - @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } - @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } - @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } - @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } - @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } - @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } - @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } - @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } - # Корпусный хвост: представление управления свёрткой группы / форма кнопки-попапа / - # авто-добавление незаполненной строки / выделение отрицательных / нач. позиция списка / - # высота списка выбора / три состояния флажка / прокрутка страницы при сжатии - @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } - @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } - @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } - @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } - @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } - @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } - @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } - @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } - # Сочетание клавиш — общее свойство (input/group/radio/page/picField/label/table/check; команда — отд. путь, §7) - @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } - # Батч простых скаляров (input/radio/group/picDecoration/button): режим выбора незаполненного, - # равная ширина колонок, выравнивание детей, масштаб/зум картинки, форма/положение картинки кнопки. - # (Table HeaderHeight/FooterHeight/CurrentRowUse — НЕ здесь, а в Emit-Table: pass-through, - # 1С толерантна к порядку детей Table — в корпусе те же теги встречаются в разных позициях.) - @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } - @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } - @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } - @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } - @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } - @{ Tag='Shape'; Key='shape'; Kind='value' } - @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } - @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } - # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр - @{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' } - # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) - @{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' } - @{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' } - @{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' } - @{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' } - # Режим авто-показа кнопок открытия/очистки (input, enum Auto/Always/FilledOnly/…) - @{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' } - @{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' } - # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент, не атрибуты) - @{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' } - @{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' } - @{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' } - @{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' } - # Хвост листовых скаляров (по 1 в корпусе): автокоррекция ввода (input) / уникальность команды - # (button) / допуск пустого множ. значения (input) / поведение при гориз. сжатии (table) - @{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' } - @{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' } - @{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' } - @{ Tag='AllowInputEmptyMultipleValues';Key='allowInputEmptyMultipleValues';Kind='bool' } - @{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' } -) - -function Emit-GenericScalars { - param($el, [string]$indent) - if ($null -eq $el) { return } - foreach ($s in $script:genericScalars) { - $p = $el.PSObject.Properties[$s.Key] - if (-not $p -or $null -eq $p.Value) { continue } - if ($s.Kind -eq 'bool') { - X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})" - } else { - $v = "$($p.Value)"; if ($v -eq '') { continue } - X "$indent<$($s.Tag)>$(Esc-Xml $v)" - } - } -} - -function Get-AppearanceValue { - param($el, [string]$canonical) - if ($null -eq $el) { return $null } - $p = $el.PSObject.Properties[$canonical] - if ($p) { return $p.Value } - foreach ($syn in $script:appearanceSynonyms.Keys) { - if ($script:appearanceSynonyms[$syn] -eq $canonical) { - $pp = $el.PSObject.Properties[$syn] - if ($pp) { return $pp.Value } - } - } - return $null -} - -# — строка = ref на стиль (kind=StyleItem); объект = атрибуты. -function Emit-FontTag { - param([string]$tag, $val, [string]$indent) - if ($val -is [string]) { - X "$indent<$tag ref=`"$(Esc-Xml $val)`" kind=`"StyleItem`"/>" - return - } - $attrs = @() - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $pp = $val.PSObject.Properties[$a] - if ($pp -and $null -ne $pp.Value) { - $v = $pp.Value - if ($v -is [bool]) { $v = if ($v) {'true'} else {'false'} } - $attrs += "$a=`"$(Esc-Xml "$v")`"" - } - } - X "$indent<$tag $($attrs -join ' ')/>" -} - -# — строка/{ref} = из стиля (); {width,style} = явная. -function Emit-BorderTag { - param($val, [string]$indent) - if ($val -is [string]) { X "$indent"; return } - $refP = $val.PSObject.Properties['ref'] - if ($refP -and $refP.Value) { X "$indent"; return } - $width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 } - $style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null } - X "$indent" - if ($style) { X "$indent`t$(Esc-Xml $style)" } - X "$indent" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time — встроенный конфиг планировщика -# на реквизите planner-типа. Структурный DSL: items[] + appearance/поведение-скаляры + -# timeScale (уровни шкалы времени) + period. Каждое присутствующее поле → каноничный -# порядок; пропущенное → дефолт (планировщик всегда несёт полный блок). Декомпилятор -# делает полный захват → раундтрип бит-в-бит; ручной авторинг может быть кратким. -$script:PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' -$script:CHART_NS = 'http://v8.1c.ru/8.2/data/chart' - -function PL-Get { - param($o, [string]$k, $def = $null) - if ($null -ne $o -and $o.PSObject.Properties[$k] -and $null -ne $o.$k) { return $o.$k } - return $def -} -function PL-Bool { - param($v) - if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } - elseif ("$v" -eq 'True') { 'true' } - elseif ("$v" -eq 'False') { 'false' } - else { "$v" } -} -function Emit-PlannerColor { - param([string]$tag, $o, [string]$key, [string]$ind) - X "$ind$(Esc-Xml "$(PL-Get $o $key 'auto')")" -} -# /… — пустое → самозакрывающийся тег (как в выгрузке платформы). -function Emit-PlannerText { - param([string]$tag, $v, [string]$ind) - if ([string]::IsNullOrEmpty("$v")) { X "$ind" } - else { X "$ind$(Esc-Xml "$v")" } -} -# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef"; -# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы. -function Test-PlannerRef { - param([string]$v) - return ($v -match '^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' -or ` - $v -match '\.EnumValue\.' -or $v -match 'EmptyRef$' -or ` - $v -match '^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') -} -# — nil (нет значения) / xr:DesignTimeRef (ссылка) / xs:string (строка/прочее). -function Emit-PlannerValue { - param($v, [string]$ind) - if ($null -eq $v -or "$v" -eq '') { X "$ind"; return } - $t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' } - X "$ind$(Esc-Xml "$v")" -} -function Emit-PlannerFont { - param($o, [string]$ind) - $f = PL-Get $o 'font' $null - if ($null -eq $f) { X "$ind"; return } - Emit-FontTag -tag 'pl:font' -val $f -indent $ind -} -function Emit-PlannerBorder { - param($o, [string]$ind, [string]$key = 'border') - $b = PL-Get $o $key $null - $bw = if ($b) { PL-Get $b 'width' 1 } else { 1 } - $bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' } - X "$ind" - X "$ind`t$(Esc-Xml "$bs")" - X "$ind" -} -function Emit-PlannerLevel { - param($lv, [string]$cns, [string]$ind) - $li = "$ind`t" - X "$ind" - X "$li$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")" - X "$li$(PL-Get $lv 'interval' 1)" - X "$li$(PL-Bool (PL-Get $lv 'show' $true))" - $line = PL-Get $lv 'line' $null - $lw = if ($line) { PL-Get $line 'width' 1 } else { 1 } - $lg = if ($line) { PL-Get $line 'gap' $false } else { $false } - $lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' } - X "$li" - X "$li`t$(Esc-Xml "$lst")" - X "$li" - X "$li$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")" - X "$li$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")" - $fmt = PL-Get $lv 'format' $null - if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } } - X "$li" - Emit-MLItems -val $fmt -indent "$li`t" - X "$li" - $labels = PL-Get $lv 'labels' $null - $ticks = if ($labels) { PL-Get $labels 'ticks' 0 } else { 0 } - X "$li" - X "$li`t$ticks" - X "$li" - X "$li$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")" - X "$li$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")" - X "$li$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))" - X "$ind" -} -function Emit-PlannerTimeScale { - param($ts, [string]$ind) - $cns = $script:CHART_NS - $ci = "$ind`t" - X "$ind" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")" - $levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() } - if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт - foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci } - $transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false } - X "$ci$(PL-Bool $transp)" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")" - X "$ci$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })" - X "$ind" -} -function Emit-PlannerItem { - param($it, [string]$ind) - X "$ind" - $ii = "$ind`t" - Emit-PlannerValue (PL-Get $it 'value' $null) $ii - Emit-PlannerText 'text' (PL-Get $it 'text' '') $ii - Emit-PlannerText 'tooltip' (PL-Get $it 'tooltip' '') $ii - X "$ii$(PL-Get $it 'begin' '0001-01-01T00:00:00')" - X "$ii$(PL-Get $it 'end' '0001-01-01T00:00:00')" - Emit-PlannerColor 'borderColor' $it 'borderColor' $ii - Emit-PlannerColor 'backColor' $it 'backColor' $ii - Emit-PlannerColor 'textColor' $it 'textColor' $ii - Emit-PlannerFont $it $ii - X "$ii" - X "$ii$(PL-Get $it 'replacementDate' '0001-01-01T00:00:00')" - X "$ii$(PL-Bool (PL-Get $it 'deleted' $false))" - $id = PL-Get $it 'id' $null - if ($null -eq $id) { $id = [guid]::NewGuid().ToString() } - X "$ii$id" - X "$ii$(PL-Bool (PL-Get $it 'textFormatted' $false))" - Emit-PlannerBorder $it $ii 'border' - X "$ii$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")" - X "$ind" -} -# Элемент измерения ( внутри ) — рекурсивен: может нести вложенные -# элементы (UI: колонка «Элементы» у элемента). Порядок: value, text, цвета, font, -# вложенные элементы, showOnlySubordinatesAreas, textFormatted. -function Emit-PlannerDimElement { - param($el, [string]$ind) - X "$ind" - $ii = "$ind`t" - Emit-PlannerValue (PL-Get $el 'value' $null) $ii - Emit-PlannerText 'text' (PL-Get $el 'text' '') $ii - Emit-PlannerColor 'borderColor' $el 'borderColor' $ii - Emit-PlannerColor 'backColor' $el 'backColor' $ii - Emit-PlannerColor 'textColor' $el 'textColor' $ii - Emit-PlannerFont $el $ii - foreach ($sub in @(PL-Get $el 'elements' @())) { Emit-PlannerDimElement $sub $ii } - X "$ii$(PL-Bool (PL-Get $el 'showOnlySubordinatesAreas' $true))" - X "$ii$(PL-Bool (PL-Get $el 'textFormatted' $false))" - X "$ind" -} -# Измерение планировщика () — объект разреза + его элементы. -function Emit-PlannerDimension { - param($d, [string]$ind) - X "$ind" - $di = "$ind`t" - Emit-PlannerValue (PL-Get $d 'value' $null) $di - Emit-PlannerText 'text' (PL-Get $d 'text' '') $di - Emit-PlannerColor 'borderColor' $d 'borderColor' $di - Emit-PlannerColor 'backColor' $d 'backColor' $di - Emit-PlannerColor 'textColor' $d 'textColor' $di - Emit-PlannerFont $d $di - foreach ($el in @(PL-Get $d 'elements' @())) { Emit-PlannerDimElement $el $di } - X "$di$(PL-Bool (PL-Get $d 'textFormatted' $false))" - X "$ind" -} -function Emit-PlannerSettings { - param($pl, [string]$ind) - X "$ind" - $si = "$ind`t" - foreach ($it in @(PL-Get $pl 'items' @())) { Emit-PlannerItem $it $si } - foreach ($d in @(PL-Get $pl 'dimensions' @())) { Emit-PlannerDimension $d $si } - Emit-PlannerColor 'borderColor' $pl 'borderColor' $si - Emit-PlannerColor 'backColor' $pl 'backColor' $si - Emit-PlannerColor 'textColor' $pl 'textColor' $si - Emit-PlannerColor 'lineColor' $pl 'lineColor' $si - Emit-PlannerFont $pl $si - X "$si$(PL-Get $pl 'beginOfRepresentationPeriod' '0001-01-01T00:00:00')" - X "$si$(PL-Get $pl 'endOfRepresentationPeriod' '0001-01-01T00:00:00')" - X "$si$(PL-Bool (PL-Get $pl 'alignElementsOfTimeScale' $true))" - X "$si$(PL-Bool (PL-Get $pl 'displayTimeScaleWrapHeaders' $true))" - X "$si$(PL-Bool (PL-Get $pl 'displayWrapHeaders' $true))" - $wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null - if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } } - Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si - X "$si$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")" - X "$si$(PL-Get $pl 'periodicVariantRepetition' 1)" - X "$si$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)" - X "$si$(PL-Get $pl 'timeScaleWrapEndIndent' 0)" - Emit-PlannerTimeScale (PL-Get $pl 'timeScale' $null) $si - $period = PL-Get $pl 'period' $null - if ($period) { - X "$si" - X "$si`t$(PL-Get $period 'begin' '0001-01-01T00:00:00')" - X "$si`t$(PL-Get $period 'end' '0001-01-01T00:00:00')" - X "$si" - } - X "$si$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))" - X "$si$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")" - X "$si$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")" - X "$si$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))" - X "$si$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))" - X "$si$(PL-Get $pl 'minColumnWidth' 0)" - X "$si$(PL-Get $pl 'minRowHeight' 0)" - X "$si$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")" - X "$si$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")" - Emit-PlannerBorder $pl $si 'border' - X "$si$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")" - X "$ind" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time — генерик-эмиттер (зеркало -# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии -# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands), -# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип). -$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } -$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } -$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale') -function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } } -function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } } -function Emit-ChartNode { - param([string]$name, $val, [string]$ind) - if ($script:CHART_ML_FIELDS.Contains($name)) { - if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } - X "$ind"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind"; return - } - if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) { - foreach ($e in $val) { Emit-ChartNode $name $e $ind } - return - } - if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) { - $keys = Get-Keys $val - if ($script:CHART_ATTR_FIELDS.Contains($name)) { - $attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" } - X "$ind"; return - } - if ($keys -contains 'gap') { - $w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style' - X "$ind" - X "$ind`t$(Esc-Xml "$st")" - X "$ind"; return - } - if (($keys -contains 'style') -and ($keys -contains 'width')) { - $w = Get-Prop $val 'width'; $st = Get-Prop $val 'style' - X "$ind" - X "$ind`t$(Esc-Xml "$st")" - X "$ind"; return - } - $isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } } - if ($isFont) { - $attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } } - X "$ind"; return - } - if (@($keys).Count -eq 0) { X "$ind"; return } - X "$ind" - foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" } - X "$ind" - return - } - if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } - if ($val -is [bool]) { X "$ind$(PL-Bool $val)"; return } - X "$ind$(Esc-Xml "$val")" -} -function Emit-ChartSettings { - param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart') - X "$ind" - foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" } - X "$ind" -} - -function Emit-Appearance { - param($el, [string]$indent, [string]$profile = 'field') - if ($null -eq $el) { return } - $order = switch ($profile) { - 'decoration' { $script:appOrderDecoration } - 'button' { $script:appOrderButton } - default { $script:appOrderField } - } - foreach ($key in $order) { - $val = Get-AppearanceValue -el $el -canonical $key - if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } - $spec = $script:appearanceSpec[$key] - switch ($spec.kind) { - 'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")" } - 'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent } - 'border' { Emit-BorderTag -val $val -indent $indent } - } - } -} - -function Emit-Layout { - param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false) - # CommandSet (отключённые команды редактора) — общее свойство поля (input/label/check/ - # spreadsheet/html/formatted/picture); в схеме рано (после TitleLocation, перед скалярами). - if ($el.excludedCommands -and @($el.excludedCommands).Count -gt 0) { - X "$indent" - foreach ($cmd in $el.excludedCommands) { X "$indent`t$cmd" } - X "$indent" - } - Emit-CommonElementProps -el $el -indent $indent - $amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth') - if ($amwExplicit) { - if ($el.autoMaxWidth -eq $false) { X "$indentfalse" } - } elseif ($multiLineDefault) { - X "$indentfalse" - } - if ($null -ne $el.maxWidth) { X "$indent$($el.maxWidth)" } - if ($el.autoMaxHeight -eq $false) { X "$indentfalse" } - if ($null -ne $el.maxHeight) { X "$indent$($el.maxHeight)" } - if ($el.width) { X "$indent$($el.width)" } - if (-not $skipHeight -and $el.height) { X "$indent$($el.height)" } - if ($null -ne $el.horizontalStretch) { X "$indent$(if ($el.horizontalStretch){'true'}else{'false'})" } - if ($null -ne $el.verticalStretch) { X "$indent$(if ($el.verticalStretch){'true'}else{'false'})" } - if ($el.groupHorizontalAlign) { X "$indent$($el.groupHorizontalAlign)" } - if ($el.groupVerticalAlign) { X "$indent$($el.groupVerticalAlign)" } - if ($el.horizontalAlign) { X "$indent$($el.horizontalAlign)" } - Emit-GenericScalars -el $el -indent $indent -} - -function Title-FromName { - param([string]$name) - if (-not $name) { return '' } - $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') - $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') - $parts = $s -split ' ' - if ($parts.Count -eq 0) { return $s } - $out = New-Object System.Collections.ArrayList - [void]$out.Add($parts[0]) - for ($i = 1; $i -lt $parts.Count; $i++) { - $p = $parts[$i] - if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { - [void]$out.Add($p) - } else { - [void]$out.Add($p.ToLower()) - } - } - return ($out -join ' ') -} - -function Emit-Title { - # Нет ключа title → авто-вывод из имени (помощь модели). - # Явный title: "" (или null) → подавить (заголовок не эмитим). - # Явный непустой → эмитим как есть. - param($el, [string]$name, [string]$indent, [switch]$auto) - $hasKey = $null -ne $el.PSObject.Properties['title'] - if ($hasKey) { - if ($el.title) { Emit-MLText -tag "Title" -text $el.title -indent $indent } - } elseif ($auto -and $name) { - Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent - } - # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. - if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } - # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. - if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } -} - -function Map-TitleLoc { - param([string]$v) - switch ("$v".ToLower()) { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - "auto" { "Auto" } - default { "$v" } - } -} - -# TitleLocation у check/radio: нет ключа → умный дефолт (Right/None), эмитится; -# "" → подавить (= дефолт платформы, она его сама не пишет); значение → эмитить (маппинг регистра). -function Emit-TitleLocation { - param($el, [string]$indent, [string]$smartDefault) - if ($null -ne $el.PSObject.Properties['titleLocation']) { - if ($el.titleLocation) { X "$indent$(Map-TitleLoc "$($el.titleLocation)")" } - } elseif ($smartDefault) { - X "$indent$smartDefault" - } -} - -function Warn-Unrecognized { - # drop-on-miss enum: значение не распознано → тег не эмитится. Громко, чтобы автор увидел потерю. - param([string]$key, $raw, [string[]]$valid, [string]$owner) - Write-Warning "Unrecognized $key '$raw' on '$owner'. Valid values: $($valid -join ', '). Value ignored." -} - -function Emit-Group { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. - $groupVal = "$($el.group)".ToLower() - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwayshorizontal" { "AlwaysHorizontal" } - "alwaysvertical" { "AlwaysVertical" } - "horizontalifpossible" { "HorizontalIfPossible" } - "collapsible" { "Vertical" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - elseif ($groupVal) { Warn-Unrecognized 'group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name } - - # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). - # Legacy: group:'collapsible' эквивалентно behavior:'collapsible'. - $behaviorVal = if ($el.behavior) { "$($el.behavior)".ToLower() } elseif ($groupVal -eq "collapsible") { "collapsible" } else { $null } - $bmap = @{ "usual"="Usual"; "collapsible"="Collapsible"; "popup"="PopUp" } - if ($behaviorVal -and $bmap.ContainsKey($behaviorVal)) { - X "$inner$($bmap[$behaviorVal])" - } elseif ($el.behavior -and -not $bmap.ContainsKey($behaviorVal)) { - Warn-Unrecognized 'behavior' $el.behavior @('collapsible','popup') $name - } - # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) - if ($el.collapsed -eq $true) { X "$innertrue" } - - # Representation - if ($el.representation) { - $repr = switch ("$($el.representation)") { - "none" { "None" } - "normal" { "NormalSeparation" } - "weak" { "WeakSeparation" } - "strong" { "StrongSeparation" } - default { "$($el.representation)" } - } - X "$inner$repr" - } - - # Использование текущей строки группы (после Representation, порядок XSD) - if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } - - # ShowTitle - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст - if ($el.collapsedTitle) { Emit-MLText -tag "CollapsedRepresentationTitle" -text $el.collapsedTitle -indent $inner } - - # United - if ($el.united -eq $false) { X "$innerfalse" } - - # Формат значения пути к данным заголовка (; парный к titleDataPath группы) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-ColumnGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation (horizontal / vertical / inCell — последнее только здесь) - $groupVal = "$($el.columnGroup)" - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "inCell" { "InCell" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - elseif ($groupVal) { Warn-Unrecognized 'columnGroup orientation' $el.columnGroup @('vertical','horizontal','inCell') $name } - - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # showInHeader эмитится общим Emit-CommonElementProps (через Emit-Layout) - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) - Emit-ColumnPics -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Input { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - default { "$($el.titleLocation)" } - } - X "$inner$loc" - } - - if ($null -ne $el.multiLine) { X "$inner$(if ($el.multiLine){'true'}else{'false'})" } - if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } - # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, - # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). - if ($null -ne $el.choiceButton) { X "$inner$(if ($el.choiceButton){'true'}else{'false'})" } - # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) - if ($null -ne $el.clearButton) { X "$inner$(if ($el.clearButton){'true'}else{'false'})" } - if ($null -ne $el.spinButton) { X "$inner$(if ($el.spinButton){'true'}else{'false'})" } - if ($null -ne $el.dropListButton) { X "$inner$(if ($el.dropListButton){'true'}else{'false'})" } - if ($null -ne $el.choiceListButton) { X "$inner$(if ($el.choiceListButton){'true'}else{'false'})" } - if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - if ($el.textEdit -eq $false) { X "$innerfalse" } - # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) - foreach ($p in @( - @('wrap','Wrap'), @('openButton','OpenButton'), @('listChoiceMode','ListChoiceMode'), - @('extendedEditMultipleValues','ExtendedEditMultipleValues'), @('chooseType','ChooseType'), - @('quickChoice','QuickChoice'), @('autoChoiceIncomplete','AutoChoiceIncomplete') - )) { - if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } - } - # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. - # availableTypes — формат типа реквизита (§type); Emit-Type сам разбирает мультитип "a | b". - if ($null -ne $el.typeDomainEnabled) { X "$inner$(if ($el.typeDomainEnabled){'true'}else{'false'})" } - if ($el.availableTypes) { Emit-Type -typeStr $el.availableTypes -indent $inner -tag 'AvailableTypes' } - # InputField-специфичные value-скаляры - foreach ($p in @( - @('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'), - @('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath') - )) { - if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")" } - } - # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). - foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) { - if ($null -ne $el.($p[0])) { - $mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' } - X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")" - } - } - if ($el.choiceButtonRepresentation) { X "$inner$($el.choiceButtonRepresentation)" } - Emit-PictureRef -val $el.choiceButtonPicture -picTag 'ChoiceButtonPicture' -indent $inner - Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) - - if ($el.inputHint) { - Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner - } - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - Emit-ChoiceList -el $el -indent $inner - - # Связи по типу / связи параметров выбора / параметры выбора - Emit-TypeLink -el $el -indent $inner - Emit-ChoiceParameterLinks -el $el -indent $inner - Emit-ChoiceParameters -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" - - X "$indent" -} - -function Emit-Check { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг - if ($null -ne $el.PSObject.Properties['checkBoxType']) { - if ($el.checkBoxType) { - $cbt = switch ("$($el.checkBoxType)".ToLower()) { 'auto' {'Auto'} 'checkbox' {'CheckBox'} 'switcher' {'Switcher'} 'tumbler' {'Tumbler'} default {"$($el.checkBoxType)"} } - X "$inner$cbt" - } - } else { X "$innerAuto" } - - Emit-TitleLocation -el $el -indent $inner -smartDefault "Right" - - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) - if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" - - X "$indent" -} - -# Maps Russian/English root of a typed reference path to canonical English root. -# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". -$script:refRootSynonyms = @{ - "Перечисление" = "Enum" - "Справочник" = "Catalog" - "Документ" = "Document" - "ПланСчетов" = "ChartOfAccounts" - "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" - "ПланВидовРасчета" = "ChartOfCalculationTypes" - "ПланВидовРасчёта" = "ChartOfCalculationTypes" - "ПланОбмена" = "ExchangePlan" - "БизнесПроцесс" = "BusinessProcess" - "Задача" = "Task" - "РегистрСведений" = "InformationRegister" - "РегистрНакопления" = "AccumulationRegister" - "РегистрБухгалтерии" = "AccountingRegister" - "РегистрРасчета" = "CalculationRegister" - "РегистрРасчёта" = "CalculationRegister" - "ЖурналДокументов" = "DocumentJournal" - "КритерийОтбора" = "FilterCriterion" -} -$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") - -# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты". -# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений. -function Normalize-MetaTypeRef { - param([string]$ref) - if ([string]::IsNullOrEmpty($ref)) { return $ref } - $dot = $ref.IndexOf('.') - if ($dot -lt 1) { return $ref } - $root = $ref.Substring(0, $dot) - if ($script:refRootSynonyms.ContainsKey($root)) { - return $script:refRootSynonyms[$root] + $ref.Substring($dot) - } - return $ref -} - -# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } -function Normalize-ChoiceValue { - param($value) - - # Booleans - if ($value -is [bool]) { - return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } - } - # Numbers (int / decimal / double) - if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { - return @{ XsiType = "xs:decimal"; Text = "$value" } - } - - $s = "$value" - if ([string]::IsNullOrEmpty($s)) { - return @{ XsiType = "xs:string"; Text = "" } - } - - # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime - if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { - return @{ XsiType = "xs:dateTime"; Text = $s } - } - - # Raw-ссылка по GUID (метаданные.значение, оба GUID): "GUID.GUID" → xr:DesignTimeRef - # (всегда ссылка, не строка; named-ссылки Enum.X.Y детектятся ниже). - if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F]{8}-[0-9a-fA-F-]+$') { - return @{ XsiType = "xr:DesignTimeRef"; Text = $s } - } - - # Try to detect typed reference path: ".[..]" - $parts = $s -split '\.' - if ($parts.Count -ge 2) { - $root = $parts[0] - $canonRoot = $null - if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } - elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } - - if ($canonRoot) { - $typeName = $parts[1] - $normalized = $null - - if ($canonRoot -eq "Enum") { - if ($parts.Count -eq 2) { - # "Enum.X" alone — not a value, treat as string - } elseif ($parts.Count -eq 3) { - # "Enum.X.Y" — insert .EnumValue. ("EmptyRef" — пустая ссылка, БЕЗ вставки) - if ($parts[2] -eq 'EmptyRef') { $normalized = "Enum.$typeName.EmptyRef" } - else { $normalized = "Enum.$typeName.EnumValue.$($parts[2])" } - } else { - # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) - $member = $parts[2] - if ($script:enumValueSynonyms -contains $member) { - $rest = $parts[3..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } else { - $rest = $parts[2..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } - } - } else { - # Other ref roots: just translate root, keep tail as-is - if ($parts.Count -ge 3) { - $tail = $parts[1..($parts.Count-1)] -join '.' - $normalized = "$canonRoot.$tail" - } - } - - if ($normalized) { - return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } - } - } - } - - return @{ XsiType = "xs:string"; Text = $s } -} - -# Emit Presentation block for a choiceList item. -# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. -# Empty/null → emits empty . -function Emit-ChoicePresentation { - param($pres, [string]$indent) - if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { - X "$indent" - return - } - - $pairs = @() - if ($pres -is [string]) { - $pairs += ,@("ru", $pres) - } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { - foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } - } elseif ($pres.PSObject -and $pres.PSObject.Properties) { - foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } - } else { - $pairs += ,@("ru", "$pres") - } - - X "$indent" - foreach ($pair in $pairs) { - X "$indent`t" - X "$indent`t`t$($pair[0])" - X "$indent`t`t$(Esc-Xml $pair[1])" - X "$indent`t" - } - X "$indent" -} - -# для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). -function Get-ChoiceValueTag { - param($norm) - if ([string]::IsNullOrEmpty($norm.Text)) { return "" } - return "$(Esc-Xml $norm.Text)" -} - -# Emit (список выбора) — у RadioButtonField и InputField. -# Элемент: { value, presentation?/title? } (+ рус. синонимы значение/представление). -function Emit-ChoiceList { - param($el, [string]$indent) - if (-not $el.choiceList -or $el.choiceList.Count -eq 0) { return } - X "$indent" - $itemIndent = "$indent`t" - foreach ($item in $el.choiceList) { - # value (+ рус. синоним "значение") - $valRaw = $null - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("value")) { $valRaw = $item["value"] } - elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } - } else { - if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } - elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } - } - - # presentation (presentation OR title синоним) - $presRaw = $null - $hasPres = $false - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } - elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } - elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } - } else { - if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } - elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } - elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } - } - - # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — - # переопределяет авто-детект (Normalize-ChoiceValue вывела бы xs:string). - $vtRaw = $null - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("valueType")) { $vtRaw = "$($item["valueType"])" } - } elseif ($item.PSObject.Properties["valueType"]) { $vtRaw = "$($item.valueType)" } - - if ($vtRaw -eq 'nil') { $norm = @{ XsiType = $null; Text = $null; Nil = $true } } - elseif ($vtRaw) { $norm = @{ XsiType = $vtRaw; Text = "$valRaw" } } - else { $norm = Normalize-ChoiceValue -value $valRaw } - - # авто-вывод presentation, если не задан - if (-not $hasPres) { - if ($norm.XsiType -eq "xr:DesignTimeRef") { - $tail = ($norm.Text -split '\.')[-1] - $presRaw = Title-FromName -name $tail - } else { - $presRaw = $norm.Text - } - } - - X "$itemIndent" - $valIndent = "$itemIndent`t" - X "$valIndent" - X "$valIndent0" - X "$valIndent" - Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" - X "$valIndent`t$(if ($norm.Nil) { '' } else { Get-ChoiceValueTag $norm })" - X "$valIndent" - X "$itemIndent" - } - X "$indent" -} - -# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null). -function Get-ElProp { - param($obj, [string[]]$names) - if ($null -eq $obj) { return $null } - foreach ($n in $names) { - if ($obj -is [System.Collections.IDictionary]) { - if ($obj.Contains($n)) { return $obj[$n] } - } elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { - return $obj.PSObject.Properties[$n].Value - } - } - return $null -} - -# Приведение строкового литерала shorthand к типу: true/false → bool, целое/дробное → число, -# иначе строка (ref-путь нормализует уже Normalize-ChoiceValue). -function ConvertTo-ScalarLiteral { - param([string]$s) - $t = "$s".Trim() - if ($t -match '^(?i:true)$') { return $true } - if ($t -match '^(?i:false)$') { return $false } - if ($t -match '^-?\d+$') { return [int]$t } - if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) } - return $t -} - -# Shorthand параметра выбора: "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. -function ConvertFrom-ChoiceParamShorthand { - param([string]$s) - $eq = $s.IndexOf('=') - if ($eq -lt 0) { return @{ name = $s.Trim() } } - $name = $s.Substring(0, $eq).Trim() - $rest = $s.Substring($eq + 1) - if ($rest -match ',') { - $vals = @() - foreach ($part in ($rest -split ',')) { $vals += ,(ConvertTo-ScalarLiteral $part) } - return @{ name = $name; value = $vals } - } - return @{ name = $name; value = (ConvertTo-ScalarLiteral $rest) } -} - -# Shorthand связи параметров выбора: "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. -function ConvertFrom-ChoiceParamLinkShorthand { - param([string]$s) - $eq = $s.IndexOf('=') - if ($eq -lt 0) { return @{ name = $s.Trim() } } - $o = @{ name = $s.Substring(0, $eq).Trim() } - $rest = $s.Substring($eq + 1).Trim() - if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { - $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] - } else { - $o['dataPath'] = $rest - } - return $o -} - -# Shorthand связи по типу: "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. -function ConvertFrom-TypeLinkShorthand { - param([string]$s) - if ($s -match '^(.*)#(\d+)$') { return @{ dataPath = $matches[1].Trim(); linkItem = [int]$matches[2] } } - return @{ dataPath = "$s".Trim() } -} - -# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . -# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. -function Emit-ChoiceParamValue { - # $isArray передаётся ЯВНО из вызывающего кода: PowerShell разворачивает одноэлементный массив - # при биндинге параметра ($value становится скаляром), поэтому определять массив тут — ненадёжно - # (1-элементный список `["X"]` эмитился бы скаляром вместо FixedArray). foreach по скаляру = 1 итерация. - param($value, [string]$indent, [bool]$isArray) - X "$indent" - if ($isArray) { - X "$indent" - foreach ($v in $value) { - $norm = Normalize-ChoiceValue -value $v - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t$(Get-ChoiceValueTag $norm)" - X "$indent`t" - } - X "$indent" - } else { - $norm = Normalize-ChoiceValue -value $value - X "$indent$(Get-ChoiceValueTag $norm)" - } -} - -# (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue; -# массив значений → FixedArray. Рус. синонимы имя/значение. -function Emit-ChoiceParameters { - param($el, [string]$indent) - $cp = $el.choiceParameters - if (-not $cp -or @($cp).Count -eq 0) { return } - X "$indent" - foreach ($item in @($cp)) { - if ($item -is [string]) { $item = ConvertFrom-ChoiceParamShorthand $item } - $name = Get-ElProp $item @('name','имя') - # Наличие ключа value (≠ значения) + ПРЯМОЙ доступ к значению (без Get-ElProp): его return - # разворачивает 1-элементный массив (PS unwrap), теряя массив-ность → FixedArray не эмитится. - # Индексер/member-доступ массив сохраняет; if-выражение/функция-return — нет. - $hasVal = $false; $val = $null - if ($item -is [System.Collections.IDictionary]) { - if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] } - elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] } - } else { - if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value } - elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value } - } - $valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string]) - X "$indent`t" - # Параметр выбора без значения → (платформа, 13 в корпусе); - # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. - if (-not $hasVal) { - X "$indent`t`t" - } else { - X "$indent`t`t" - Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" -isArray $valIsArray - X "$indent`t`t" - } - X "$indent`t" - } - X "$indent" -} - -# (связи параметров выбора) — [{name, dataPath, valueChange?}]. -# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. -function Emit-ChoiceParameterLinks { - param($el, [string]$indent) - $cpl = $el.choiceParameterLinks - if (-not $cpl -or @($cpl).Count -eq 0) { return } - X "$indent" - foreach ($lk in @($cpl)) { - if ($lk -is [string]) { $lk = ConvertFrom-ChoiceParamLinkShorthand $lk } - $name = Get-ElProp $lk @('name','имя') - $dp = Get-ElProp $lk @('dataPath','path','путь') - $vcRaw = Get-ElProp $lk @('valueChange','режимИзменения') - $vc = "Clear" - if ($vcRaw) { - $vc = switch -Regex ("$vcRaw".ToLower()) { - '^(clear|очистить|очистка)$' { "Clear"; break } - '^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break } - default { "$vcRaw" } - } - } - X "$indent`t" - X "$indent`t`t$(Esc-Xml "$name")" - X "$indent`t`t$(Esc-Xml "$dp")" - X "$indent`t`t$vc" - X "$indent`t" - } - X "$indent" -} - -# (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. -function Emit-TypeLink { - param($el, [string]$indent) - $tl = $el.typeLink - if (-not $tl) { return } - if ($tl -is [string]) { $tl = ConvertFrom-TypeLinkShorthand $tl } - $dp = Get-ElProp $tl @('dataPath','path','путь') - $li = Get-ElProp $tl @('linkItem','элементСвязи') - if ($null -eq $li) { $li = 0 } - X "$indent" - X "$indent`t$(Esc-Xml "$dp")" - X "$indent`t$li" - X "$indent" -} - -function Emit-Radio { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-TitleLocation -el $el -indent $inner -smartDefault "None" - - # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. - $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } - $rbt = switch -Regex ($rbtRaw.ToLower()) { - '^(auto|авто)$' { "Auto"; break } - '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } - '^(tumbler|тумблер)$' { "Tumbler"; break } - default { $rbtRaw } - } - X "$inner$rbt" - - if ($null -ne $el.columnsCount) { - X "$inner$($el.columnsCount)" - } - - Emit-ChoiceList -el $el -indent $inner - - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" - - X "$indent" -} - -# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму -# (reuse Resolve-MLFormatted, как у extendedTooltip). Атрибут formatted эмитится ВСЕГДА -# (специфика декораций). Sibling-ключ `formatted` — back-compat override авто-детекта. -function Emit-DecorationTitle { - param($el, [string]$name, [string]$indent, [switch]$auto) - $hasKey = $null -ne $el.PSObject.Properties['title'] - $titleVal = if ($hasKey) { $el.title } elseif ($auto -and $name) { Title-FromName -name $name } else { $null } - if ($titleVal) { - $r = Resolve-MLFormatted $titleVal - $fmt = if ($null -ne $el.PSObject.Properties['formatted']) { [bool]$el.formatted } else { $r.formatted } - X "$indent<Title formatted=`"$(if ($fmt) { 'true' } else { 'false' })`">" - Emit-MLItems -val $r.text -indent "$indent`t" - X "$indent" - } - if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } - if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } -} - -function Emit-Label { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title - # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). - Emit-CommonFlags -el $el -indent $inner - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - Emit-Appearance -el $el -indent $inner -profile 'decoration' - - Emit-DecorationTitle -el $el -name $name -indent $inner -auto - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" - - X "$indent" -} - -function Emit-LabelField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.editMode) { X "$inner$($el.editMode)" } - # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode - if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } - # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение - if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } - Emit-ColumnPics -el $el -indent $inner - # ВНИМАНИЕ: у LabelField платформенный тег именно (опечатка 1С), не . - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" - - X "$indent" -} - -# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). -# Платформа всегда эмитит этот блок на дин-список-таблице; компилятор зеркалит дефолты, -# DSL-ключ переопределяет; декомпилятор инвертирует (опускает значения = дефолту). -function Emit-DynListTableBlock { - param($el, [string]$indent) - # (useAlternationRowColor — общее свойство таблицы, эмитится в Emit-Table) - # Group A (гарант. блок, n=5079): дефолт + override - $ar = if ($el.autoRefresh -eq $true) { "true" } else { "false" } - X "$indent$ar" - $arp = if ($el.PSObject.Properties["autoRefreshPeriod"] -and $null -ne $el.autoRefreshPeriod) { $el.autoRefreshPeriod } else { 60 } - X "$indent$arp" - X "$indent" - X "$indent`tCustom" - X "$indent`t0001-01-01T00:00:00" - X "$indent`t0001-01-01T00:00:00" - X "$indent" - $cfi = if ($el.choiceFoldersAndItems) { $el.choiceFoldersAndItems } else { "Items" } - X "$indent$cfi" - $rcr = if ($el.restoreCurrentRow -eq $true) { "true" } else { "false" } - X "$indent$rcr" - X "$indent" - $sr = if ($el.showRoot -eq $false) { "false" } else { "true" } - X "$indent$sr" - $arc = if ($el.allowRootChoice -eq $true) { "true" } else { "false" } - X "$indent$arc" - $uodc = if ($el.updateOnDataChange) { $el.updateOnDataChange } else { "Auto" } - X "$indent$uodc" - if ($el.userSettingsGroup) { X "$indent$($el.userSettingsGroup)" } - $agcru = if ($el.allowGettingCurrentRowURL -eq $false) { "false" } else { "true" } - X "$indent$agcru" -} - -function Emit-Table { - param($el, [string]$name, [int]$id, [string]$indent) - - $script:currentTableName = $name # дефолт source для кастомных дополнений в commandBar - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - # ChangeRowSet/Order — эмитим явное значение (в т.ч. false: платформа пишет его на ValueTable) - if ($el.PSObject.Properties['changeRowSet'] -and $null -ne $el.changeRowSet) { - X "$inner$(if ($el.changeRowSet -eq $true){'true'}else{'false'})" - } - if ($el.PSObject.Properties['changeRowOrder'] -and $null -ne $el.changeRowOrder) { - X "$inner$(if ($el.changeRowOrder -eq $true){'true'}else{'false'})" - } - if ($el.autoInsertNewRow -eq $true) { X "$innertrue" } - # RowFilter — nil-плейсхолдер (всегда пустой); ключ присутствует → эмитим - if ($el.PSObject.Properties['rowFilter']) { X "$inner" } - # Высота в строках таблицы () — отдельное свойство от (высота элемента, - # эмитится generic-ом Emit-Layout ниже). Таблица может нести оба (237 в корпусе). - if ($el.heightInTableRows) { X "$inner$($el.heightInTableRows)" } - if ($el.header -eq $false) { X "$inner
false
" } - if ($el.footer -eq $true) { X "$inner
true
" } - - if ($el.commandBarLocation) { - X "$inner$($el.commandBarLocation)" - } - if ($el.searchStringLocation) { - X "$inner$($el.searchStringLocation)" - } - if ($el.choiceMode -eq $true) { X "$innertrue" } - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). - if ($null -ne $el.autofill) { X "$inner$(if ($el.autofill){'true'}else{'false'})" } - if ($el.multipleChoice -eq $true) { X "$innertrue" } - if ($el.searchOnInput) { X "$inner$($el.searchOnInput)" } - if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } - # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) - if ($null -ne $el.headerHeight) { X "$inner$($el.headerHeight)" } - if ($null -ne $el.footerHeight) { X "$inner$($el.footerHeight)" } - if ($el.useAlternationRowColor -eq $true) { X "$innertrue" } - if ($el.selectionMode) { X "$inner$($el.selectionMode)" } - if ($el.rowSelectionMode) { X "$inner$($el.rowSelectionMode)" } - if ($el.verticalLines -eq $false) { X "$innerfalse" } - if ($el.horizontalLines -eq $false) { X "$innerfalse" } - if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } - # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) - Emit-PictureRef -val $el.rowsPicture -picTag 'RowsPicture' -indent $inner - # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) - if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } - # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) - if ($el.refreshRequest) { X "$inner$($el.refreshRequest)" } - # Блок свойств дин-список-таблицы (помечена эвристикой 11b.4) - if ($el.PSObject.Properties["_dynList"] -and $el._dynList) { Emit-DynListTableBlock -el $el -indent $inner } - if ($el.viewStatusLocation) { X "$inner$($el.viewStatusLocation)" } - if ($el.searchControlLocation) { X "$inner$($el.searchControlLocation)" } - Emit-Layout -el $el -indent $inner - - # CommandSet таблицы эмитится через Emit-Layout (общий механизм поля) - - # Оформление (цвета/граница таблицы) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - # AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто. - if ($null -ne $el.commandBar) { - Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar - } elseif ($null -ne $el.tableAutofill) { - $acbId = New-Id - X "$inner" - $afVal = if ($el.tableAutofill) { "true" } else { "false" } - X "$inner`t$afVal" - X "$inner" - } else { - Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner - } - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - $adds = $el.additions - Emit-TableAddition -typeKey 'searchString' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchString') - Emit-TableAddition -typeKey 'viewStatus' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'viewStatus') - Emit-TableAddition -typeKey 'searchControl' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchControl') - - # Columns - if ($el.columns -and $el.columns.Count -gt 0) { - X "$inner" - foreach ($col in $el.columns) { - Emit-Element -el $col -indent "$inner`t" - } - X "$inner" - } - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" - - X "$indent
" -} - -function Emit-Pages { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.pagesRepresentation) { - X "$inner$($el.pagesRepresentation)" - } - # Использование текущей строки (после PagesRepresentation, порядок XSD) - if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) заголовка группы страниц — TitleFont/TitleTextColor/… (как у Page) - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" - - # Children (pages) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Page { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). - # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. - Emit-PictureRef -val $el.picture -picTag 'Picture' -indent $inner - - if ($el.group) { - # Доступные значения страницы/обычной группы: Vertical / HorizontalIfPossible / AlwaysHorizontal - # (InCell — только у columnGroup). Horizontal/AlwaysVertical оставлены forgiving (legacy). - $orientation = switch ("$($el.group)") { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwaysHorizontal" { "AlwaysHorizontal" } - "alwaysVertical" { "AlwaysVertical" } - "horizontalIfPossible" { "HorizontalIfPossible" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - else { Warn-Unrecognized 'page group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name } - } - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - Emit-Layout -el $el -indent $inner - - # Оформление страницы (BackColor / TitleTextColor / TitleFont) — после ShowTitle, перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Button { - param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) - - X "$indent" -} - -function Emit-PictureDecoration { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-DecorationTitle -el $el -name $name -indent $inner - # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) - if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } - Emit-CommonFlags -el $el -indent $inner - - # Источник картинки — ТОЛЬКО $el.src (у PictureDecoration ключ 'picture' = тип/имя элемента, не источник). - # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . - if ($el.src) { - $srcStr = "$($el.src)" - $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } - X "$inner" - if ($srcStr -match '^abs:(.*)$') { X "$inner`t$(Esc-Xml $matches[1])" } - else { X "$inner`t$(Esc-Xml $srcStr)" } - X "$inner`t$lt" - if ($el.transparentPixel) { X "$inner`t" } - X "$inner" - } - - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - # EnableDrag — фактическое значение (декорация-картинка перетаскиваема; декомпилятор ловит generic-ом) - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - - # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) - Emit-Appearance -el $el -indent $inner -profile 'decoration' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" - - X "$indent" -} - -function Emit-PictureField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.hyperlink -eq $true) { X "$innertrue" } - - Emit-Layout -el $el -indent $inner - # EnableDrag — фактическое значение (поле картинки перетаскиваемо; декомпилятор ловит generic-ом) - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) - if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # ValuesPicture — picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. - Emit-PictureRef -val $el.valuesPicture -picTag 'ValuesPicture' -indent $inner - if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" - - X "$indent" -} - -function Emit-Calendar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - "auto" { "Auto" } - default { "$($el.titleLocation)" } - } - X "$inner$loc" - } - - Emit-Layout -el $el -indent $inner - - # Календарно-специфичные свойства (порядок схемы: после layout, до companions) - if ($el.selectionMode) { X "$inner$($el.selectionMode)" } - if ($null -ne $el.showCurrentDate) { $v = if ($el.showCurrentDate) { "true" } else { "false" }; X "$inner$v" } - if ($null -ne $el.widthInMonths) { X "$inner$($el.widthInMonths)" } - if ($null -ne $el.heightInMonths) { X "$inner$($el.heightInMonths)" } - if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner$v" } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" - - X "$indent" -} - -# Спец-поля «документ/датчик» (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): -# единый скелет поля (path/title/flags/titleLocation/editMode/layout/companions/events). -# Типоспец. enum/bool скаляры — через generic (Emit-Layout→Emit-GenericScalars); -# числовые скаляры датчиков (min/max/шаги) — без xsi:type (≠ типизированных InputField). -# enableDrag/enableStartDrag — общие (Emit-CommonElementProps), фактическое значение. -function Emit-SimpleField { - param($el, [string]$name, [int]$id, [string]$indent, [string]$xmlTag, [string]$typeKey) - - X "$indent<$xmlTag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.editMode) { X "$inner$($el.editMode)" } - - Emit-Layout -el $el -indent $inner - - # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через Emit-Layout. - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - - # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) - foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'), @('largeStep','LargeStep'), @('markingStep','MarkingStep'), @('step','Step'))) { - if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$($el.($p[0]))" } - } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey $typeKey - - X "$indent" -} - -# GanttChartField — скелет поля + вложенная (полноценная таблица, через Emit-Element). -# Порядок (по корпусу): path/title/flags/titleLocation/layout/appearance, companions, Table, events. -function Emit-GanttChart { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - Emit-Layout -el $el -indent $inner - Emit-Appearance -el $el -indent $inner -profile 'field' - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем Emit-Element) - if ($el.ganttTable) { Emit-Element -el $el.ganttTable -indent $inner } - Emit-Events -el $el -elementName $name -indent $inner -typeKey "ganttChart" - X "$indent" -} - -function Emit-CommandBar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.commandSource) { X "$inner$($el.commandSource)" } - - if ($el.autofill -eq $true) { X "$innertrue" } - - # CommandBar хранит HorizontalLocation фактически (включая Auto — декомпилятор ловит только при наличии); - # ≠ дополнениям, где Auto = умолчание-скип (Get-HLocation). - if ($el.horizontalLocation) { - $hlv = switch ("$($el.horizontalLocation)".ToLower()) { 'auto' {'Auto'} 'left' {'Left'} 'right' {'Right'} 'center' {'Center'} default {"$($el.horizontalLocation)"} } - X "$inner$hlv" - } - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-ButtonGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.commandSource) { X "$inner$($el.commandSource)" } - - if ($el.representation) { - X "$inner$($el.representation)" - } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children (кнопки в контексте командной панели) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-Popup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - # Источник команд попапа (после Title/ToolTip, перед компаньоном) — как у ButtonGroup/CommandBar - if ($el.commandSource) { X "$inner$($el.commandSource)" } - - Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - Emit-Layout -el $el -indent $inner - - # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -# --- 8. Attribute emitter --- - -# FunctionalOption.X — у Attribute/Command/Column. -# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. -function Emit-FunctionalOptions { - param($fo, [string]$indent) - if (-not $fo -or @($fo).Count -eq 0) { return } - X "$indent" - foreach ($opt in @($fo)) { - $v = "$opt" - if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть - elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом - else { $v = "FunctionalOption.$v" } - X "$indent`t$v" - } - X "$indent" -} - -# Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. -function Emit-AttrColumn { - param($col, [string]$indent) - $colId = New-Id - X "$indent" - if ($col.title) { Emit-MLText -tag "Title" -text $col.title -indent "$indent`t" } - Emit-Type -typeStr "$($col.type)" -indent "$indent`t" - # Проверка заполнения колонки → (как у реквизита; bool true→ShowError / строка verbatim) - $cfcRaw = if ($null -ne $col.PSObject.Properties['fillCheck']) { $col.fillCheck } elseif ($null -ne $col.PSObject.Properties['fillChecking']) { $col.fillChecking } else { $null } - if ($null -ne $cfcRaw) { $cfcv = if ($cfcRaw -is [bool]) { if ($cfcRaw) { 'ShowError' } else { $null } } else { "$cfcRaw" }; if ($cfcv) { X "$indent`t$cfcv" } } - Emit-FunctionalOptions -fo $col.functionalOptions -indent "$indent`t" - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита - if ($null -ne $col.view) { Emit-XrFlag -tag 'View' -val $col.view -indent "$indent`t" } - if ($null -ne $col.edit) { Emit-XrFlag -tag 'Edit' -val $col.edit -indent "$indent`t" } - X "$indent" -} - -# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- -# Та же сущность, что параметры СКД (см. skd-compile), но в форме: обёртка -# + дети с префиксом dcssch:. DSL переиспользует грамматику параметров СКД (shorthand + -# объект). Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега»): -# useRestriction — эмитим ВСЕГДА, дефолт TRUE (в СКД дефолт false); -# title — авто из имени (Title-FromName), если ключ не задан. -# Канон. порядок детей (по корпусу acc/erp 8.3.24): name, title, valueType, value, -# useRestriction, expression, availableValue*, valueListAllowed, availableAsField, -# inputParameters, denyIncompleteValues, use. - -# Многоязычный текст в DCS-контексте — с xsi:type="v8:LocalStringType" (form-compile -# Emit-MLText его НЕ ставит, т.к. формовые голые; в dcssch:* — обязателен). -function Emit-DLMLText { - param([string]$tag, $text, [string]$indent) - X "$indent<$tag xsi:type=`"v8:LocalStringType`">" - Emit-MLItems -val $text -indent "$indent`t" - X "$indent</$tag>" -} - -function Has-DLProp { - param($obj, [string]$name) - if ($null -eq $obj) { return $false } - if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } - if ($obj.PSObject -and $obj.PSObject.Properties[$name]) { return $true } - return $false -} - -function Split-DLValueListCsv { - param([string]$s) - $result = @() - if ($null -eq $s) { return ,$result } - $items = @(); $buf = New-Object System.Text.StringBuilder; $inQuote = $null - for ($i = 0; $i -lt $s.Length; $i++) { - $ch = $s[$i] - if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } - elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } - elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } - else { [void]$buf.Append($ch) } - } - if ($buf.Length -gt 0) { $items += $buf.ToString() } - foreach ($raw in $items) { - $t = $raw.Trim() - if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } - if ($t -ne "") { $result += $t } - } - return ,$result -} - -# Shorthand: "Имя [Заголовок]: Тип = Значение @valueList @hidden" -function Parse-DLParamShorthand { - param([string]$s) - $result = @{ name = ""; type = ""; value = $null; title = $null } - if ($s -match '@valueList') { $result.valueListAllowed = $true; $s = $s -replace '\s*@valueList', '' } - if ($s -match '@hidden') { $result.hidden = $true; $s = $s -replace '\s*@hidden', '' } - if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim(); $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } - # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). - if ($s -match '^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$') { - $result.name = $Matches[1].Trim() - $typeRaw = $Matches[2].Trim() - if ($typeRaw -match '[|+]') { - $result.type = (($typeRaw -split '\s*[|+]\s*') | ForEach-Object { Resolve-TypeStr ($_.Trim()) }) -join ' | ' - } else { - $result.type = Resolve-TypeStr $typeRaw - } - if ($Matches[4]) { - $rhs = $Matches[4].Trim() - $items = Split-DLValueListCsv $rhs - if ($items.Count -ge 2) { $result.value = $items; $result.valueListAllowed = $true } - elseif ($items.Count -eq 1) { $result.value = $items[0] } - else { $result.value = $rhs } - } - } else { $result.name = $s.Trim() } - return $result -} - -function Test-DLEmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "" -or $s -eq "_" -or $s.ToLowerInvariant() -eq "null") { return $true } - return $false -} - -# Эмиссия <dcssch:value> по типу/значению (xs:* или dcscor:DesignTimeValue для ссылок). -function Emit-DLValue { - param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) - if (Test-DLEmptyValue $val) { - # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil, даже при известном - # типе (в отличие от типизированного пустого в параметрах отчёта СКД). - if ($valueListAllowed) { return } - X "$indent<dcssch:value xsi:nil=`"true`"/>" - return - } - $valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" } - if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type - elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } - else { - if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } - else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } - } -} - -# <dcssch:valueType> — обёртка + тело через Emit-SingleType (ref-типы → cfg:, как в форме). -function Emit-DLValueType { - param($typeStr, [string]$indent) - if (-not $typeStr) { return } - X "$indent<dcssch:valueType>" - $parts = "$typeStr" -split '\s*[|+]\s*' - foreach ($part in $parts) { Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" } - X "$indent</dcssch:valueType>" -} - -function Emit-DLAvailableValue { - param($av, [string]$type, [string]$indent) - X "$indent<dcssch:availableValue>" - $avVal = if (Has-DLProp $av 'value') { $av.value } else { $null } - Emit-DLValue -type $type -val $avVal -indent "$indent`t" -valueListAllowed $false - $pres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { $null } - if ($pres) { Emit-DLMLText -tag "dcssch:presentation" -text $pres -indent "$indent`t" } - X "$indent</dcssch:availableValue>" -} - -# <dcssch:inputParameters> — ChoiceParameters / ChoiceParameterLinks / простое значение (порт из skd). -function Emit-DLInputParameters { - param($ip, [string]$indent) - if ($null -eq $ip) { return } - $items = @($ip) - if ($items.Count -eq 0) { return } - X "$indent<dcssch:inputParameters>" - foreach ($item in $items) { - X "$indent`t<dcscor:item>" - if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" - if (Has-DLProp $item 'choiceParameters') { - $cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() } - if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" } - else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" - foreach ($cpItem in $cpItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" - foreach ($v in @($cpItem.values)) { - if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" } - else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" } - } - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-DLProp $item 'choiceParameterLinks') { - $cplItems = if ($null -ne $item.choiceParameterLinks) { @($item.choiceParameterLinks) } else { @() } - if ($cplItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" } - else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" - foreach ($cplItem in $cplItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" - X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" - $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } - X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-DLProp $item 'typeLink') { - # Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра). - $tl = $item.typeLink - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">" - $tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" } - $tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" } - X "$indent`t`t</dcscor:value>" - } elseif (Has-DLProp $item 'value') { - $val = $item.value - if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" } - elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" } - elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } - else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" } - } - X "$indent`t</dcscor:item>" - } - X "$indent</dcssch:inputParameters>" -} - -# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ── -# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект -# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}. -function Test-EmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "") { return $true } - if ($s -eq "_") { return $true } - if ($s.ToLowerInvariant() -eq "null") { return $true } - return $false -} -function Emit-EmptyValue { - param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) - if ($valueListAllowed) { return } - $t = if ($null -eq $type) { "" } else { "$type" } - $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } - $pf = $tagPrefix - if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } - elseif ($t -eq "StandardPeriod") { - X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" - X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - X "$indent</${pf}value>" - } - elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } - elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" } - elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" } - elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" } - else { X "$indent<${pf}value xsi:nil=`"true`"/>" } -} -function Parse-DataParamShorthand { - param([string]$s) - $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } - if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } - if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } - if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } - if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } - $s = $s.Trim() - if ($s -match '^([^=]+)=\s*(.+)$') { - $result.parameter = $Matches[1].Trim() - $valStr = $Matches[2].Trim() - $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") - if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } - elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } - elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } - else { $result.value = $valStr } - } else { $result.parameter = $s } - return $result -} -function Emit-DataParameters { - param($items, [string]$indent, $blockViewMode = $null) - if (-not $items -or @($items).Count -eq 0) { return } - X "$indent<dcsset:dataParameters>" - foreach ($dp in @($items)) { - if ($dp -is [string]) { - $parsed = Parse-DataParamShorthand $dp - $dpObj = New-Object PSObject - $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter - if ($null -ne $parsed.value) { $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value } - if ($parsed.use -eq $false) { $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false } - if ($parsed.userSettingID) { $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID } - if ($parsed.viewMode) { $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode } - $dp = $dpObj - } - X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" - $dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string]) - if ($dpValIsArr) { - # Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое. - $avtype = "$($dp.valueType)" - foreach ($v in @($dp.value)) { - $vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" } - if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" } - elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" } - else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" } - } - } elseif ($dp.nilValue -eq $true) { - X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" - } elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) { - # Явный типизированный пустой (xs:string-плейсхолдер и т.п.) - Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false - } elseif (Test-EmptyValue $dp.value) { - # Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер). - # (В отличие от skd-settings, где значение всегда присутствует.) - } elseif ($null -ne $dp.value) { - $vtype = "$($dp.valueType)" - if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) { - $_hasDate = $false; $_hasSD = $false - if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] } - else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') } - $_variantStr = "$($dp.value.variant)" - $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') - if ($_isSBD) { - $_d = $null - if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } - elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" } - X "$indent`t`t</dcscor:value>" - } else { - $_sd = $null; $_ed = $null - if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } - else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" } - X "$indent`t`t</dcscor:value>" - } - } elseif ($vtype -match '^[a-zA-Z]+:') { - $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } - X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" - } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { - X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>" - } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^decimal') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^string') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } - } - if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" } - if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" } - if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } - X "$indent`t</dcscor:item>" - } - if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } - X "$indent</dcsset:dataParameters>" -} - -function Emit-DLParameter { - param($p, $parsed, [string]$indent) - X "$indent<Parameter>" - $ci = "$indent`t" - X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>" - # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. - $title = $null - if ($parsed.title) { $title = $parsed.title } - elseif ($p -isnot [string] -and (Has-DLProp $p 'title') -and $p.title) { $title = $p.title } - elseif ($p -isnot [string] -and (Has-DLProp $p 'presentation') -and $p.presentation) { $title = $p.presentation } - if ($null -eq $title -or ($title -is [string] -and $title -eq '')) { $title = Title-FromName -name $parsed.name } - Emit-DLMLText -tag "dcssch:title" -text $title -indent $ci - # valueType - if ($parsed.type) { Emit-DLValueType -typeStr $parsed.type -indent $ci } - # value (дефолт nil; при valueListAllowed пустое — опускаем) - $vla = [bool]$parsed.valueListAllowed - $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) - if ($valIsArray) { - foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false } - } elseif ($parsed.valueExplicit -and ($null -ne $parsed.value) -and ("$($parsed.value)" -eq '') -and (("$($parsed.type)" -eq '') -or ("$($parsed.type)" -match '^string'))) { - # Явный пустой СТРОКОВЫЙ параметр (value:"" от декомпилятора) → типизированный пустой - # <dcssch:value xsi:type="xs:string"/>, НЕ nil. Решается ФОРМОЙ value (""→typed-empty, - # null/отсутствие→nil), независимо от valueListAllowed; декомпилятор различает ""/null - # (Convert-TypedValue пустого xs:string → "", nil → value опущен/null). Корпус: 26 xs:string. - X "$ci<dcssch:value xsi:type=`"xs:string`"/>" - } elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) { - # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil - X "$ci<dcssch:value xsi:nil=`"true`"/>" - } else { - Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla - } - # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. - $ur = $true - if ($p -isnot [string] -and (Has-DLProp $p 'useRestriction')) { $ur = [bool]$p.useRestriction } - X "$ci<dcssch:useRestriction>$(if ($ur) { 'true' } else { 'false' })</dcssch:useRestriction>" - # expression - $expr = $null - if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" } - if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" } - # availableValues - if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) { - foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci } - } - # valueListAllowed - if ($vla) { X "$ci<dcssch:valueListAllowed>true</dcssch:valueListAllowed>" } - # availableAsField=false (hidden или явный) - $aaf = $null - if ($parsed.hidden -eq $true) { $aaf = $false } - if ($p -isnot [string] -and (Has-DLProp $p 'availableAsField')) { $aaf = [bool]$p.availableAsField } - if ($aaf -eq $false) { X "$ci<dcssch:availableAsField>false</dcssch:availableAsField>" } - # inputParameters - if ($p -isnot [string] -and (Has-DLProp $p 'inputParameters') -and $p.inputParameters) { Emit-DLInputParameters -ip $p.inputParameters -indent $ci } - # denyIncompleteValues - if ($p -isnot [string] -and (Has-DLProp $p 'denyIncompleteValues') -and $p.denyIncompleteValues -eq $true) { X "$ci<dcssch:denyIncompleteValues>true</dcssch:denyIncompleteValues>" } - # use - $useVal = $null - if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" } - if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" } - X "$indent</Parameter>" -} - -function Emit-DLParameters { - param($params, [string]$indent) - if (-not $params) { return } - foreach ($p in @($params)) { - if ($p -is [string]) { - $parsed = Parse-DLParamShorthand $p - } else { - $resolvedType = "" - if ((Has-DLProp $p 'type') -and $p.type) { - if ($p.type -is [array] -or ($p.type -is [System.Collections.IList] -and $p.type -isnot [string])) { - $resolvedType = (@($p.type | ForEach-Object { Resolve-TypeStr "$_" })) -join ' | ' - } else { $resolvedType = Resolve-TypeStr "$($p.type)" } - } elseif ((Has-DLProp $p 'valueType') -and $p.valueType) { - $resolvedType = Resolve-TypeStr "$($p.valueType)" - } - $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null } - if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } - if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true } - } - Emit-DLParameter -p $p -parsed $parsed -indent $indent - } -} - -function Emit-Attributes { - param($attrs, [string]$indent, $conditionalAppearance = $null) - - $hasCA = $conditionalAppearance -and @($conditionalAppearance).Count -gt 0 - # Платформа ВСЕГДА эмитит <Attributes> (100% корпуса; 162 формы — пустой <Attributes/>). - if ((-not $attrs -or $attrs.Count -eq 0) -and -not $hasCA) { X "$indent<Attributes/>"; return } - if (-not $attrs -or $attrs.Count -eq 0) { - # Нет реквизитов, но есть условное оформление (последний child <Attributes>) - X "$indent<Attributes>" - Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' - X "$indent</Attributes>" - return - } - - X "$indent<Attributes>" - $seenAttrs = @{} - foreach ($attr in $attrs) { - $attrId = New-Id - $attrName = "$($attr.name)" - Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute' - - X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">" - $inner = "$indent`t`t" - - # Title атрибута (зеркало Emit-Title): нет ключа → авто-вывод из имени (кроме main); - # title "" → подавить; непустой → эмитить как есть. - $hasTitleKey = $null -ne $attr.PSObject.Properties['title'] - if ($hasTitleKey) { - if ($attr.title) { Emit-MLText -tag "Title" -text $attr.title -indent $inner } - } elseif ($attr.main -ne $true) { - Emit-MLText -tag "Title" -text (Title-FromName -name $attrName) -indent $inner - } - - # Type - if ($attr.type) { - Emit-Type -typeStr "$($attr.type)" -indent $inner - } else { - X "$inner<Type/>" - } - # valueType: уточнение типа значений ValueList → <Settings xsi:type="v8:TypeDescription"> - # (та же грамматика типа, что и Type, включая составной "A | B"). Forgiving-синонимы. - # Три состояния: нет ключа → нет Settings; "" → пустой <Settings…/>; тип → с типом. - $vtSpec = $null; $hasVt = $false - foreach ($k in @('valueType','typeDescription','описаниеТипов','типЗначений')) { - if ($attr.PSObject.Properties[$k]) { $vtSpec = $attr.$k; $hasVt = $true; break } - } - if ($hasVt) { - Emit-Type -typeStr "$vtSpec" -indent $inner -tag "Settings" -tagAttrs ' xsi:type="v8:TypeDescription"' - } - # Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика). - # Идёт сразу после <Type> (как valueType/DynamicList Settings — взаимоисключающи). - if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) { - Emit-PlannerSettings -pl $attr.planner -ind $inner - } - # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart">. - # Тип Settings выводится из типа реквизита (d5p1:GanttChart → d4p1:GanttChart). - if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) { - $ctype = if ("$($attr.type)" -match 'GanttChart') { 'd4p1:GanttChart' } else { 'd4p1:Chart' } - Emit-ChartSettings -chart $attr.chart -ind $inner -ctype $ctype - } - - if ($attr.main -eq $true) { - X "$inner<MainAttribute>true</MainAttribute>" - } - # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) - if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner } - if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner } - $mainSaved = $false - if ($attr.main -eq $true -and $attr.type) { - $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' - } - # Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто. - $emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved } - if ($emitSaved) { - X "$inner<SavedData>true</SavedData>" - } - # Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>; - # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). - # Нет ключа или false → не эмитим. - if ($null -ne $attr.PSObject.Properties['save'] -and $null -ne $attr.save) { - $saveFields = New-Object System.Collections.ArrayList - if ($attr.save -is [bool]) { - if ($attr.save) { [void]$saveFields.Add($attrName) } - } else { - foreach ($e in @($attr.save)) { - $fld = "$e" - if ([string]::IsNullOrEmpty($fld)) { continue } - if ($fld -ne $attrName -and $fld -notmatch '\.' -and $fld -notmatch '^\d+/\d+') { $fld = "$attrName.$fld" } - if (-not $saveFields.Contains($fld)) { [void]$saveFields.Add($fld) } - } - } - if ($saveFields.Count -gt 0) { - X "$inner<Save>" - foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" } - X "$inner</Save>" - } - } - # Проверка заполнения реквизита → <FillCheck> (реальный тег; <FillChecking> в схеме нет). - # bool true → ShowError (единственное значение в корпусе); строка → verbatim. Синоним fillChecking. - $fcRaw = if ($null -ne $attr.PSObject.Properties['fillCheck']) { $attr.fillCheck } elseif ($null -ne $attr.PSObject.Properties['fillChecking']) { $attr.fillChecking } else { $null } - if ($fcRaw) { - $fcv = if ($fcRaw -is [bool]) { 'ShowError' } else { "$fcRaw" } - X "$inner<FillCheck>$fcv</FillCheck>" - } - - # UseAlways: поля, всегда читаемые (дин-список/таблица). Две формы DSL сливаются: - # attr.useAlways[] (короткие имена) + columns с useAlways:true → <Field>ИмяРеквизита.Поле</Field>. - $uaFields = New-Object System.Collections.ArrayList - if ($attr.useAlways) { - foreach ($e in @($attr.useAlways)) { - $fld = "$e" - # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" - # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. - # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). - if ($fld.StartsWith('~')) { - $bare = $fld.Substring(1) - if ($bare -notmatch "^$([regex]::Escape($attrName))\.") { $bare = "$attrName.$bare" } - $fld = "~$bare" - } elseif ($fld -notmatch "^$([regex]::Escape($attrName))\." -and $fld -notmatch '^\d+/\d+') { - # UUID-ссылка (1/0:GUID) — НЕ префиксуем (платформа хранит её без "имя.") - $fld = "$attrName.$fld" - } - if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } - } - } - if ($attr.columns) { - foreach ($col in $attr.columns) { - if ($col.useAlways -eq $true) { - $fld = "$attrName.$($col.name)" - if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } - } - } - } - if ($uaFields.Count -gt 0) { - X "$inner<UseAlways>" - foreach ($f in $uaFields) { X "$inner`t<Field>$f</Field>" } - X "$inner</UseAlways>" - } - - Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner - - # Columns: прямые <Column> (ValueTable/Tree) + <AdditionalColumns table="X"> (доп. колонки - # табличных частей объекта). Порядок схемы: прямые сначала, затем AdditionalColumns-группы. - # Для дин-списка (есть settings) прямые колонки НЕ эмитим (служат лишь для UseAlways). - $hasDirectCols = $attr.columns -and $attr.columns.Count -gt 0 -and -not $attr.settings - $hasAddCols = $attr.additionalColumns -and @($attr.additionalColumns).Count -gt 0 - if ($hasDirectCols -or $hasAddCols) { - X "$inner<Columns>" - if ($hasDirectCols) { - $seenCols = @{} # колонки уникальны в пределах своего реквизита - foreach ($col in $attr.columns) { - Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'" - Emit-AttrColumn -col $col -indent "$inner`t" - } - } - if ($hasAddCols) { - foreach ($ac in @($attr.additionalColumns)) { - $acCols = @($ac.columns) - if ($acCols.Count -eq 0) { - # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) - X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>" - continue - } - X "$inner`t<AdditionalColumns table=`"$($ac.table)`">" - $seenAcCols = @{} # уникальность в пределах группы AdditionalColumns - foreach ($col in $acCols) { - Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'" - Emit-AttrColumn -col $col -indent "$inner`t`t" - } - X "$inner`t</AdditionalColumns>" - } - } - X "$inner</Columns>" - } - - # Settings (динамический список) - if ($attr.settings) { - $st = $attr.settings - X "$inner<Settings xsi:type=`"DynamicList`">" - $si = "$inner`t" - # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). - if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" } - $hasQuery = $st.query -and "$($st.query)".Trim() - # Явный ключ manualQuery (в т.ч. false) ПОБЕЖДАЕТ эвристику hasQuery (платформа изредка - # хранит QueryText при ManualQuery=false — декомпилятор фиксирует это отклонение). - $hasMQKey = ($st.PSObject.Properties['manualQuery']) -and ($null -ne $st.manualQuery) - $mq = if ($hasMQKey) { if ($st.manualQuery) { "true" } else { "false" } } elseif ($hasQuery) { "true" } else { "false" } - X "$si<ManualQuery>$mq</ManualQuery>" - # DynamicDataRead: дефолт true; false только при явном отключении - $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" } - X "$si<DynamicDataRead>$ddr</DynamicDataRead>" - if ($hasQuery) { - $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir - X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>" - } - # Явные поля набора (редко): override title/dataPath - if ($st.fields) { - foreach ($fld in $st.fields) { - # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet - # (поле-вложенный набор = реквизит табличной части; маркер nested). - # folder = папка-группировка полей (DataSetFieldFolder, без <field>); nested = вложенный набор. - $isFolder = [bool](Get-Prop $fld 'folder') - $ftype = if ($fld.nested) { "DataSetFieldNestedDataSet" } elseif ($isFolder) { "DataSetFieldFolder" } else { "DataSetFieldField" } - X "$si<Field xsi:type=`"dcssch:$ftype`">" - # dataPath: явный (включая пустой "" → self-closing <dcssch:dataPath/>) побеждает; иначе fallback на field. - if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" } - elseif ($isFolder) { $dp = "" } - else { $dp = "$($fld.field)" } - if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" } - if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" } - if ($fld.title) { - X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">" - Emit-MLItems -val $fld.title -indent "$si`t`t" - X "$si`t</dcssch:title>" - } - # Ограничения использования поля — после title, перед presentationExpression (порядок исходника) - Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t" - Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t" - # presentationExpression поля — перед valueType (порядок исходника) - if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" } - # valueType поля набора (тип значения; вычисляемые/кастомные поля) - if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" } - # appearance поля (формат/оформление) — после valueType (порядок исходника) - if ($fld.appearance) { - X "$si`t<dcssch:appearance>" - foreach ($prop in $fld.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$si`t`t" } - X "$si`t</dcssch:appearance>" - } - # inputParameters поля (связь по параметрам выбора) — в конце - if ($fld.inputParameters) { Emit-DLInputParameters -ip $fld.inputParameters -indent "$si`t" } - X "$si</Field>" - } - } - # Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*. - Emit-CalcFields -calcFields $st.calculatedFields -indent $si - # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. - Emit-DLParameters -params $st.parameters -indent $si - # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) - # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. - if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" } - if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } } - if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" } - # GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false). - if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" } - # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). - if ($null -ne $st.autoSaveUserSettings) { X "$si<AutoSaveUserSettings>$(if ($st.autoSaveUserSettings){'true'}else{'false'})</AutoSaveUserSettings>" } - # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. - # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. - $lsi = "$si`t" - $lsOpenLen = $script:xml.Length - X "$si<ListSettings>" - $lsAfterOpenLen = $script:xml.Length # для self-closing, если внутри ничего не эмитнётся - if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) { - # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. - # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. - foreach ($prop in $st.listSettings.PSObject.Properties) { - $tag = $prop.Name; $pv = $prop.Value - # Значение дескриптора: строка-код "vu" ИЛИ объект { meta:"vu", presentation:<текст/ML> } - # (контейнер несёт собственный userSettingPresentation — кастомную подпись настройки). - if (($pv -is [PSCustomObject]) -or ($pv -is [System.Collections.IDictionary])) { - $meta = "$(Get-Prop $pv 'meta')"; $bpres = Get-Prop $pv 'presentation' - } else { $meta = "$pv"; $bpres = $null } - $bvm = if ($meta -match 'v') { 'Normal' } else { $null } - switch ($tag) { - 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } - 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } - 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } - 'itemsViewMode' { X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" } - 'itemsUserSettingID' { X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" } - 'itemsUserSettingPresentation' { Emit-USPresentation -val $pv -tag "dcsset:itemsUserSettingPresentation" -indent $lsi } - 'dataParameters' { Emit-DataParameters -items $st.dataParameters -indent $lsi } - 'structure' { Emit-ListGrouping (Get-ListGroupingValue $st) $lsi } - } - } - } else { - # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. - Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID - # dataParameters — после filter, до order (XSD-порядок ListSettings) - if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi } - Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID - Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID - # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode - Emit-ListGrouping (Get-ListGroupingValue $st) $lsi - X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" - X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" - } - if ($script:xml.Length -eq $lsAfterOpenLen) { - # Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing. - $script:xml.Length = $lsOpenLen - X "$si<ListSettings/>" - } else { - X "$si</ListSettings>" - } - X "$inner</Settings>" - } - - X "$indent`t</Attribute>" - } - # Условное оформление формы — последний child <Attributes> (та же DCS-грамматика, что settings CA) - Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' - X "$indent</Attributes>" -} - -# --- 9. Parameter emitter --- - -function Emit-Parameters { - param($params, [string]$indent) - - if (-not $params -or $params.Count -eq 0) { return } - - X "$indent<Parameters>" - $seenParams = @{} - foreach ($param in $params) { - Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter' - X "$indent`t<Parameter name=`"$($param.name)`">" - $inner = "$indent`t`t" - - Emit-Type -typeStr "$($param.type)" -indent $inner - - if ($param.key -eq $true) { - X "$inner<KeyParameter>true</KeyParameter>" - } - - X "$indent`t</Parameter>" - } - X "$indent</Parameters>" -} - -# --- 10. Command emitter --- - -function Emit-Commands { - param($cmds, [string]$indent) - - if (-not $cmds -or $cmds.Count -eq 0) { return } - - X "$indent<Commands>" - $seenCmds = @{} - foreach ($cmd in $cmds) { - $cmdId = New-Id - Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command' - X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">" - $inner = "$indent`t`t" - - # Заголовок команды (зеркало Emit-Title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс - # (в оригинале <Title> нет — не додумывать); ключ отсутствует → авто-вывод из имени (помощь модели). - if ($null -ne $cmd.PSObject.Properties['title']) { - if ($cmd.title) { Emit-MLText -tag "Title" -text $cmd.title -indent $inner } - } else { - $cmdTitle = Title-FromName -name "$($cmd.name)" - if ($cmdTitle) { Emit-MLText -tag "Title" -text $cmdTitle -indent $inner } - } - - if ($cmd.tooltip) { - Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner - } - - # Доступность команды по ролям (после ToolTip, до Action) - if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner } - - if ($cmd.action) { - X "$inner<Action>$($cmd.action)</Action>" - } - - if ($cmd.modifiesSavedData -eq $true) { X "$inner<ModifiesSavedData>true</ModifiesSavedData>" } - - Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner - - if ($cmd.currentRowUse) { - X "$inner<CurrentRowUse>$($cmd.currentRowUse)</CurrentRowUse>" - } - - # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). - # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) - $cmdTable = $cmd.table - if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId } - if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица } - if ($cmdTable) { - X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>" - } - - if ($cmd.shortcut) { - X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" - } - - Emit-CommandPicture -pic $cmd.picture -elemLt $cmd.loadTransparent -indent $inner - - if ($cmd.representation) { - X "$inner<Representation>$($cmd.representation)</Representation>" - } - - X "$indent`t</Command>" - } - X "$indent</Commands>" -} - -# Резолв ключа-группы древовидной формы → CommandGroup (зависит от панели). Дружелюбные -# алиасы стандартных form-групп; любой иной ключ (CommandGroup.X / GUID / "") — verbatim. -function Resolve-CommandGroupKey { - param([string]$key, [string]$panelTag) - $k = ($key -replace '\s','').ToLower() - if ($panelTag -eq 'NavigationPanel') { - switch ($k) { - 'important' { return 'FormNavigationPanelImportant' } - 'важное' { return 'FormNavigationPanelImportant' } - 'goto' { return 'FormNavigationPanelGoTo' } - 'перейти' { return 'FormNavigationPanelGoTo' } - 'seealso' { return 'FormNavigationPanelSeeAlso' } - 'смтакже' { return 'FormNavigationPanelSeeAlso' } - } - } else { - switch ($k) { - 'important' { return 'FormCommandBarImportant' } - 'важное' { return 'FormCommandBarImportant' } - 'createbasedon' { return 'FormCommandBarCreateBasedOn' } - 'создатьнаосновании' { return 'FormCommandBarCreateBasedOn' } - } - } - return $key # verbatim -} - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. -# Значение панели: МАССИВ (плоская форма; элемент может нести group) ИЛИ ОБЪЕКТ -# (древовидная форма: {группа: [команды]}, group берётся из ключа, элементы его не дублируют). -# Элемент: строка (голый command, Type=Auto) или объект. Порядок тегов: -# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). -function Emit-CommandInterface { - param($ci, [string]$indent) - if (-not $ci) { return } - $inner = "$indent`t" - $panels = @( - @{ Tag='CommandBar'; Syns=@('commandBar','команднаяПанель','КоманднаяПанель') }, - @{ Tag='NavigationPanel'; Syns=@('navigationPanel','панельНавигации','ПанельНавигации') } - ) - $present = @() - foreach ($p in $panels) { - $items = $null - foreach ($syn in $p.Syns) { if ($null -ne $ci.PSObject.Properties[$syn]) { $items = $ci.($syn); break } } - if ($null -ne $items) { $present += ,@{ Tag=$p.Tag; Items=$items } } - } - if ($present.Count -eq 0) { return } - X "$indent<CommandInterface>" - foreach ($p in $present) { - X "$inner<$($p.Tag)>" - # Нормализация: плоский список пар (элемент, group-из-дерева). Объект → дерево. - $flat = New-Object System.Collections.ArrayList - if ($p.Items -is [System.Management.Automation.PSCustomObject]) { - foreach ($prop in $p.Items.PSObject.Properties) { - $grpFromTree = Resolve-CommandGroupKey -key $prop.Name -panelTag $p.Tag - foreach ($it in @($prop.Value)) { [void]$flat.Add(@{ item=$it; treeGroup=$grpFromTree }) } - } - } else { - foreach ($it in @($p.Items)) { [void]$flat.Add(@{ item=$it; treeGroup=$null }) } - } - foreach ($fi in $flat) { - $item = $fi.item; $treeGroup = $fi.treeGroup - if ($item -is [string]) { - $cmd = $item; $type = 'Auto'; $attr = $null; $grp = $null; $idx = $null; $dv = $null; $vis = $null - } else { - $cmd = Get-ElProp $item @('command','команда') - $type = Get-ElProp $item @('type','тип'); if (-not $type) { $type = 'Auto' } - $attr = Get-ElProp $item @('attribute','реквизит') - $grp = Get-ElProp $item @('group','группа','группаКоманд') - $idx = Get-ElProp $item @('index','индекс') - $dv = Get-ElProp $item @('defaultVisible','видимость','видимостьПоУмолчанию') - $vis = Get-ElProp $item @('visible','видимостьПоРолям','настройкаВидимости') - } - # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк - if ($treeGroup) { $grp = $treeGroup } - X "$inner`t<Item>" - X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>" - X "$inner`t`t<Type>$type</Type>" - if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" } - if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" } - if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" } - if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" } - if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" } - X "$inner`t</Item>" - } - X "$inner</$($p.Tag)>" - } - X "$indent</CommandInterface>" -} - -# --- 11. Properties emitter --- - -function Emit-Properties { - param($props, [string]$indent) - - if (-not $props) { return } - - # camelCase -> PascalCase mapping for known properties - $propMap = @{ - "autoTitle" = "AutoTitle" - "windowOpeningMode" = "WindowOpeningMode" - "commandBarLocation" = "CommandBarLocation" - "saveDataInSettings" = "SaveDataInSettings" - "autoSaveDataInSettings" = "AutoSaveDataInSettings" - "autoTime" = "AutoTime" - "usePostingMode" = "UsePostingMode" - "repostOnWrite" = "RepostOnWrite" - "autoURL" = "AutoURL" - "autoFillCheck" = "AutoFillCheck" - "customizable" = "Customizable" - "enterKeyBehavior" = "EnterKeyBehavior" - "verticalScroll" = "VerticalScroll" - "scalingMode" = "ScalingMode" - "useForFoldersAndItems" = "UseForFoldersAndItems" - "reportResult" = "ReportResult" - "detailsData" = "DetailsData" - "reportFormType" = "ReportFormType" - "autoShowState" = "AutoShowState" - "width" = "Width" - "height" = "Height" - "group" = "Group" - } - - foreach ($p in $props.PSObject.Properties) { - $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { - # Auto PascalCase: first letter uppercase - $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) - } - # Convert boolean to lowercase string (PS renders as True/False) - $val = $p.Value - # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) - if ($val -is [string] -and $val -eq '') { continue } - if ($val -is [bool]) { - $val = if ($val) { "true" } else { "false" } - } - X "$indent<$xmlName>$val</$xmlName>" - } -} - -# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - -# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый -# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами -# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство. -function Normalize-PanelSynonyms { - param($el) - if ($null -eq $el) { return } - $panelSyns = @{ - 'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель') - 'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню') - } - foreach ($canon in $panelSyns.Keys) { - foreach ($syn in $panelSyns[$canon]) { - $p = $el.PSObject.Properties[$syn] - if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) { - if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) { - $v = $p.Value - $el.PSObject.Properties.Remove($syn) | Out-Null - $el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force - } - break - } - } - } -} - -function Normalize-ElementSynonyms { - param($el) - if ($null -eq $el) { return } - Normalize-PanelSynonyms $el - # Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому - # значению (имя элемента); объект/массив уже отнесён к панель-свойству выше. - $typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } - foreach ($pair in $typeSyn.GetEnumerator()) { - $src = $el.PSObject.Properties[$pair.Key] - if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) { - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) { - $val = $el.extTooltip - $el.PSObject.Properties.Remove("extTooltip") | Out-Null - $el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force - } - # Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри - foreach ($pk in @('commandBar','contextMenu')) { - $pp = $el.PSObject.Properties[$pk] - if ($null -ne $pp) { - $kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null } - if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } } - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { Normalize-ElementSynonyms $child } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } - } -} - -function HasCmdBarRecursive { - param($el) - if ($null -eq $el) { return $false } - if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } - } - return $false -} - -function ApplyDynamicListTableHeuristic { - param($el, [string]$listName, [bool]$hasMainTable) - if ($null -eq $el) { return } - if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { - # Маркер дин-список-таблицы → Emit-Table эмитит блок свойств (Group A defaults) - $el | Add-Member -NotePropertyName "_dynList" -NotePropertyValue $true -Force - if ($null -eq $el.PSObject.Properties["tableAutofill"]) { - $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force - } - if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { - $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force - } - # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. - # Декомпилятор опускает ключ при rpdp == smart-default (ждёт реинъекции); реальное отсутствие - # фиксирует ""-маркером (НЕ перезатирается). Гейт hasMainTable снят: дин-список без mainTable - # (напр. query-based) тоже несёт RowPictureDataPath. - if ($null -eq $el.PSObject.Properties["rowPictureDataPath"]) { - $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } - } -} - -function Test-IsObjectLikeType { - param([string]$type) - if ([string]::IsNullOrEmpty($type)) { return $false } - if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } - $objectSuffixes = @( - "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", - "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", - "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", - "ChartOfCalculationTypesObject", "ExchangePlanObject" - ) - $recordSetPrefixes = @( - "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", - "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", - "InformationRegisterRecordManager" - ) - foreach ($suffix in $objectSuffixes) { - if ($type -like "$suffix.*") { return $true } - } - foreach ($prefix in $recordSetPrefixes) { - if ($type -like "$prefix.*") { return $true } - } - return $false -} - -# 11b.1: Normalize synonyms recursively -if ($def.elements) { - foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } -} - -# 11b.2: Extract autoCmdBar element from def.elements -$script:mainAcbDef = $null -if ($def.elements) { - $autoBars = @() - $rest = @() - foreach ($el in $def.elements) { - if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { - $autoBars += $el - } else { - $rest += $el - } - } - if ($autoBars.Count -gt 1) { - Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." - exit 1 - } - if ($autoBars.Count -eq 1) { - $script:mainAcbDef = $autoBars[0] - # Replace def.elements with the filtered list - $def.PSObject.Properties.Remove("elements") | Out-Null - $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force - } -} - -# 11b.3: Infer main attribute (only if no attribute has main:true) -if ($def.attributes) { - $hasExplicitMain = $false - foreach ($attr in $def.attributes) { - if ($attr.main -eq $true) { $hasExplicitMain = $true; break } - } - if (-not $hasExplicitMain) { - $candidates = @() - foreach ($attr in $def.attributes) { - # Skip if user explicitly opted out via main:false - if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } - if (Test-IsObjectLikeType "$($attr.type)") { - $candidates += $attr - } - } - if ($candidates.Count -eq 1) { - $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force - Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" - } elseif ($candidates.Count -gt 1) { - $names = ($candidates | ForEach-Object { $_.name }) -join ", " - Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" - } - } -} - -# 11b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) -if ($def.attributes -and $def.elements) { - foreach ($attr in $def.attributes) { - if ("$($attr.type)" -ne "DynamicList") { continue } - $mt = $null - if ($attr.PSObject.Properties["settings"] -and $null -ne $attr.settings) { - if ($attr.settings -is [hashtable]) { - if ($attr.settings.ContainsKey("mainTable")) { $mt = $attr.settings["mainTable"] } - } elseif ($attr.settings.PSObject.Properties["mainTable"]) { - $mt = $attr.settings.mainTable - } - } - $hasMt = -not [string]::IsNullOrEmpty("$mt") - foreach ($el in $def.elements) { - ApplyDynamicListTableHeuristic $el $attr.name $hasMt - } - } -} - -# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 -function Compute-MainAcbAutofill { - if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { - return [bool]$script:mainAcbDef.autofill - } - return $true - } - if ($def.elements) { - foreach ($el in $def.elements) { - if (HasCmdBarRecursive $el) { return $false } - } - } - return $true -} - -# --- 12. Main compilation --- - -# Title -if ($def.title) { - Emit-MLText -tag "Title" -text $def.title -indent "`t" -} - -# Header -X '<?xml version="1.0" encoding="UTF-8"?>' -X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" - -# Oops — Title was emitted before header. Need to fix the order. -# Actually, let me restructure: build the body into a separate buffer, then assemble - -# Reset and rebuild properly -$script:xml = New-Object System.Text.StringBuilder 8192 -$script:nextId = 1 -$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме) - -X '<?xml version="1.0" encoding="UTF-8"?>' -X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" - -# 12a. Title (from def.title or properties.title — must be multilingual XML) -$formTitle = $def.title -if (-not $formTitle -and $def.properties -and $def.properties.title) { - $formTitle = $def.properties.title -} -if ($formTitle) { - Emit-MLText -tag "Title" -text $formTitle -indent "`t" -} - -# 12b. Properties (skip 'title' — handled above as multilingual) -# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; -# otherwise platform appends synonym → "Title: Synonym" double-titles). -$propsClone = New-Object PSObject -$hasAutoTitle = $false -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } - } -} -if ($formTitle -and -not $hasAutoTitle) { - $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false -} -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -ne "title") { - $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value - } - } -} -Emit-Properties -props $propsClone -indent "`t" - -# 12c. CommandSet (excluded commands) -if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { - X "`t<CommandSet>" - foreach ($cmd in $def.excludedCommands) { - X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>" - } - X "`t</CommandSet>" -} - -# 12c2. MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок -# (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). -# $null-проверка (не truthy): одноэлементный массив с пустой строкой @("") разворачивается -# в boolean-контексте в "" → falsy; 12 форм корпуса несут один пустой item (Value=""). -if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).Count -gt 0) { - X "`t<MobileDeviceCommandBarContent>" - foreach ($nm in @($def.mobileCommandBarContent)) { - X "`t`t<xr:Item>" - X "`t`t`t<xr:Presentation/>" - X "`t`t`t<xr:CheckState>0</xr:CheckState>" - # пустое значение → самозакрывающийся тег (зеркало платформы) - if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" } - else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" } - X "`t`t</xr:Item>" - } - X "`t</MobileDeviceCommandBarContent>" -} - -# 12d. AutoCommandBar (always present, id=-1) -$acbAutofill = Compute-MainAcbAutofill -$acbName = "ФормаКоманднаяПанель" -$acbHAlign = $null -if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { - $acbName = "$($script:mainAcbDef.autoCmdBar)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { - $acbName = "$($script:mainAcbDef.name)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { - $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" - } -} -$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) -$acbDIAttr = if ($script:mainAcbDef) { DI-Attr $script:mainAcbDef } else { "" } # DisplayImportance форменной панели -$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) -if ($acbHasInner) { - X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr>" - if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" } - if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" } - if ($hasAcbChildren) { - X "`t`t<ChildItems>" - foreach ($child in $script:mainAcbDef.children) { - Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true - } - X "`t`t</ChildItems>" - } - X "`t</AutoCommandBar>" -} else { - X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr/>" -} - -# 12e. Events -if ($def.events) { - foreach ($p in $def.events.PSObject.Properties) { - if ($script:knownFormEvents -notcontains $p.Name) { - Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" - } - } - X "`t<Events>" - foreach ($p in $def.events.PSObject.Properties) { - X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>" - } - X "`t</Events>" -} - -# 12f. ChildItems (elements) -if ($def.elements -and $def.elements.Count -gt 0) { - X "`t<ChildItems>" - foreach ($el in $def.elements) { - Emit-Element -el $el -indent "`t`t" - } - X "`t</ChildItems>" -} - -# 12g. Attributes -Emit-Attributes -attrs $def.attributes -indent "`t" -conditionalAppearance $def.conditionalAppearance - -# 12h. Parameters -Emit-Parameters -params $def.parameters -indent "`t" - -# 12i. Commands -Emit-Commands -cmds $def.commands -indent "`t" - -# 12i2. CommandInterface (командный интерфейс формы — последний дочерний Form) -Emit-CommandInterface -ci $def.commandInterface -indent "`t" - -# 12j. Close -X '</Form>' - -# --- 13. Write output --- - -$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } -$outDir = [System.IO.Path]::GetDirectoryName($outPath) -if (-not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null -} - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) - -# --- 13b. Auto-register form in parent object XML --- - -# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml -$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) -$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) -$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) -$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) -$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) - -$formName = [System.IO.Path]::GetFileName($formNameDir) -$objectName = [System.IO.Path]::GetFileName($objectDir) -$formsLeaf = [System.IO.Path]::GetFileName($formsDir) - -if ($formsLeaf -eq 'Forms') { - $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" - if (Test-Path $objectXmlPath) { - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $true - $objDoc.Load($objectXmlPath) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) - if ($childObjects) { - $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) - if (-not $existing) { - $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") - $formElem.InnerText = $formName - - $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) - if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } - - if ($insertBefore) { - $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null - $ws = $objDoc.CreateWhitespace("`n`t`t`t") - $childObjects.InsertBefore($ws, $insertBefore) | Out-Null - } else { - $lastChild = $childObjects.LastChild - if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { - $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null - $childObjects.InsertBefore($formElem, $lastChild) | Out-Null - } else { - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null - $childObjects.AppendChild($formElem) | Out-Null - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null - } - } - - $regEnc = New-Object System.Text.UTF8Encoding($true) - $regSettings = New-Object System.Xml.XmlWriterSettings - $regSettings.Encoding = $regEnc - $regSettings.Indent = $false - $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) - $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) - $objDoc.Save($regWriter) - $regWriter.Close() - $regStream.Close() - - Write-Host " Registered: <Form>$formName</Form> in $objectName.xml" - } - } - } -} - -# --- 14. Summary --- - -$elCount = $script:nextId - 1 -Write-Host "[OK] Compiled: $OutputPath" -Write-Host " Elements+IDs: $elCount" -if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } -if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } -if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } +# form-compile v1.174 — Compile 1C managed form from JSON or object metadata +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputPath, + + [switch]$FromObject, + [string]$ObjectPath, + [string]$Purpose, + [string]$Preset = "erp-standard", + [string]$EmitDsl +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# ═══════════════════════════════════════════════════════════════════════════ +# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation +# ═══════════════════════════════════════════════════════════════════════════ + +function Parse-ObjectMeta([string]$ObjectPath) { + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($ObjectPath) + + $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + + # Detect object type from root child + $metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns) + if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 } + $typeNode = $metaRoot.FirstChild + $objType = $typeNode.LocalName # "Document", "Catalog", etc. + + $propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) + $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) + + # Name + $objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText + + # Synonym (Russian) + $synonym = $objName + $synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + if ($synNode) { $synonym = $synNode.InnerText } + + # Helper: extract type string from md:Type + $extractType = { + param($typeParent) + if (-not $typeParent) { return "string" } + $types = @() + foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) { + $types += $t.InnerText + } + if ($types.Count -eq 0) { return "string" } + return ($types -join " | ") + } + + # Helper: check if type is a reference + $isRefType = { + param([string]$t) + return ($t -match 'Ref\.' -or $t -match 'ссылка\.') + } + + # Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) + $extractFields = { + param($parentNode, [string]$tagName) + $result = @() + if (-not $parentNode) { return $result } + foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { + $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) + $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText + $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } + $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) + $fType = & $extractType $fTypeNode + $result += @{ + Name = $fName + Synonym = $fSyn + Type = $fType + IsRef = (& $isRefType $fType) + } + } + return $result + } + + # Attributes + $attributes = @(& $extractFields $childObjs "Attribute") + + # Tabular sections + $tabularSections = @() + if ($childObjs) { + foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) { + $tsp = $tsNode.SelectSingleNode("md:Properties", $ns) + $tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText + $tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName } + $tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns) + $tsCols = @(& $extractFields $tsCo "Attribute") + $tabularSections += @{ + Name = $tsName + Synonym = $tsSyn + Columns = $tsCols + } + } + } + + $meta = @{ + Type = $objType + Name = $objName + Synonym = $synonym + Attributes = $attributes + TabularSections = $tabularSections + } + + # Type-specific properties + switch ($objType) { + "Document" { + $ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns) + $meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" } + } + "Catalog" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + # Owners + $owners = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + } + "InformationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) + $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } + $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) + $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } + } + "AccumulationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) + $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } + } + "ChartOfCharacteristicTypes" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + $owners = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + $meta.HasValueType = $true + } + "ExchangePlan" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $meta.Hierarchical = $false + $meta.HierarchyType = $null + $meta.Owners = @() + } + "ChartOfAccounts" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $meta.Hierarchical = $true + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + $meta.Owners = @() + $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) + $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } + $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") + $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") + } + } + + return $meta +} + +function Load-Preset([string]$PresetName, [string]$ScriptDir) { + # Hardcoded defaults (ERP-oriented) + $defaults = @{ + "document.item" = @{ + header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" } + footer = @{ fields = @("Комментарий"); position = "insidePage" } + tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true } + additional = @{ position = "page"; layout = "2col"; bspGroup = $true } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + commandBar = "auto" + properties = @{ autoTitle = $false } + } + "document.list" = @{ + columns = "all"; columnType = "labelField"; hiddenRef = $true + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "document.choice" = @{ + basedOn = "document.list" + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "catalog.item" = @{ + header = @{ layout = "1col"; distribute = "left" } + codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" } + parent = @{ title = "Входит в группу"; position = "afterCodeDescription" } + owner = @{ readOnly = $true; position = "first" } + tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true } + footer = @{ fields = @(); position = "none" } + additional = @{ position = "none"; bspGroup = $true } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + commandBar = "auto" + properties = @{} + } + "catalog.folder" = @{ + parent = @{ title = "Входит в группу" } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "catalog.list" = @{ + columns = "all"; columnType = "labelField"; hiddenRef = $true + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "catalog.choice" = @{ + basedOn = "catalog.list"; choiceMode = $true + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + # ─── Register defaults ─── + "informationRegister.record" = @{ + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "informationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "accumulationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + # ─── Catalog-like type defaults ─── + "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } + "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } + "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } + "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } + "exchangePlan.item" = @{ basedOn = "catalog.item" } + "exchangePlan.list" = @{ basedOn = "catalog.list" } + "exchangePlan.choice" = @{ basedOn = "catalog.choice" } + # ─── ChartOfAccounts defaults ─── + "chartOfAccounts.item" = @{ + parent = @{ title = "Подчинен счету" } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{} + } + "chartOfAccounts.folder" = @{ + parent = @{ title = "Подчинен счету" } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "chartOfAccounts.list" = @{ basedOn = "catalog.list" } + "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } + } + + # Deep merge helper + $deepMerge = { + param($base, $overlay) + if (-not $overlay) { return $base } + if (-not $base) { return $overlay } + $result = @{} + foreach ($k in $base.Keys) { $result[$k] = $base[$k] } + foreach ($k in $overlay.Keys) { + if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { + $result[$k] = & $deepMerge $result[$k] $overlay[$k] + } else { + $result[$k] = $overlay[$k] + } + } + return $result + } + + # Try built-in preset + $presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets" + $builtInPath = Join-Path $presetDir "$PresetName.json" + if (Test-Path $builtInPath) { + $presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json + # Convert PSCustomObject to hashtable recursively + $toHash = { + param($obj) + if ($obj -is [System.Management.Automation.PSCustomObject]) { + $h = @{} + foreach ($p in $obj.PSObject.Properties) { + $h[$p.Name] = & $toHash $p.Value + } + return $h + } + if ($obj -is [System.Object[]]) { + return @($obj | ForEach-Object { & $toHash $_ }) + } + return $obj + } + $presetHash = & $toHash $presetJson + foreach ($k in @($presetHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k] + } + } + + # Try project-level preset (scan up from output path) + $scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved) + while ($scanDir) { + $projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json" + if (Test-Path $projPreset) { + $projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json + $projHash = & $toHash $projJson + foreach ($k in @($projHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] + } + break + } + $parentDir = Split-Path $scanDir -Parent + if ($parentDir -eq $scanDir) { break } + $scanDir = $parentDir + } + + # Resolve basedOn references + foreach ($k in @($defaults.Keys)) { + $sect = $defaults[$k] + if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) { + $baseName = $sect["basedOn"] + if ($defaults.ContainsKey($baseName)) { + $merged = & $deepMerge $defaults[$baseName] $sect + $merged.Remove("basedOn") + $defaults[$k] = $merged + } + } + } + + return $defaults +} + +# --- Helper: build a field element DSL entry --- +# Non-displayable types — cannot be bound to form elements +$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') + +function Test-DisplayableType([string]$typeStr) { + foreach ($nd in $script:nonDisplayableTypes) { + if ($typeStr -match [regex]::Escape($nd)) { return $false } + } + return $true +} + +function New-FieldElement { + param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps) + + $isRef = ($attrType -match 'Ref\.') + $isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean') + + # Determine element type + $elType = "input" + if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") { + $elType = "check" + } + + $el = [ordered]@{ $elType = $attrName; path = $dataPath } + + # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. + # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы + # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) + + # Extra props + if ($extraProps) { + foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] } + } + + return $el +} + +# --- Catalog DSL generators --- +function Generate-CatalogDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "catalog.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "Folder" { return Generate-CatalogFolderDSL $meta $p } + "List" { return Generate-CatalogListDSL $meta $p } + "Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData } + "Item" { return Generate-CatalogItemDSL $meta $p $fd } + } +} + +function Generate-CatalogFolderDSL($meta, [hashtable]$p) { + $elements = @() + # Code (if CodeLength > 0) + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + # Description + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + # Parent + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + $elements += $parentEl + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + $formProps = [ordered]@{ useForFoldersAndItems = "Folders" } + foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $elements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +function Generate-CatalogListDSL($meta, [hashtable]$p) { + # Columns + $columns = @() + # Description always first + $columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" } + # Code if present + if ($meta.CodeLength -gt 0) { + $columns += [ordered]@{ labelField = "Код"; path = "Список.Code" } + } + # Custom attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + # Hierarchical properties + if ($meta.Hierarchical) { + $tableEl["initialTreeView"] = "ExpandTopLevel" + $tableEl["enableStartDrag"] = $true + $tableEl["enableDrag"] = $true + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + # Start from list + $listKey = "catalog.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-CatalogListDSL $meta $lp + + # Add choice-specific properties + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + # Set ChoiceMode on table + $dsl.elements[0]["choiceMode"] = $true + + return $dsl +} + +function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerChildren = @() + + # Owner (if subordinate) + if ($meta.Owners -and $meta.Owners.Count -gt 0) { + $ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true } + $headerChildren += $ownerEl + } + + # Code + Description + $cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" } + $cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" } + $hasCode = ($meta.CodeLength -gt 0) + + if ($cdLayout -eq "horizontal" -and $hasCode) { + $cdChildren = @() + $descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" } + $codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" } + if ($cdOrder -eq "descriptionFirst") { + $cdChildren = @($descEl, $codeEl) + } else { + $cdChildren = @($codeEl, $descEl) + } + $headerChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false + representation = "none"; children = $cdChildren + } + } else { + # Vertical or no code + $headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + if ($hasCode) { + $headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + } + + # Parent (for hierarchical catalogs) + $parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" } + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + if ($meta.Hierarchical) { + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + if ($parentPos -eq "beforeCodeDescription") { + # Insert before Code/Description (after Owner if present) + $insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 } + $newChildren = @() + for ($i = 0; $i -lt $headerChildren.Count; $i++) { + if ($i -eq $insertIdx) { $newChildren += $parentEl } + $newChildren += $headerChildren[$i] + } + $headerChildren = $newChildren + } else { + # afterCodeDescription (default) + $headerChildren += $parentEl + } + } + + # Custom attributes → header + $footerFieldNames = @() + if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) } + + foreach ($attr in $meta.Attributes) { + if ($footerFieldNames -contains $attr.Name) { continue } + if (-not (Test-DisplayableType $attr.Type)) { continue } + $headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build root elements + $rootElements = @() + + # ГруппаШапка + $rootElements += [ordered]@{ + group = "vertical"; name = "ГруппаШапка"; showTitle = $false + representation = "none"; children = $headerChildren + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты", "Представления") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + $tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" } + + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{} + $tsCols += $colEl + } + $tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + $rootElements += $tableEl + } + + # Footer fields + foreach ($fn in $footerFieldNames) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr) { + $rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # BSP group + $bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + if ($bspGroup) { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + + # Properties + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + # UseForFoldersAndItems + if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") { + $formProps["useForFoldersAndItems"] = "Items" + } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +# --- Document DSL generators --- +function Generate-DocumentDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "document.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "List" { return Generate-DocumentListDSL $meta $p } + "Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData } + "Item" { return Generate-DocumentItemDSL $meta $p $fd } + } +} + +function Generate-DocumentListDSL($meta, [hashtable]$p) { + $columns = @() + # Standard columns: Number + Date + $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } + $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } + # All custom attributes as labelField + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + $listKey = "document.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-DocumentListDSL $meta $lp + + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + return $dsl +} + +function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" } + $headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" } + $headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" } + $dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" } + + $footerFields = @() + if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) } + $footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" } + + $addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" } + $addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" } + $addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + $addLeft = @(); $addRight = @() + if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) } + if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) } + + $headerRight = @() + if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) } + + $tsExclude = @("ДополнительныеРеквизиты") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + + # Classify attributes + $claimed = @{} + foreach ($fn in $footerFields) { $claimed[$fn] = "footer" } + foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" } + foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" } + foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" } + + $unclaimed = @() + foreach ($attr in $meta.Attributes) { + if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr } + } + + # Distribute unclaimed + $leftAttrs = @(); $rightExtraAttrs = @() + switch ($headerDistribute) { + "left" { $leftAttrs = $unclaimed } + "right" { $rightExtraAttrs = $unclaimed } + default { # "even" + $half = [Math]::Ceiling($unclaimed.Count / 2) + for ($i = 0; $i -lt $unclaimed.Count; $i++) { + if ($i -lt $half) { $leftAttrs += $unclaimed[$i] } + else { $rightExtraAttrs += $unclaimed[$i] } + } + } + } + + # Build ГруппаНомерДата + $numDateChildren = @( + [ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 } + [ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle } + ) + $numDateGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren + } + + # Build left column + $leftChildren = @($numDateGroup) + foreach ($attr in $leftAttrs) { + $leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build right column + $rightChildren = @() + foreach ($rn in $headerRight) { + $rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn } + if ($rAttr) { + $rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($attr in $rightExtraAttrs) { + $rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Header group + $headerGroup = $null + if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) { + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren } + ) + } + } else { + # 1col or no right items + $allHeaderFields = $leftChildren + $rightChildren + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields } + ) + } + } + + # Footer elements + $footerElements = @() + foreach ($fn in $footerFields) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr -and (Test-DisplayableType $fAttr.Type)) { + $footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # Visible tabular sections + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + # Additional page content + $additionalPage = $null + if ($addPos -eq "page") { + $addLeftEls = @(); $addRightEls = @() + foreach ($aln in $addLeft) { + $alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln } + if ($alAttr) { + $addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($arn in $addRight) { + $arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn } + if ($arAttr) { + $addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + $addPageChildren = @() + if ($addLayout -eq "2col") { + $addPageChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls } + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls } + ) + } + } else { + $addPageChildren += @($addLeftEls + $addRightEls) + } + if ($addBspGroup) { + $addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + $additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren } + } + + # Build TS page elements + $tsPages = @() + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}) + } + $tsPages += [ordered]@{ + page = "Группа$($ts.Name)"; title = $ts.Synonym + children = @( + [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + ) + } + } + + # Assemble root elements + $rootElements = @() + + if ($visibleTS.Count -eq 0) { + # Simple form — no Pages + $rootElements += $headerGroup + if ($footerElements.Count -gt 0) { $rootElements += $footerElements } + if ($addBspGroup -and $addPos -ne "none") { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + } else { + # Pages form + if ($headerPos -eq "abovePages") { + $rootElements += $headerGroup + $pagesChildren = @() + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } else { + # insidePage (default) + $osnovnoeChildren = @($headerGroup) + if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) { + $osnovnoeChildren += $footerElements + } + $pagesChildren = @() + $pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren } + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } + + # Footer below pages + if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) { + $rootElements += $footerElements + } + } + + # Properties + $formProps = [ordered]@{ autoTitle = $false } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true } + ) + } +} + +# ─── InformationRegister ────────────────────────────────────────────────── + +function Generate-InformationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "informationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } + "List" { return Generate-InformationRegisterListDSL $meta $p } + } +} + +function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { + $elements = @() + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + + # Period first (if periodic) + if ($isPeriodic) { + $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" + + $columns = @() + # Period + if ($isPeriodic) { + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + } + # Recorder/LineNumber for subordinate registers + if ($isRecorderSubordinate) { + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── AccumulationRegister ───────────────────────────────────────────────── + +function Generate-AccumulationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "accumulationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + switch ($purpose) { + "List" { return Generate-AccumulationRegisterListDSL $meta $p } + } +} + +function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { + $columns = @() + # AccumulationRegisters always have Period, Recorder, LineNumber + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── + +function Generate-ChartOfCharacteristicTypesDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + $catObjType = "CatalogObject.$($meta.Name)" + $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $ccoctListType + } + } + + # For Item forms: inject ValueType field after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $vtEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $vtEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── + +function Generate-ExchangePlanDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # ExchangePlans are not hierarchical and have no Folder form + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ExchangePlan types + $catObjType = "CatalogObject.$($meta.Name)" + $epObjType = "ExchangePlanObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $epListType = "ExchangePlan.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $epObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $epListType + } + } + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } + $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $sentEl + $newElements += $recvEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ChartOfAccounts ────────────────────────────────────────────────────── + +function Generate-ChartOfAccountsDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "chartOfAccounts.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } + "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } + "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } + "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } + } +} + +function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { + $elements = @() + + # Header: Code + Parent + $headerLeftChildren = @() + if ($meta.CodeLength -gt 0) { + $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + $headerRightChildren = @() + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + if ($headerRightChildren.Count -gt 0) { + $elements += [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } + ) + } + } elseif ($headerLeftChildren.Count -gt 0) { + $elements += $headerLeftChildren + } + + # Description + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + + # OffBalance + $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } + + # AccountingFlags as checkboxes + if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { + $flagChildren = @() + foreach ($flag in $meta.AccountingFlags) { + $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } + } + $elements += [ordered]@{ + group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" + children = $flagChildren + } + } + + # ExtDimensionTypes table + if ($meta.MaxExtDimensionCount -gt 0) { + # Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С), + # иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта. + $edTable = "ВидыСубконто" + $edCols = @() + $edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } + $edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } + if ($meta.ExtDimensionAccountingFlags) { + foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { + $edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } + } + } + $elements += [ordered]@{ + table = $edTable + path = "Объект.ExtDimensionTypes" + columns = $edCols + } + } + + # Custom attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты","Представления") + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $tsCols = @() + foreach ($col in $ts.Columns) { + if (-not (Test-DisplayableType $col.Type)) { continue } + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) + } + $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { + $elements = @() + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + useForFoldersAndItems = "Folders" + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { + # Delegate to Catalog List and patch types + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + +# --- Detect XML format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +Assert-EditAllowed $script:outPathResolved 'editable' +$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) + +# --- 0. Path normalization and mode dispatch --- + +# Form name → purpose mapping +$script:formNameToPurpose = @{ + "ФормаДокумента" = "Item" + "ФормаЭлемента" = "Item" + "ФормаСписка" = "List" + "ФормаВыбора" = "Choice" + "ФормаГруппы" = "Folder" + "ФормаЗаписи" = "Record" + "ФормаСчета" = "Item" + "ФормаУзла" = "Item" +} + +if ($FromObject -and $JsonPath) { + Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode." + exit 1 +} +if (-not $FromObject -and -not $JsonPath) { + Write-Error "Either -JsonPath or -FromObject is required." + exit 1 +} + +if ($FromObject) { + # Normalize OutputPath: append /Ext/Form.xml if missing + $outNorm = $OutputPath -replace '[\\/]$', '' + if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') { + if ($outNorm -match '[/\\]Ext$') { + $OutputPath = "$outNorm/Form.xml" + } else { + $OutputPath = "$outNorm/Ext/Form.xml" + } + Write-Host "[resolved] OutputPath -> $OutputPath" + } + + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } + $pathParts = $outAbs -split '[/\\]' + # Find "Forms" segment + $formsIdx = -1 + for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { + if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break } + } + + $resolvedObjectPath = $null + $resolvedPurpose = $null + + if ($formsIdx -ge 2) { + $formName = $pathParts[$formsIdx + 1] + $objectName = $pathParts[$formsIdx - 1] + $typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar + + # Derive purpose from form name + if ($script:formNameToPurpose.ContainsKey($formName)) { + $resolvedPurpose = $script:formNameToPurpose[$formName] + } + + # Derive object XML path + $candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml" + if (Test-Path $candidateObjPath) { + $resolvedObjectPath = $candidateObjPath + } + } + + # Apply: explicit -ObjectPath / -Purpose override resolved values + $fromObjPath = $null + if ($ObjectPath) { + $fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath } + # Append .xml if missing + if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" } + } elseif ($resolvedObjectPath) { + $fromObjPath = $resolvedObjectPath + Write-Host "[resolved] ObjectPath -> $fromObjPath" + } else { + Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly." + exit 1 + } + + if (-not (Test-Path $fromObjPath)) { + Write-Error "Object file not found: $fromObjPath" + exit 1 + } + + $effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" } + if ($resolvedPurpose -and -not $Purpose) { + Write-Host "[resolved] Purpose -> $effectivePurpose" + } + + $meta = Parse-ObjectMeta $fromObjPath + Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)" + + $presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot + + $supportedPurposes = switch ($meta.Type) { + "Document" { @("Item","List","Choice") } + "Catalog" { @("Item","Folder","List","Choice") } + "InformationRegister" { @("Record","List") } + "AccumulationRegister" { @("List") } + "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } + "ExchangePlan" { @("Item","List","Choice") } + "ChartOfAccounts" { @("Item","Folder","List","Choice") } + default { @() } + } + if ($supportedPurposes.Count -eq 0) { + Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." + exit 1 + } + if ($supportedPurposes -notcontains $effectivePurpose) { + Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')" + exit 1 + } + + # Generate DSL + $dsl = switch ($meta.Type) { + "Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfAccounts" { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + } + + # Emit DSL if requested + if ($EmitDsl) { + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl } + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($dslPath, $dslJson, $enc) + Write-Host "[from-object] DSL saved: $dslPath" + } + + # Feed DSL into existing compiler + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $def = $dslJson | ConvertFrom-Json +} else { + # --- 1. Load and validate JSON (original mode) --- + + if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 + } + + $json = Get-Content -Raw -Encoding UTF8 $JsonPath + $def = $json | ConvertFrom-Json +} + +# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile) +$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path } +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath)) + } + foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + +# --- 2. ID allocator --- + +$script:nextId = 1 +function New-Id { + $id = $script:nextId + $script:nextId++ + return $id +} + +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +function Assert-UniqueName { + param([string]$name, [hashtable]$seen, [string]$kind) + if ($seen.ContainsKey($name)) { + Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')" + exit 1 + } + $seen[$name] = $true +} + +# --- 3. XML helper --- + +$script:xml = New-Object System.Text.StringBuilder 8192 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . + # Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально); + # " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов. + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>') +} + +# --- 4. Multilang helper --- + +# Эмитит <v8:item> для значения: строка → один ru-элемент; объект {lang:text} → по элементу на язык. +function Emit-MLItems { + param($val, [string]$indent) + if ($val -is [System.Collections.IDictionary]) { + foreach ($k in $val.Keys) { + X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>" + } + } elseif ($val -is [System.Management.Automation.PSCustomObject]) { + foreach ($p in $val.PSObject.Properties) { + X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>" + } + } else { + X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>" + } +} + +function Emit-MLText { + param([string]$tag, $text, [string]$indent, [string]$xsiType) + $attr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } + X "$indent<$tag$attr>" + Emit-MLItems -val $text -indent "$indent`t" + X "$indent</$tag>" +} + +# <dcsset:userSettingPresentation> и подобные DCS-подписи: платформа пишет плоскую строку как +# xsi:type="xs:string" (скаляр; корпус 26), мультиязычный текст — как xsi:type="v8:LocalStringType" +# (7). Декомпилятор различает (Get-PresText: строка ИЛИ объект {ru,en}). +function Emit-USPresentation { + param($val, [string]$tag, [string]$indent) + if ($null -eq $val) { return } + if ($val -is [string]) { + X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>" + } else { + Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType" + } +} + +# Детектор «настоящей» inline-разметки форматированного текста (1С: <link>/<b>/<color>/… +# и закрывающий </>). Плейсхолдеры вида <не заполнен> НЕ срабатывают (нет известного тега/</>). +# ВАЖНО: regex должен быть идентичен в form-decompile (иначе гибрид-раундтрип поедет). +$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' +function Test-HasRealMarkup { + param($text) + if ($null -eq $text) { return $false } + $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } + elseif ($text -is [System.Management.Automation.PSCustomObject]) { @($text.PSObject.Properties.Value) } + else { @("$text") } + foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } + return $false +} +# DSL-значение ML-поля → @{ text; formatted }. Форма {text, formatted} = явный override; +# строка/мапа → авто-детект formatted по разметке. +function Resolve-MLFormatted { + param($val) + $hasText = $false + if ($val -is [System.Management.Automation.PSCustomObject]) { $hasText = [bool]$val.PSObject.Properties['text'] } + elseif ($val -is [System.Collections.IDictionary]) { $hasText = $val.Contains('text') } + if ($hasText) { + $t = if ($val -is [System.Collections.IDictionary]) { $val['text'] } else { $val.text } + $f = if ($val -is [System.Collections.IDictionary]) { $val['formatted'] } else { $val.formatted } + return @{ text = $t; formatted = [bool]$f } + } + return @{ text = $val; formatted = (Test-HasRealMarkup $val) } +} + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип +# (harness нормализует GUID для хвоста с иными идентификаторами). +$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + +# ───────────────────────────────────────────────────────────────────────────── +# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. +# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). +# ───────────────────────────────────────────────────────────────────────────── +function New-Guid-String { return [System.Guid]::NewGuid().ToString() } + +$script:comparisonTypes = @{ + "=" = "Equal"; "<>" = "NotEqual" + ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual" + "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "like" = "Like"; "notLike" = "NotLike" + "подобно" = "Like"; "неподобно" = "NotLike" # рус. синоним (хэш регистронезависим: ПОДОБНО=подобно) + "filled" = "Filled"; "notFilled" = "NotFilled" +} + +function Parse-FilterShorthand { + param([string]$s) + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } + if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } + if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } + if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } + if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } + if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' } + $s = $s.Trim() + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notLike\b', 'like\b', 'неподобно\b', 'подобно\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $result.op = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" } + elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart } # дата без valueType → Emit-FilterItem выведет StandardBeginningDate Custom (дефолт даты в фильтре) + elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" } + elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" } + else { $result.value = $valPart; $result["valueType"] = "xs:string" } + } + } else { $result.field = $s } + return $result +} + +# Значение типа v8:Type (напр. тип «Неопределено» = <prefix>:Undefined) ссылается на тип +# платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО +# на теге значения (префикс авто-назначаемый: d6p1/d8p1/dN…). Без объявления QName битый. +# Возвращает строку-атрибут ' xmlns:<pref>="…"' (перед xsi:type), либо "". +function Get-ValueTypeNsAttr { + param([string]$valueType, [string]$value) + if ($valueType -eq 'v8:Type' -and "$value" -match '^([A-Za-z]\w*):') { + $pref = $Matches[1] + if ($pref -notin @('xs','cfg','v8','v8ui','ent','dcscor','dcsset','dcssch')) { + return " xmlns:$pref=`"http://v8.1c.ru/8.2/data/types`"" + } + } + return "" +} + +function Emit-FilterItem { + param($item, [string]$indent) + if ($item.group) { + $groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } } + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" + if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } # группа отключена (перед groupType, порядок исходника) + X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>" + if ($item.items) { + foreach ($sub in $item.items) { + if ($sub -is [string]) { + $parsed = Parse-FilterShorthand $sub + $obj = @{ field = $parsed.field; op = $parsed.op } + if ($parsed.use -eq $false) { $obj.use = $false } + if ($null -ne $parsed.value) { $obj.value = $parsed.value } + if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } + if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } + if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } + $sub = [pscustomobject]$obj + } + Emit-FilterItem -item $sub -indent "$indent`t" + } + } + if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } + if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + if ($item.userSettingID) { + $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:item>" + return + } + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" + if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } + X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>" + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>" + $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) + if ($valIsArray) { + if (@($item.value).Count -eq 0) { + X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">" + X "$indent`t`t<v8:valueType/>" + X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>" + X "$indent`t</dcsset:right>" + } else { + foreach ($v in $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + if ($v -is [bool]) { $vt = 'xs:boolean' } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } + elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } + elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } + elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } + else { $vt = 'xs:string' } + } + $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } + $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v" + X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>" + } + } + } elseif ($null -ne $item.value -and ( + "$($item.valueType)" -match 'Standard(Beginning|End)Date$' -or + (-not $item.valueType -and "$($item.value)" -match '^\d{4}-\d{2}-\d{2}T'))) { + # Стандартная дата начала/окончания. Формы значения: + # объект {variant, date?} — полная (Custom несёт <v8:date>); + # строка-вариант "BeginningOfThisDay" — именованный вариант без даты; + # голая ISO-дата без valueType — шорткат для Custom+date (дата в фильтре платформой + # почти всегда хранится как StandardBeginningDate Custom, корпус 268 vs 2 xs:dateTime; + # явный valueType="xs:dateTime" → плоская дата, ветка ниже). + $sdType = if ($item.valueType) { "$($item.valueType)" -replace '^v8:','' } else { 'StandardBeginningDate' } + $sv = $item.value + if (($sv -is [PSCustomObject]) -or ($sv -is [System.Collections.IDictionary])) { + $variant = if ($sv -is [PSCustomObject]) { "$($sv.variant)" } else { "$($sv['variant'])" } + $hasDate = if ($sv -is [PSCustomObject]) { [bool]$sv.PSObject.Properties['date'] } else { $sv.Contains('date') } + $dateV = if ($hasDate) { if ($sv -is [PSCustomObject]) { "$($sv.date)" } else { "$($sv['date'])" } } else { $null } + } elseif ("$sv" -match '^\d{4}-\d{2}-\d{2}T') { + $variant = 'Custom'; $hasDate = $true; $dateV = "$sv" + } else { + $variant = "$sv"; $hasDate = $false; $dateV = $null + } + X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">" + X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>" + if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" } + X "$indent`t</dcsset:right>" + } elseif ("$($item.value)" -eq '_') { + # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> + # (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем). + $vt = if ($item.valueType) { "$($item.valueType)" } else { 'xs:string' } + X "$indent`t<dcsset:right xsi:type=`"$vt`"/>" + } elseif ($null -ne $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + $v = $item.value + if ($v -is [bool]) { $vt = "xs:boolean" } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" } + elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" } + elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" } + elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" } + else { $vt = "xs:string" } + } + $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } + $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)" + X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>" + } + if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } + if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:item>" +} + +function Emit-Filter { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<dcsset:filter>" + foreach ($item in $items) { + if ($item -is [string]) { + $parsed = Parse-FilterShorthand $item + $obj = @{ field = $parsed.field; op = $parsed.op } + if ($parsed.use -eq $false) { $obj.use = $false } + if ($null -ne $parsed.value) { $obj.value = $parsed.value } + if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } + if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } + if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } + Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t" + } else { Emit-FilterItem -item $item -indent "$indent`t" } + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:filter>" +} + +function Emit-Order { + param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<dcsset:order>" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" } } + else { + $parts = $item -split '\s+' + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" } + elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + X "$indent`t</dcsset:item>" + } + } else { + if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" }; continue } + $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } + if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" } + X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + X "$indent`t</dcsset:item>" + } + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:order>" +} + +function Emit-AppearanceValue { + param([string]$key, $val, [string]$indent) + X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + function _HasKey { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } + if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } + return $false + } + function _Get { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return $o.$k } + if ($o -is [System.Collections.IDictionary]) { return $o[$k] } + return $null + } + $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') + $useWrapper = $false + $innerVal = $val + $nestedItems = $null + if ($isTopLevelLine) { + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { + $innerVal = (_Get $val 'value') + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } + if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" } + X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + $isFontDict = $false + if ($innerVal -is [PSCustomObject]) { + $tProp = $innerVal.PSObject.Properties['@type'] + if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } + } elseif ($innerVal -is [System.Collections.IDictionary]) { + if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } + } + $isLineDict = $false + if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } + $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) + if ($isLineDict) { + $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } + $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } + $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">" + X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>" + X "$indent`t</dcscor:value>" + } elseif ($isFontDict) { + $attrParts = @() + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $null + if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } } + else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } } + if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } + } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" + } elseif ($isDict -and (_HasKey $innerVal 'field')) { + # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки + X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>" + } elseif ($isDict) { + # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value + Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType" + } else { + $actualVal = "$innerVal" + $keyTypeMap = @{ + 'Размещение' = 'dcscor:DataCompositionTextPlacementType' + 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' + 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' + 'ОриентацияТекста' = 'xs:decimal' + 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' + 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' + } + $keyType = $keyTypeMap[$key] + if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" } + elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } + elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" } + elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { + # Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит + # нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше). + # Пустая строка → самозакрывающийся тег (как у платформы). + if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" } + else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } + } + elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" } + elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } + else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } + } + if ($nestedItems) { + $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } + if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } } + elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } } + } + X "$indent</dcscor:item>" +} + +function Emit-ConditionalAppearance { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance', $blockUserSettingPresentation = $null) + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<$wrapTag>" + foreach ($ca in $items) { + X "$indent`t<dcsset:item>" + if ($ca.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" } + if ($ca.selection -and $ca.selection.Count -gt 0) { + X "$indent`t`t<dcsset:selection>" + foreach ($sel in $ca.selection) { + X "$indent`t`t`t<dcsset:item>" + X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>" + X "$indent`t`t`t</dcsset:item>" + } + X "$indent`t`t</dcsset:selection>" + } else { X "$indent`t`t<dcsset:selection/>" } + if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" } + else { X "$indent`t`t<dcsset:filter/>" } + if ($ca.appearance) { + X "$indent`t`t<dcsset:appearance>" + foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" } + X "$indent`t`t</dcsset:appearance>" + } + if ($ca.presentation) { + if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { + # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) + X "$indent`t`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t" + X "$indent`t`t</dcsset:presentation>" + } + else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" } + } + if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" } + if ($ca.userSettingID) { + $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } + X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } + if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { + $useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') + $set = @{} + foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } + foreach ($n in $useInOrder) { + if ($set.ContainsKey($n)) { + $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) + X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>" + } + } + } + X "$indent`t</dcsset:item>" + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</$wrapTag>" +} + +# === Группировка строк динамического списка (DCS-структура ListSettings) === +# Линейная цепочка <dcsset:item StructureItemGroup> (каждый уровень = одно поле в groupItems; +# вложенность — через дочерний <dcsset:item>). Зеркало skd Emit-GroupItems/Emit-StructureItem, +# но плоская модель уровней (список всегда линеен, без selection/order/children). +function Get-ListGroupingValue { + param($st) + foreach ($k in 'grouping','structure','группировка') { + if ($st.PSObject.Properties[$k] -and $st.$k) { return $st.$k } + } + return $null +} + +function Parse-ListGrouping { + param($grouping) + # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. + # Unary comma: иначе PS разворачивает одноэлементный массив при return → строка → индексация даёт char. + if (-not $grouping) { return ,@() } + if ($grouping -is [string]) { return ,@($grouping -split '\s*>\s*' | Where-Object { "$_" -ne '' }) } + return ,@($grouping) +} + +function Emit-GroupItemField { + param($level, [string]$indent) + if ($level -is [string]) { + $field = $level; $gt = 'Items'; $pat = 'None'; $pab = '0001-01-01T00:00:00'; $pae = '0001-01-01T00:00:00' + } else { + $field = "$($level.field)" + $gt = if ($level.groupType) { "$($level.groupType)" } else { 'Items' } + $pat = if ($level.periodAdditionType) { "$($level.periodAdditionType)" } else { 'None' } + $pab = if ($level.periodAdditionBegin) { "$($level.periodAdditionBegin)" } else { '0001-01-01T00:00:00' } + $pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } + } + X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">" + X "$indent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + X "$indent`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>" + X "$indent`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>" + # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. + $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>" + X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>" + X "$indent</dcsset:item>" +} + +function Emit-ListGroupingLevels { + param($levels, [int]$i, [string]$indent) + X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" + X "$indent`t<dcsset:groupItems>" + Emit-GroupItemField $levels[$i] "$indent`t`t" + X "$indent`t</dcsset:groupItems>" + if ($i -lt $levels.Count - 1) { Emit-ListGroupingLevels $levels ($i + 1) "$indent`t" } + X "$indent</dcsset:item>" +} + +function Emit-ListGrouping { + param($grouping, [string]$indent) + $levels = Parse-ListGrouping $grouping + if ($levels.Count -eq 0) { return } + Emit-ListGroupingLevels $levels 0 $indent +} + +# === Вычисляемые поля DataSet динамического списка (<CalculatedField>) === +# Зеркало skd: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder" +# или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression (dcscommon ns). +$script:calcRestrictMap = @{ 'noField'='field'; 'noFilter'='condition'; 'noCondition'='condition'; 'noGroup'='group'; 'noOrder'='order' } +$script:dcsCommonNs = 'http://v8.1c.ru/8.1/data-composition-system/common' + +function Parse-CalcShorthand { + param([string]$s) + $restrict = @() + foreach ($m in [regex]::Matches($s, '#(noField|noFilter|noCondition|noGroup|noOrder)\b')) { $restrict += $m.Groups[1].Value } + $s = [regex]::Replace($s, '\s*#(noField|noFilter|noCondition|noGroup|noOrder)\b', '') + $eq = $s.IndexOf('=') + if ($eq -gt 0) { $lhs = $s.Substring(0, $eq); $rhs = $s.Substring($eq + 1).Trim() } else { $lhs = $s; $rhs = '' } + $title = '' + if ($lhs -match '\[([^\]]+)\]') { $title = $Matches[1]; $lhs = $lhs -replace '\s*\[[^\]]+\]', '' } + $lhs = $lhs.Trim() + $type = ''; $dataPath = $lhs + if ($lhs.Contains(':')) { $parts = $lhs -split ':', 2; $dataPath = $parts[0].Trim(); $type = Resolve-TypeStr ($parts[1].Trim()) } + return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict } +} + +function Emit-CalcFields { + param($calcFields, [string]$indent) + if (-not $calcFields) { return } + foreach ($cf in $calcFields) { + $pres = $null; $orderExpr = $null; $restrict = @() + if ($cf -is [string]) { + $p = Parse-CalcShorthand $cf + $dataPath = "$($p.dataPath)"; $expression = "$($p.expression)"; $title = $p.title; $typeStr = "$($p.type)" + foreach ($r in $p.restrict) { if ($script:calcRestrictMap[$r]) { $restrict += $script:calcRestrictMap[$r] } } + } else { + $dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } elseif ($cf.field) { "$($cf.field)" } else { "$($cf.name)" } + $expression = "$($cf.expression)" + $title = $cf.title + $typeStr = if ($cf.valueType) { "$($cf.valueType)" } elseif ($cf.type) { "$($cf.type)" } else { '' } + $ur = if ($cf.useRestriction) { $cf.useRestriction } elseif ($cf.restrict) { $cf.restrict } else { $null } + if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) { + foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $restrict += $k } } + } elseif ($ur -is [string]) { + foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $restrict += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } } + } elseif ($ur) { + foreach ($r in $ur) { $rr = "$r"; $restrict += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) } + } + $pres = $cf.presentationExpression + $orderExpr = $cf.orderExpression + } + $ci = "$indent`t" + X "$indent<CalculatedField>" + X "$ci<dcssch:dataPath>$(Esc-Xml $dataPath)</dcssch:dataPath>" + X "$ci<dcssch:expression>$(Esc-Xml $expression)</dcssch:expression>" + if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' } + if ($restrict.Count -gt 0) { + X "$ci<dcssch:useRestriction>" + foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } } + X "$ci</dcssch:useRestriction>" + } + if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-Xml "$pres")</dcssch:presentationExpression>" } + if ($orderExpr) { + $oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) } + foreach ($oe in $oeList) { + if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' } + else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } } + X "$ci<dcssch:orderExpression>" + X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-Xml $exprV)</expression>" + X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>" + X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>" + X "$ci</dcssch:orderExpression>" + } + } + if ($typeStr) { Emit-DLValueType -typeStr $typeStr -indent $ci } + X "$indent</CalculatedField>" + } +} + +# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction). +# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField #noFilter #noGroup #noOrder" | массив. +function Get-RestrictList { + param($ur) + $out = @() + if (-not $ur) { return ,$out } + if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) { + foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $out += $k } } + } elseif ($ur -is [string]) { + foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $out += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } } + } else { + foreach ($r in $ur) { $rr = "$r"; $out += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) } + } + return ,$out +} + +function Emit-RestrictBlock { + param([string]$tag, $ur, [string]$indent) + $r = Get-RestrictList $ur + if ($r.Count -eq 0) { return } + X "$indent<dcssch:$tag>" + foreach ($k in @('field','condition','group','order')) { if ($r -contains $k) { X "$indent`t<dcssch:$k>true</dcssch:$k>" } } + X "$indent</dcssch:$tag>" +} + +# --- 5. Type emitter --- + +$script:formTypeSynonyms = New-Object System.Collections.Hashtable +$script:formTypeSynonyms["строка"] = "string" +$script:formTypeSynonyms["число"] = "decimal" +$script:formTypeSynonyms["булево"] = "boolean" +$script:formTypeSynonyms["дата"] = "date" +$script:formTypeSynonyms["датавремя"]= "dateTime" +$script:formTypeSynonyms["number"] = "decimal" +$script:formTypeSynonyms["bool"] = "boolean" +$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" +$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" +$script:formTypeSynonyms["документссылка"] = "DocumentRef" +$script:formTypeSynonyms["документобъект"] = "DocumentObject" +$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" +$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:formTypeSynonyms["задачассылка"] = "TaskRef" +$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" +$script:formTypeSynonyms["характеристика"] = "Characteristic" +$script:formTypeSynonyms["любаяссылка"] = "AnyRef" +$script:formTypeSynonyms["любаяссылкаиб"] = "AnyIBRef" +# Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: (эмитим verbatim) +$script:formTypeSynonyms["standardperiod"] = "v8:StandardPeriod" +$script:formTypeSynonyms["стандартныйпериод"] = "v8:StandardPeriod" +$script:formTypeSynonyms["standardbeginningdate"] = "v8:StandardBeginningDate" +$script:formTypeSynonyms["стандартнаядатаначала"] = "v8:StandardBeginningDate" +$script:formTypeSynonyms["uuid"] = "v8:UUID" +$script:formTypeSynonyms["уникальныйидентификатор"] = "v8:UUID" +$script:formTypeSynonyms["списокзначений"] = "ValueList" + +# Known invalid types (runtime/UI types that don't exist in XDTO schema) +$script:knownInvalidTypes = @{ + "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" + "FormDataCollection" = "Runtime type. Use ValueTable" + "FormDataTree" = "Runtime type. Use ValueTree" + "FormDataTreeItem" = "Runtime type, not valid in XML" + "FormDataCollectionItem"= "Runtime type, not valid in XML" + "FormGroup" = "UI element type, not a data type" + "FormField" = "UI element type, not a data type" + "FormButton" = "UI element type, not a data type" + "FormDecoration" = "UI element type, not a data type" + "FormTable" = "UI element type, not a data type" +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $base = $Matches[1].Trim(); $params = $Matches[2] + $r = $script:formTypeSynonyms[$base.ToLower()] + if ($r) { return "$r($params)" } + return $typeStr + } + if ($typeStr.Contains('.')) { + $i = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) + $r = $script:formTypeSynonyms[$prefix.ToLower()] + if ($r) { return "$r$suffix" } + return $typeStr + } + $r = $script:formTypeSynonyms[$typeStr.ToLower()] + if ($r) { return $r } + return $typeStr +} + +function Emit-Type { + # $tag/$tagAttrs — обёртка (по умолчанию <Type>); для уточнения типа значений ValueList + # вызывается с tag="Settings", tagAttrs=' xsi:type="v8:TypeDescription"'. + param($typeStr, [string]$indent, [string]$tag = "Type", [string]$tagAttrs = "") + + if (-not $typeStr) { + X "$indent<$tag$tagAttrs/>" + return + } + + $typeString = "$typeStr" + + # Composite type: "Type1 | Type2" or "Type1 + Type2" + $parts = $typeString -split '\s*[|+]\s*' + + X "$indent<$tag$tagAttrs>" + foreach ($part in $parts) { + $part = $part.Trim() + Emit-SingleType -typeStr $part -indent "$indent`t" + } + X "$indent</$tag>" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + $typeStr = Resolve-TypeStr $typeStr + + # TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID + # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. + if ($typeStr -match '^typeid:([0-9a-fA-F-]{36})$') { + X "$indent<v8:TypeId>$($Matches[1])</v8:TypeId>" + return + } + + # boolean + if ($typeStr -eq "boolean") { + X "$indent<v8:Type>xs:boolean</v8:Type>" + return + } + + # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) + if ($typeStr -match '^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' } + X "$indent<v8:Type>xs:string</v8:Type>" + X "$indent<v8:StringQualifiers>" + X "$indent`t<v8:Length>$len</v8:Length>" + X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" + X "$indent</v8:StringQualifiers>" + return + } + + # decimal(D,F) or decimal(D,F,nonneg) + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indent<v8:Type>xs:decimal</v8:Type>" + X "$indent<v8:NumberQualifiers>" + X "$indent`t<v8:Digits>$digits</v8:Digits>" + X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" + X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" + X "$indent</v8:NumberQualifiers>" + return + } + + # date / dateTime / time + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + X "$indent<v8:Type>xs:dateTime</v8:Type>" + X "$indent<v8:DateQualifiers>" + X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" + X "$indent</v8:DateQualifiers>" + return + } + + # ValueTable, ValueTree, ValueList, etc. + $v8Types = @{ + "ValueTable" = "v8:ValueTable" + "ValueTree" = "v8:ValueTree" + "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription" + "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray" + "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($v8Types[$typeStr])</v8:Type>" + return + } + + # UI types + $uiTypes = @{ + "FormattedString" = "v8ui:FormattedString" + "Picture" = "v8ui:Picture" + "Color" = "v8ui:Color" + "Font" = "v8ui:Font" + } + if ($uiTypes.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($uiTypes[$typeStr])</v8:Type>" + return + } + + # DCS types + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ + "DataCompositionSettings" = "dcsset:DataCompositionSettings" + "DataCompositionSchema" = "dcssch:DataCompositionSchema" + "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" + } + if ($dcsMap.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($dcsMap[$typeStr])</v8:Type>" + return + } + } + + # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. + # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. (Дотированные формы + # ConstantsSet.X / ReportObject.X ловит общий cfg:-regex ниже.) + if ($typeStr -in @("DynamicList","ConstantsSet","ReportObject")) { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + return + } + + # TypeSet (набор типов) → <v8:TypeSet>: определяемый тип / характеристика (именованные) + # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. + if ($typeStr -match '^(DefinedType|Characteristic)\.') { + X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>" + return + } + if ($typeStr -match '^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$') { + X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>" + return + } + + # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + return + } + + # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на <v8:Type>). + # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. + # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, + # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. + $specialTypeNs = @{ + "mxl:SpreadsheetDocument" = "http://v8.1c.ru/8.2/data/spreadsheet" + "fd:FormattedDocument" = "http://v8.1c.ru/8.2/data/formatted-document" + "d5p1:TextDocument" = "http://v8.1c.ru/8.1/data/txtedt" + "d5p1:Chart" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:GanttChart" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:Dendrogram" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:FlowchartContextType" = "http://v8.1c.ru/8.2/data/graphscheme" + "d5p1:DataAnalysisTimeIntervalUnitType" = "http://v8.1c.ru/8.2/data/data-analysis" + "d5p1:GeographicalSchema" = "http://v8.1c.ru/8.2/data/geo" + "pdfdoc:PDFDocument" = "http://v8.1c.ru/8.3/data/pdf" + "pl:Planner" = "http://v8.1c.ru/8.3/data/planner" + } + if ($specialTypeNs.ContainsKey($typeStr)) { + $pref = $typeStr.Substring(0, $typeStr.IndexOf(':')) + X "$indent<v8:Type xmlns:$pref=`"$($specialTypeNs[$typeStr])`">$typeStr</v8:Type>" + return + } + + # Fallback with validation + if ($script:knownInvalidTypes.ContainsKey($typeStr)) { + throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" + } + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod). + if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') { + X "$indent<v8:Type>$typeStr</v8:Type>" + } elseif ($typeStr.Contains('.')) { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + } else { + Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" + X "$indent<v8:Type>$typeStr</v8:Type>" + } +} + +# --- 6. Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении" + "StartChoice" = "НачалоВыбора" + "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор" + "Clearing" = "Очистка" + "Opening" = "Открытие" + "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки" + "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением" + "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования" + "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки" + "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста" + "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания" + "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания" + "Drop" = "Помещение" + "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { + return "$elementName$suffix" + } + return "$elementName$eventName" +} + +# --- 7. Element emitters --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "radio" = @("OnChange") + "label" = @("Click","URLProcessing") + "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") + "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") + "pages" = @("OnCurrentPageChange") + "page" = @("OnCurrentPageChange") + "button" = @("Click") + "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") + "calendar" = @("OnChange","OnActivate") + "picture" = @("Click") + "cmdBar" = @() + "popup" = @() + "group" = @() +} +$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") + +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён). +function Get-EventPairs { + param($el, [string]$elementName) + $pairs = New-Object System.Collections.ArrayList + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { + $h = "$($p.Value)" + if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name } + [void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h }) + } + } elseif ($el.on) { + foreach ($evt in $el.on) { + $evtName = "$evt" + $h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName } + [void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h }) + } + } + return $pairs +} + +# Проверить, подключено ли событие к элементу (в любом из форматов). +function Test-ElementEvent { + param($el, [string]$eventName) + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } } + } + if ($el.on -contains $eventName) { return $true } + return $false +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent, [string]$typeKey) + + $pairs = Get-EventPairs -el $el -elementName $elementName + if ($pairs.Count -eq 0) { return } + + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($pr in $pairs) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") { + Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + + X "$indent<Events>" + foreach ($pr in $pairs) { + X "$indent`t<Event name=`"$($pr.name)`">$($pr.handler)</Event>" + } + X "$indent</Events>" +} + +# ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/флаги/hyperlink) +# вместо/вместе с текстом. Признак структурированной формы: объект с любым НЕ-текстовым ключом +# ({text,formatted} и языковые ключи {ru,en} → обычная текст-форма). +$script:companionStructKeys = @( + 'width','autoMaxWidth','maxWidth','height','autoMaxHeight','maxHeight','verticalAlign','titleHeight', + 'horizontalStretch','verticalStretch','horizontalAlign','groupHorizontalAlign','groupVerticalAlign', + 'visible','hidden','enabled','disabled','hyperlink','events','tooltip', + 'textColor','backColor','borderColor','font','border','цветтекста','цветфона','цветрамки','шрифт','рамка' +) +function Test-CompanionStructured { + param($content) + if (-not (($content -is [System.Collections.IDictionary]) -or ($content -is [System.Management.Automation.PSCustomObject]))) { return $false } + foreach ($k in $script:companionStructKeys) { + $present = if ($content -is [System.Collections.IDictionary]) { $content.Contains($k) } else { [bool]$content.PSObject.Properties[$k] } + if ($present) { return $true } + } + return $false +} + +function Emit-CompanionTitle { + param($content, [string]$indent) + $r = Resolve-MLFormatted $content + $fmt = if ($r.formatted) { 'true' } else { 'false' } + X "$indent<Title formatted=`"$fmt`">" + Emit-MLItems -val $r.text -indent "$indent`t" + X "$indent" +} + +# DisplayImportance — атрибут открывающего тега элемента (адаптивная важность: VeryHigh/High/Usual/Low/VeryLow). +# Возвращает ` DisplayImportance="X"` или "" (для companion-эмиттеров без $el → "" молча). +function DI-Attr { + param($el) + if ($null -ne $el -and $el.displayImportance) { return " DisplayImportance=`"$(Esc-Xml "$($el.displayImportance)")`"" } + return "" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent, $content = $null) + $id = New-Id + $hasContent = $null -ne $content -and -not ($content -is [string] -and "$content" -eq '') + if (-not $hasContent) { + X "$indent<$tag name=`"$name`" id=`"$id`"/>" + return + } + $inner = "$indent`t" + # DI-Attr берём от СОБСТВЕННОГО объекта компаньона ($content), НЕ от ambient $el родителя + # (PowerShell dynamic scope — иначе companion наследует DisplayImportance владельца: баг). + X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $content)>" + if (Test-CompanionStructured $content) { + # структурированная форма (own-content). Порядок как у платформы: own-content (флаги/hyperlink/ + # layout/оформление) ПЕРЕД Title (в корпусе layout-first 582 vs 10). + $txtPresent = if ($content -is [System.Collections.IDictionary]) { $content.Contains('text') } else { [bool]$content.PSObject.Properties['text'] } + Emit-CommonFlags -el $content -indent $inner + if ($content.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $content -indent $inner + Emit-Appearance -el $content -indent $inner -profile 'decoration' + if ($txtPresent) { Emit-CompanionTitle -content $content -indent $inner } + # ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration) + if ($content.tooltip) { Emit-MLText -tag "ToolTip" -text $content.tooltip -indent $inner } + # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) + Emit-Events -el $content -elementName $name -indent $inner -typeKey 'label' + } else { + Emit-CompanionTitle -content $content -indent $inner + } + X "$indent" +} + +# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] } +# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion). +# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar). +function Emit-CompanionPanel { + param([string]$tag, [string]$name, [string]$indent, $panel) + $id = New-Id + $autofill = $null + $children = $null + $halign = $null + if ($panel -is [array]) { + $children = $panel + } elseif ($null -ne $panel) { + if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill } + if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" } + $children = $panel.children + } + $hasChildren = $children -and @($children).Count -gt 0 + # Платформа пишет только при false; true = дефолт (тег опускается). + $emitAfFalse = ($autofill -eq $false) + if (-not $emitAfFalse -and -not $hasChildren -and -not $halign) { + X "$indent<$tag name=`"$name`" id=`"$id`"/>" + return + } + X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $panel)>" + if ($halign) { X "$indent`t$halign" } + if ($emitAfFalse) { X "$indent`tfalse" } + if ($hasChildren) { + X "$indent`t" + foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true } + X "$indent`t" + } + X "$indent" +} + +# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type. +$script:additionTypeMap = [ordered]@{ + 'searchString' = @{ Tag = 'SearchStringAddition'; Type = 'SearchStringRepresentation'; Suffix = 'СтрокаПоиска' } + 'viewStatus' = @{ Tag = 'ViewStatusAddition'; Type = 'ViewStatusRepresentation'; Suffix = 'СостояниеПросмотра' } + 'searchControl' = @{ Tag = 'SearchControlAddition'; Type = 'SearchControl'; Suffix = 'УправлениеПоиском' } +} +# Синонимы типа дополнения (для override-карты additions и резолва тип-ключа). +$script:additionKeySynonyms = @{ + 'searchString' = @('SearchStringAddition','SearchStringRepresentation','строкаПоиска','отображениеСтрокиПоиска') + 'viewStatus' = @('ViewStatusAddition','ViewStatusRepresentation','состояниеПросмотра') + 'searchControl' = @('SearchControlAddition','SearchControl','управлениеПоиском') +} + +# HorizontalLocation: auto (дефолт, тег опускаем) / left / right; forgiving + рус.синонимы. +function Get-HLocation { + param($el) + $v = if ($el -and $el.PSObject.Properties['horizontalLocation']) { $el.horizontalLocation } else { $null } + if (-not $v) { return $null } + switch -Regex ("$v".ToLower()) { + '^(auto|авто)$' { return $null } # дефолт — не эмитим + '^(left|слева|лево)$' { return 'Left' } + '^(right|справа|право)$' { return 'Right' } + '^(center|центр|по центру)$' { return 'Center' } + default { return "$v" } + } +} + +# Тело дополнения: AdditionSource + свойства (как у поля) + companions. $props может быть $null +# (стандартное дополнение без отклонений). Порядок 1С-толерантен (diff порядок-независим). +function Emit-AdditionBody { + param($props, [string]$source, [string]$srcType, [string]$addName, [string]$indent) + $inner = "$indent`t" + X "$inner" + X "$inner`t$source" + X "$inner`t$srcType" + X "$inner" + if ($props) { + if ($props.PSObject.Properties['title'] -and $props.title) { Emit-MLText -tag "Title" -text $props.title -indent $inner } + Emit-CommonFlags -el $props -indent $inner + if ($props.tooltip) { Emit-MLText -tag "ToolTip" -text $props.tooltip -indent $inner } + if ($props.tooltipRepresentation) { X "$inner$($props.tooltipRepresentation)" } + $hl = Get-HLocation $props; if ($hl) { X "$inner$hl" } + Emit-Layout -el $props -indent $inner + Emit-Appearance -el $props -indent $inner -profile 'field' + } + Emit-Companion -tag "ContextMenu" -name "${addName}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${addName}РасширеннаяПодсказка" -indent $inner +} + +# Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. +function Emit-Addition { + param($el, [string]$name, [int]$id, [string]$typeKey, [string]$indent) + $map = $script:additionTypeMap[$typeKey] + $source = if ($el.source) { "$($el.source)" } elseif ($script:currentTableName) { $script:currentTableName } else { '' } + X "$indent<$($map.Tag) name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + Emit-AdditionBody -props $el -source $source -srcType $map.Type -addName $name -indent $indent + X "$indent" +} + +# Стандартное табличное дополнение (авто-генерация на уровне таблицы). $override — объект отклонений +# из per-table карты additions (или $null = чистый дефолт). +function Emit-TableAddition { + param([string]$typeKey, [string]$tableName, [string]$indent, $override = $null) + $map = $script:additionTypeMap[$typeKey] + $addName = "$tableName$($map.Suffix)" + $id = New-Id + X "$indent<$($map.Tag) name=`"$addName`" id=`"$id`">" + Emit-AdditionBody -props $override -source $tableName -srcType $map.Type -addName $addName -indent $indent + X "$indent" +} + +# Прочитать override-объект для типа дополнения из per-table карты additions (с синонимами). +function Get-AdditionOverride { + param($additions, [string]$typeKey) + if ($null -eq $additions) { return $null } + foreach ($k in @($typeKey) + $script:additionKeySynonyms[$typeKey]) { + $p = $additions.PSObject.Properties[$k] + if ($p) { return $p.Value } + } + return $null +} + +function Emit-Element { + param($el, [string]$indent, [bool]$inCmdBar = $false) + + # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. + Normalize-PanelSynonyms $el + + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). + # Maps any synonym to canonical short DSL key. + # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя); + # объект/массив уже отнесён к панель-свойству выше. + $strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель') + $synonyms = @{ + "commandBar" = "cmdBar" + "autoCommandBar" = "autoCmdBar" + "КоманднаяПанель" = "cmdBar" + "InputField" = "input" + "ПолеВвода" = "input" + "CheckBoxField" = "check" + "ПолеФлажка" = "check" + "RadioButtonField" = "radio" + "ПолеПереключателя" = "radio" + "radioButton" = "radio" + "PictureField" = "picField" + "ПолеКартинки" = "picField" + "LabelField" = "labelField" + "ПолеНадписи" = "labelField" + "CalendarField" = "calendar" + "ПолеКалендаря" = "calendar" + "LabelDecoration" = "label" + "Надпись" = "label" + "PictureDecoration" = "picture" + "Картинка" = "picture" + "UsualGroup" = "group" + "Группа" = "group" + "ОбычнаяГруппа" = "group" + "ColumnGroup" = "columnGroup" + "ГруппаКолонок" = "columnGroup" + "Pages" = "pages" + "ГруппаСтраниц" = "pages" + "Page" = "page" + "Страница" = "page" + "Table" = "table" + "Таблица" = "table" + "Button" = "button" + "Кнопка" = "button" + "Popup" = "popup" + "ВсплывающееМеню" = "popup" + # Дополнения командной панели таблицы (тип-как-ключ) — forgiving: XML-тег/Type/рус.имя → канон + "SearchStringAddition" = "searchString" + "SearchStringRepresentation" = "searchString" + "строкаПоиска" = "searchString" + "отображениеСтрокиПоиска" = "searchString" + "Отображение строки поиска" = "searchString" + "ViewStatusAddition" = "viewStatus" + "ViewStatusRepresentation" = "viewStatus" + "состояниеПросмотра" = "viewStatus" + "Состояние просмотра" = "viewStatus" + "SearchControlAddition" = "searchControl" + "SearchControl" = "searchControl" + "управлениеПоиском" = "searchControl" + "Управление поиском" = "searchControl" + # Спец-поля (документ/датчик) — XML-имя/рус. → канон + "SpreadSheetDocumentField" = "spreadsheet" + "ПолеТабличногоДокумента" = "spreadsheet" + "HTMLDocumentField" = "html" + "ПолеHTMLДокумента" = "html" + "TextDocumentField" = "textDoc" + "ПолеТекстовогоДокумента" = "textDoc" + "FormattedDocumentField" = "formattedDoc" + "ПолеФорматированногоДокумента" = "formattedDoc" + "ProgressBarField" = "progressBar" + "ПолеИндикатора" = "progressBar" + "TrackBarField" = "trackBar" + "ПолеПолосыРегулирования" = "trackBar" + "ChartField" = "chart" + "ПолеДиаграммы" = "chart" + "GanttChartField" = "ganttChart" + "ПолеДиаграммыГанта" = "ganttChart" + "GraphicalSchemaField" = "graphicalSchema" + "ПолеГрафическойСхемы" = "graphicalSchema" + "PlannerField" = "planner" + "ПолеПланировщика" = "planner" + "PeriodField" = "periodField" + "ПолеПериода" = "periodField" + "DendrogramField" = "dendrogram" + "ПолеДендрограммы" = "dendrogram" + } + foreach ($pair in $synonyms.GetEnumerator()) { + if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { + if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue } + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + + # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. + # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. + foreach ($pn in @($el.PSObject.Properties.Name)) { + $norm = ($pn -replace '\s','').ToLower() + $canon = $script:propSynonyms[$norm] + if ($canon -and $pn -ne $canon) { + if ($null -eq $el.PSObject.Properties[$canon]) { + $val = $el.($pn) + $el | Add-Member -NotePropertyName $canon -NotePropertyValue $val -Force + } + $el.PSObject.Properties.Remove($pn) | Out-Null + } + } + + # Determine element type from key + $typeKey = $null + $xmlTag = $null + + # picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка + # у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. + # pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей + # (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. + foreach ($key in @("columnGroup","buttonGroup","pages","page","group","input","check","radio","label","labelField","table","button","calendar","cmdBar","popup","searchString","viewStatus","searchControl","picField","picture","spreadsheet","html","textDoc","formattedDoc","progressBar","trackBar","chart","ganttChart","graphicalSchema","planner","periodField","dendrogram")) { + if ($el.$key -ne $null) { + $typeKey = $key + break + } + } + + if (-not $typeKey) { + Write-Warning "Unknown element type, skipping" + return + } + + # Validate known keys — warn about typos and unknown properties + $knownKeys = @{ + # type keys + "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры + "spreadsheet"=1;"html"=1;"textDoc"=1;"formattedDoc"=1;"progressBar"=1;"trackBar"=1 + "chart"=1;"ganttChart"=1;"graphicalSchema"=1;"planner"=1;"periodField"=1;"dendrogram"=1;"ganttTable"=1 + "showPercent"=1;"largeStep"=1;"markingStep"=1;"step"=1 + "horizontalScrollBar"=1;"viewScalingMode"=1;"output"=1;"selectionShowMode"=1;"protection"=1 + "edit"=1;"showGrid"=1;"showGroups"=1;"showHeaders"=1;"showRowAndColumnNames"=1;"showCellNames"=1 + "pointerType"=1;"drawingSelectionShowMode"=1;"warningOnEditRepresentation"=1;"markingAppearance"=1 + # report-form контекст (generic-скаляры элементов) + "horizontalSpacing"=1;"representationInContextMenu"=1;"settingsNamedItemDetailedRepresentation"=1 + # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель + "itemHeight"=1;"dropListWidth"=1;"choiceButtonPicture"=1;"transparentPixel"=1 + # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы + "titleDataPath"=1;"extendedEdit"=1;"maxRowsCount"=1;"autoMaxRowsCount"=1;"heightControlVariant"=1 + "warningOnEdit"=1;"nonselectedPictureText"=1;"editTextUpdate"=1;"footerText"=1 + # columnGroup-specific + "showInHeader"=1 + # radio-specific + "radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1 + # naming & binding + "name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1 + # companion-панели (свойства): командная панель + контекстное меню + "commandBar"=1;"contextMenu"=1 + # источник команд группы/панели (ButtonGroup/CommandBar) + "commandSource"=1 + # visibility & state + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 + # events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости) + "events"=1;"on"=1;"handlers"=1 + # layout + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + "maxWidth"=1;"maxHeight"=1 + "groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1 + # input-specific + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + "textEdit"=1 + "wrap"=1;"openButton"=1;"listChoiceMode"=1;"showInFooter"=1 + "extendedEditMultipleValues"=1;"chooseType"=1;"autoCellHeight"=1 + "choiceButtonRepresentation"=1;"footerHorizontalAlign"=1;"headerHorizontalAlign"=1 + "headerDataPath"=1;"headerFormat"=1;"currentRowUse"=1 + "format"=1;"editFormat"=1;"choiceParameters"=1;"choiceParameterLinks"=1;"typeLink"=1 + # label/hyperlink + "hyperlink"=1;"formatted"=1 + # group-specific + "collapsedTitle"=1;"showTitle"=1;"united"=1;"collapsed"=1;"behavior"=1 + # hierarchy + "children"=1;"columns"=1 + # table-specific + "changeRowSet"=1;"changeRowOrder"=1;"autoInsertNewRow"=1;"rowFilter"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1;"viewStatusLocation"=1;"searchControlLocation"=1 + "excludedCommands"=1 + "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 + "rowPictureDataPath"=1;"tableAutofill"=1;"heightInTableRows"=1 + "multipleChoice"=1;"searchOnInput"=1;"shortcut"=1 + "rowSelectionMode"=1;"verticalLines"=1;"horizontalLines"=1 + # dynamic-list table block + "defaultItem"=1;"useAlternationRowColor"=1;"fileDragMode"=1;"autoRefresh"=1 + "autoRefreshPeriod"=1;"choiceFoldersAndItems"=1;"restoreCurrentRow"=1;"showRoot"=1 + "allowRootChoice"=1;"updateOnDataChange"=1;"allowGettingCurrentRowURL"=1 + "userSettingsGroup"=1;"rowsPicture"=1 + # calendar-specific + "selectionMode"=1;"showCurrentDate"=1;"widthInMonths"=1;"heightInMonths"=1;"showMonthsPanel"=1 + # pages-specific + "pagesRepresentation"=1 + # button-specific + "type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"parameter"=1;"defaultButton"=1;"locationInCommandBar"=1;"displayImportance"=1 + # picture/decoration + "src"=1;"valuesPicture"=1;"loadTransparent"=1;"headerPicture"=1;"footerPicture"=1 + # cmdBar-specific + "autofill"=1 + # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице + "autoCmdBar"=1 + # дополнения командной панели таблицы (тип-ключи + свойства) + "searchString"=1;"viewStatus"=1;"searchControl"=1;"source"=1;"horizontalLocation"=1;"additions"=1 + # generic-скаляры (pass-through) + точечные + "verticalAlign"=1;"throughAlign"=1;"enableContentChange"=1;"pictureSize"=1;"titleHeight"=1 + "childItemsWidth"=1;"showLeftMargin"=1;"cellHyperlink"=1;"viewMode"=1;"verticalScrollBar"=1 + "rowInputMode"=1;"mask"=1;"createButton"=1;"fixingInTable"=1;"verticalSpacing"=1 + # InputField choice-скаляры + "choiceListButton"=1;"quickChoice"=1;"autoChoiceIncomplete"=1 + "choiceForm"=1;"choiceHistoryOnInput"=1;"footerDataPath"=1;"minValue"=1;"maxValue"=1 + # Button — пометка toggle-кнопки (ключ 'checked', не 'check' — во избежание конфликта с типом) + "checked"=1 + } + # Оформление (цвета/шрифты/граница) — авто-регистрация из самих структур, чтобы allowlist + # не дрейфовал при добавлении новых ключей/синонимов. Канонические + forgiving-синонимы. + foreach ($k in $script:appearanceSpec.Keys) { $knownKeys[$k] = 1 } + foreach ($k in $script:appearanceSynonyms.Keys) { $knownKeys[$k] = 1 } + foreach ($k in $script:propSynonyms.Keys) { $knownKeys[$k] = 1 } + foreach ($p in $el.PSObject.Properties) { + if ($p.Name -like '_*') { continue } # внутренние маркеры (напр. _dynList) + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." + } + } + + $name = Get-ElementName -el $el -typeKey $typeKey + Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element' + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } + "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "searchString" { Emit-Addition -el $el -name $name -typeKey "searchString" -id $id -indent $indent } + "viewStatus" { Emit-Addition -el $el -name $name -typeKey "viewStatus" -id $id -indent $indent } + "searchControl" { Emit-Addition -el $el -name $name -typeKey "searchControl" -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + "spreadsheet" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "SpreadSheetDocumentField" -typeKey "spreadsheet" } + "html" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "HTMLDocumentField" -typeKey "html" } + "textDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TextDocumentField" -typeKey "textDoc" } + "formattedDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "FormattedDocumentField" -typeKey "formattedDoc" } + "progressBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ProgressBarField" -typeKey "progressBar" } + "trackBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TrackBarField" -typeKey "trackBar" } + "chart" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ChartField" -typeKey "chart" } + "graphicalSchema" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "GraphicalSchemaField" -typeKey "graphicalSchema" } + "planner" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PlannerField" -typeKey "planner" } + "periodField" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PeriodField" -typeKey "periodField" } + "dendrogram" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "DendrogramField" -typeKey "dendrogram" } + "ganttChart" { Emit-GanttChart -el $el -name $name -id $id -indent $indent } + } +} + +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +function Emit-XrFlag { + param([string]$tag, $val, [string]$indent) + if ($null -eq $val) { return } + if ($val -is [bool]) { + X "$indent<$tag>" + X "$indent`t$(if ($val){'true'}else{'false'})" + X "$indent" + return + } + # объектная форма { common, roles } + $common = if ($null -ne $val.common) { [bool]$val.common } else { $false } + X "$indent<$tag>" + X "$indent`t$(if ($common){'true'}else{'false'})" + if ($val.roles) { + foreach ($r in $val.roles.PSObject.Properties) { + # Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". + # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. + $rname = "$($r.Name)" -replace '^(Role|Роль)\.', '' + if ($rname -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $rname = "Role.$rname" } + $rval = if ([bool]$r.Value) { 'true' } else { 'false' } + X "$indent`t$rval" + } + } + X "$indent" +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } + if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с +# историческим выводом input/label, чтобы не сдвигать существующие снапшоты. +# -skipHeight: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). +# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. +# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. +function Emit-CommonElementProps { + param($el, [string]$indent) + if ($el.defaultItem -eq $true) { X "$indenttrue" } + if ($el.PSObject.Properties['skipOnInput'] -and $null -ne $el.skipOnInput) { + $siv = if ($el.skipOnInput -eq $true) { 'true' } else { 'false' } + X "$indent$siv" + } + # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + if ($null -ne $el.enableStartDrag) { X "$indent$(if ($el.enableStartDrag){'true'}else{'false'})" } + if ($el.fileDragMode) { X "$indent$($el.fileDragMode)" } + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + foreach ($p in @(@('showInHeader','ShowInHeader'), @('showInFooter','ShowInFooter'), @('autoCellHeight','AutoCellHeight'))) { + if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } + } + # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) + if ($el.headerDataPath) { X "$indent$(Esc-Xml "$($el.headerDataPath)")" } + if ($el.footerHorizontalAlign) { X "$indent$($el.footerHorizontalAlign)" } + if ($el.headerHorizontalAlign) { X "$indent$($el.headerHorizontalAlign)" } + # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) + if ($el.headerFormat) { Emit-MLText -tag "HeaderFormat" -text $el.headerFormat -indent $indent } +} + +# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). +# Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). +# Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. +# src с префиксом "abs:" → встроенная картинка ; иначе именованная/стилевая . +function Emit-PictureRef { + param($val, [string]$picTag, [string]$indent) + if (-not $val) { return } + $src = $null; $lt = $false; $tpx = $null + if ($val -is [string]) { $src = $val } + else { $src = $val.src; if ($val.loadTransparent -eq $true) { $lt = $true }; $tpx = $val.transparentPixel } + if (-not $src) { return } + $srcStr = "$src" + X "$indent<$picTag>" + if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } + else { X "$indent`t$(Esc-Xml $srcStr)" } + X "$indent`t$(if ($lt) { 'true' } else { 'false' })" + if ($tpx) { X "$indent`t" } + X "$indent" +} + +# Картинки заголовка/подвала колонки поля — по схеме сразу после , +# перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь). +function Emit-ColumnPics { + param($el, [string]$indent) + Emit-PictureRef -val $el.headerPicture -picTag 'HeaderPicture' -indent $indent + Emit-PictureRef -val $el.footerPicture -picTag 'FooterPicture' -indent $indent +} + +# кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false +# (обратная конвенция относительно header/values-картинок). Прощающий ввод: +# принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель +# опишет картинку объектно по аналогии с headerPicture. $elemLt — legacy +# элемент-уровневый ключ loadTransparent (используется, если в объекте флаг не задан). +function Emit-CommandPicture { + param($pic, $elemLt, [string]$indent) + if (-not $pic) { return } + $src = $null; $lt = $null; $tpx = $null + if ($pic -is [string]) { $src = $pic } + else { $src = $pic.src; if ($null -ne $pic.loadTransparent) { $lt = [bool]$pic.loadTransparent }; $tpx = $pic.transparentPixel } + if (-not $src) { return } + if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt } + $srcStr = "$src" + X "$indent" + if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } + else { X "$indent`t$(Esc-Xml $srcStr)" } + X "$indent`t$(if ($lt -eq $false) { 'false' } else { 'true' })" + if ($tpx) { X "$indent`t" } + X "$indent" +} + +# --- Оформление элемента: цвета / шрифты / граница --- +# Прямые свойства элемента (// + header/footer-варианты у полей). +# Ключи DSL — англ. camelCase 1:1 с тегами; принимаем рус. синонимы (forgiving). +# Значения: цвет — verbatim-строка (style:/web:/sys:/win:/#RRGGBB); шрифт — строка-ref или +# объект-атрибуты; граница — строка-ref или объект {width,style|ref}. Порядок тегов — XSD (профиль). +$script:appearanceSpec = @{ + titleTextColor = @{ tag='TitleTextColor'; kind='color' } + titleBackColor = @{ tag='TitleBackColor'; kind='color' } + titleFont = @{ tag='TitleFont'; kind='font' } + footerTextColor = @{ tag='FooterTextColor'; kind='color' } + footerBackColor = @{ tag='FooterBackColor'; kind='color' } + footerFont = @{ tag='FooterFont'; kind='font' } + textColor = @{ tag='TextColor'; kind='color' } + backColor = @{ tag='BackColor'; kind='color' } + borderColor = @{ tag='BorderColor'; kind='color' } + border = @{ tag='Border'; kind='border'} + font = @{ tag='Font'; kind='font' } +} +# Рус. синоним (lower) → canonical key +$script:appearanceSynonyms = @{ + 'цветтекста'='textColor'; 'цветфона'='backColor'; 'цветрамки'='borderColor' + 'цветтекстазаголовка'='titleTextColor'; 'цветфоназаголовка'='titleBackColor'; 'шрифтзаголовка'='titleFont' + 'цветтекстаподвала'='footerTextColor'; 'цветфонаподвала'='footerBackColor'; 'шрифтподвала'='footerFont' + 'шрифт'='font'; 'рамка'='border' +} +# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. +# Ключи карты нормализованы (lowercase, без пробелов); сопоставление в Normalize-PropSynonyms тоже. +# Прощающий ввод: модель может писать свойство по-русски. Англ. ключ работает всегда (это доп. слой). +# Видимость/Доступность НЕ включаем — наш hidden/disabled инвертирован, был бы баг семантики. +$script:propSynonyms = @{ + 'пометка'='checked' + 'кнопкавыбора'='choiceButton'; 'кнопкаочистки'='clearButton'; 'кнопкарегулирования'='spinButton' + 'кнопкавыпадающегосписка'='dropListButton'; 'кнопкасписковоговыбора'='choiceListButton' + 'кнопкаоткрытия'='openButton'; 'кнопкапоумолчанию'='defaultButton' + 'быстрыйвыбор'='quickChoice'; 'формавыбора'='choiceForm'; 'историявыборапривводе'='choiceHistoryOnInput' + 'выборгруппиэлементов'='choiceFoldersAndItems'; 'фиксациявтаблице'='fixingInTable' + 'путькданнымподвала'='footerDataPath'; 'автоотметканезаполненного'='markIncomplete' + 'многострочныйрежим'='multiLine'; 'режимпароля'='passwordMode'; 'переноспословам'='wrap' + 'расположениезаголовка'='titleLocation'; 'пропускатьпривводе'='skipOnInput' + 'заголовок'='title'; 'ширина'='width'; 'высота'='height'; 'подсказкаввода'='inputHint' +} +# Профили порядка тегов по базовым типам (XSD-последовательность) +$script:appOrderField = @('titleTextColor','titleBackColor','titleFont','footerTextColor','footerBackColor','footerFont','textColor','backColor','borderColor','border','font') +$script:appOrderDecoration = @('textColor','font','backColor','borderColor','border') +$script:appOrderButton = @('textColor','backColor','borderColor','font') + +# Простые скаляры элемента (pass-through: captured/emitted «как есть»). Только НЕ-перекрывающиеся +# теги (не обрабатываемые специфично где-то ещё). kind: bool → true/false; value → строка verbatim. +$script:genericScalars = @( + @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } + @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } + @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } + @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } + @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } + @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } + @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } + @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } + @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } + @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } + @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } + @{ Tag='Mask'; Key='mask'; Kind='value' } + @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } + @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } + @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } + # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through + @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } + @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } + @{ Tag='Output'; Key='output'; Kind='value' } + @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } + @{ Tag='PointerType'; Key='pointerType'; Kind='value' } + @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } + @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } + @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } + @{ Tag='Protection'; Key='protection'; Kind='bool' } + @{ Tag='Edit'; Key='edit'; Kind='bool' } + @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } + @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } + @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } + @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } + @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } + @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } + @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } + @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } + @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } + @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } + @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } + @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } + @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } + @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } + # Корпусный хвост: представление управления свёрткой группы / форма кнопки-попапа / + # авто-добавление незаполненной строки / выделение отрицательных / нач. позиция списка / + # высота списка выбора / три состояния флажка / прокрутка страницы при сжатии + @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } + @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } + @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } + @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } + @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } + @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } + @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } + @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } + # Сочетание клавиш — общее свойство (input/group/radio/page/picField/label/table/check; команда — отд. путь, §7) + @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } + # Батч простых скаляров (input/radio/group/picDecoration/button): режим выбора незаполненного, + # равная ширина колонок, выравнивание детей, масштаб/зум картинки, форма/положение картинки кнопки. + # (Table HeaderHeight/FooterHeight/CurrentRowUse — НЕ здесь, а в Emit-Table: pass-through, + # 1С толерантна к порядку детей Table — в корпусе те же теги встречаются в разных позициях.) + @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } + @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } + @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } + @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } + @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } + @{ Tag='Shape'; Key='shape'; Kind='value' } + @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } + @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } + # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр + @{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' } + # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) + @{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' } + @{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' } + @{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' } + @{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' } + # Режим авто-показа кнопок открытия/очистки (input, enum Auto/Always/FilledOnly/…) + @{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' } + @{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' } + # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент, не атрибуты) + @{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' } + @{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' } + @{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' } + @{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' } + # Хвост листовых скаляров (по 1 в корпусе): автокоррекция ввода (input) / уникальность команды + # (button) / допуск пустого множ. значения (input) / поведение при гориз. сжатии (table) + @{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' } + @{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' } + @{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' } + @{ Tag='AllowInputEmptyMultipleValues';Key='allowInputEmptyMultipleValues';Kind='bool' } + @{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' } +) + +function Emit-GenericScalars { + param($el, [string]$indent) + if ($null -eq $el) { return } + foreach ($s in $script:genericScalars) { + $p = $el.PSObject.Properties[$s.Key] + if (-not $p -or $null -eq $p.Value) { continue } + if ($s.Kind -eq 'bool') { + X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})" + } else { + $v = "$($p.Value)"; if ($v -eq '') { continue } + X "$indent<$($s.Tag)>$(Esc-Xml $v)" + } + } +} + +function Get-AppearanceValue { + param($el, [string]$canonical) + if ($null -eq $el) { return $null } + $p = $el.PSObject.Properties[$canonical] + if ($p) { return $p.Value } + foreach ($syn in $script:appearanceSynonyms.Keys) { + if ($script:appearanceSynonyms[$syn] -eq $canonical) { + $pp = $el.PSObject.Properties[$syn] + if ($pp) { return $pp.Value } + } + } + return $null +} + +# — строка = ref на стиль (kind=StyleItem); объект = атрибуты. +function Emit-FontTag { + param([string]$tag, $val, [string]$indent) + if ($val -is [string]) { + X "$indent<$tag ref=`"$(Esc-Xml $val)`" kind=`"StyleItem`"/>" + return + } + $attrs = @() + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $pp = $val.PSObject.Properties[$a] + if ($pp -and $null -ne $pp.Value) { + $v = $pp.Value + if ($v -is [bool]) { $v = if ($v) {'true'} else {'false'} } + $attrs += "$a=`"$(Esc-Xml "$v")`"" + } + } + X "$indent<$tag $($attrs -join ' ')/>" +} + +# — строка/{ref} = из стиля (); {width,style} = явная. +function Emit-BorderTag { + param($val, [string]$indent) + if ($val -is [string]) { X "$indent"; return } + $refP = $val.PSObject.Properties['ref'] + if ($refP -and $refP.Value) { X "$indent"; return } + $width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 } + $style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null } + X "$indent" + if ($style) { X "$indent`t$(Esc-Xml $style)" } + X "$indent" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time — встроенный конфиг планировщика +# на реквизите planner-типа. Структурный DSL: items[] + appearance/поведение-скаляры + +# timeScale (уровни шкалы времени) + period. Каждое присутствующее поле → каноничный +# порядок; пропущенное → дефолт (планировщик всегда несёт полный блок). Декомпилятор +# делает полный захват → раундтрип бит-в-бит; ручной авторинг может быть кратким. +$script:PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' +$script:CHART_NS = 'http://v8.1c.ru/8.2/data/chart' + +function PL-Get { + param($o, [string]$k, $def = $null) + if ($null -ne $o -and $o.PSObject.Properties[$k] -and $null -ne $o.$k) { return $o.$k } + return $def +} +function PL-Bool { + param($v) + if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } + elseif ("$v" -eq 'True') { 'true' } + elseif ("$v" -eq 'False') { 'false' } + else { "$v" } +} +function Emit-PlannerColor { + param([string]$tag, $o, [string]$key, [string]$ind) + X "$ind$(Esc-Xml "$(PL-Get $o $key 'auto')")" +} +# /… — пустое → самозакрывающийся тег (как в выгрузке платформы). +function Emit-PlannerText { + param([string]$tag, $v, [string]$ind) + if ([string]::IsNullOrEmpty("$v")) { X "$ind" } + else { X "$ind$(Esc-Xml "$v")" } +} +# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef"; +# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы. +function Test-PlannerRef { + param([string]$v) + return ($v -match '^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' -or ` + $v -match '\.EnumValue\.' -or $v -match 'EmptyRef$' -or ` + $v -match '^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') +} +# — nil (нет значения) / xr:DesignTimeRef (ссылка) / xs:string (строка/прочее). +function Emit-PlannerValue { + param($v, [string]$ind) + if ($null -eq $v -or "$v" -eq '') { X "$ind"; return } + $t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' } + X "$ind$(Esc-Xml "$v")" +} +function Emit-PlannerFont { + param($o, [string]$ind) + $f = PL-Get $o 'font' $null + if ($null -eq $f) { X "$ind"; return } + Emit-FontTag -tag 'pl:font' -val $f -indent $ind +} +function Emit-PlannerBorder { + param($o, [string]$ind, [string]$key = 'border') + $b = PL-Get $o $key $null + $bw = if ($b) { PL-Get $b 'width' 1 } else { 1 } + $bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' } + X "$ind" + X "$ind`t$(Esc-Xml "$bs")" + X "$ind" +} +function Emit-PlannerLevel { + param($lv, [string]$cns, [string]$ind) + $li = "$ind`t" + X "$ind" + X "$li$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")" + X "$li$(PL-Get $lv 'interval' 1)" + X "$li$(PL-Bool (PL-Get $lv 'show' $true))" + $line = PL-Get $lv 'line' $null + $lw = if ($line) { PL-Get $line 'width' 1 } else { 1 } + $lg = if ($line) { PL-Get $line 'gap' $false } else { $false } + $lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' } + X "$li" + X "$li`t$(Esc-Xml "$lst")" + X "$li" + X "$li$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")" + X "$li$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")" + $fmt = PL-Get $lv 'format' $null + if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } } + X "$li" + Emit-MLItems -val $fmt -indent "$li`t" + X "$li" + $labels = PL-Get $lv 'labels' $null + $ticks = if ($labels) { PL-Get $labels 'ticks' 0 } else { 0 } + X "$li" + X "$li`t$ticks" + X "$li" + X "$li$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")" + X "$li$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")" + X "$li$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))" + X "$ind" +} +function Emit-PlannerTimeScale { + param($ts, [string]$ind) + $cns = $script:CHART_NS + $ci = "$ind`t" + X "$ind" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")" + $levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() } + if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт + foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci } + $transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false } + X "$ci$(PL-Bool $transp)" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")" + X "$ci$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })" + X "$ind" +} +function Emit-PlannerItem { + param($it, [string]$ind) + X "$ind" + $ii = "$ind`t" + Emit-PlannerValue (PL-Get $it 'value' $null) $ii + Emit-PlannerText 'text' (PL-Get $it 'text' '') $ii + Emit-PlannerText 'tooltip' (PL-Get $it 'tooltip' '') $ii + X "$ii$(PL-Get $it 'begin' '0001-01-01T00:00:00')" + X "$ii$(PL-Get $it 'end' '0001-01-01T00:00:00')" + Emit-PlannerColor 'borderColor' $it 'borderColor' $ii + Emit-PlannerColor 'backColor' $it 'backColor' $ii + Emit-PlannerColor 'textColor' $it 'textColor' $ii + Emit-PlannerFont $it $ii + X "$ii" + X "$ii$(PL-Get $it 'replacementDate' '0001-01-01T00:00:00')" + X "$ii$(PL-Bool (PL-Get $it 'deleted' $false))" + $id = PL-Get $it 'id' $null + if ($null -eq $id) { $id = [guid]::NewGuid().ToString() } + X "$ii$id" + X "$ii$(PL-Bool (PL-Get $it 'textFormatted' $false))" + Emit-PlannerBorder $it $ii 'border' + X "$ii$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")" + X "$ind" +} +# Элемент измерения ( внутри ) — рекурсивен: может нести вложенные +# элементы (UI: колонка «Элементы» у элемента). Порядок: value, text, цвета, font, +# вложенные элементы, showOnlySubordinatesAreas, textFormatted. +function Emit-PlannerDimElement { + param($el, [string]$ind) + X "$ind" + $ii = "$ind`t" + Emit-PlannerValue (PL-Get $el 'value' $null) $ii + Emit-PlannerText 'text' (PL-Get $el 'text' '') $ii + Emit-PlannerColor 'borderColor' $el 'borderColor' $ii + Emit-PlannerColor 'backColor' $el 'backColor' $ii + Emit-PlannerColor 'textColor' $el 'textColor' $ii + Emit-PlannerFont $el $ii + foreach ($sub in @(PL-Get $el 'elements' @())) { Emit-PlannerDimElement $sub $ii } + X "$ii$(PL-Bool (PL-Get $el 'showOnlySubordinatesAreas' $true))" + X "$ii$(PL-Bool (PL-Get $el 'textFormatted' $false))" + X "$ind" +} +# Измерение планировщика () — объект разреза + его элементы. +function Emit-PlannerDimension { + param($d, [string]$ind) + X "$ind" + $di = "$ind`t" + Emit-PlannerValue (PL-Get $d 'value' $null) $di + Emit-PlannerText 'text' (PL-Get $d 'text' '') $di + Emit-PlannerColor 'borderColor' $d 'borderColor' $di + Emit-PlannerColor 'backColor' $d 'backColor' $di + Emit-PlannerColor 'textColor' $d 'textColor' $di + Emit-PlannerFont $d $di + foreach ($el in @(PL-Get $d 'elements' @())) { Emit-PlannerDimElement $el $di } + X "$di$(PL-Bool (PL-Get $d 'textFormatted' $false))" + X "$ind" +} +function Emit-PlannerSettings { + param($pl, [string]$ind) + X "$ind" + $si = "$ind`t" + foreach ($it in @(PL-Get $pl 'items' @())) { Emit-PlannerItem $it $si } + foreach ($d in @(PL-Get $pl 'dimensions' @())) { Emit-PlannerDimension $d $si } + Emit-PlannerColor 'borderColor' $pl 'borderColor' $si + Emit-PlannerColor 'backColor' $pl 'backColor' $si + Emit-PlannerColor 'textColor' $pl 'textColor' $si + Emit-PlannerColor 'lineColor' $pl 'lineColor' $si + Emit-PlannerFont $pl $si + X "$si$(PL-Get $pl 'beginOfRepresentationPeriod' '0001-01-01T00:00:00')" + X "$si$(PL-Get $pl 'endOfRepresentationPeriod' '0001-01-01T00:00:00')" + X "$si$(PL-Bool (PL-Get $pl 'alignElementsOfTimeScale' $true))" + X "$si$(PL-Bool (PL-Get $pl 'displayTimeScaleWrapHeaders' $true))" + X "$si$(PL-Bool (PL-Get $pl 'displayWrapHeaders' $true))" + $wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null + if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } } + Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si + X "$si$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")" + X "$si$(PL-Get $pl 'periodicVariantRepetition' 1)" + X "$si$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)" + X "$si$(PL-Get $pl 'timeScaleWrapEndIndent' 0)" + Emit-PlannerTimeScale (PL-Get $pl 'timeScale' $null) $si + $period = PL-Get $pl 'period' $null + if ($period) { + X "$si" + X "$si`t$(PL-Get $period 'begin' '0001-01-01T00:00:00')" + X "$si`t$(PL-Get $period 'end' '0001-01-01T00:00:00')" + X "$si" + } + X "$si$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))" + X "$si$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")" + X "$si$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")" + X "$si$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))" + X "$si$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))" + X "$si$(PL-Get $pl 'minColumnWidth' 0)" + X "$si$(PL-Get $pl 'minRowHeight' 0)" + X "$si$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")" + X "$si$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")" + Emit-PlannerBorder $pl $si 'border' + X "$si$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")" + X "$ind" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time — генерик-эмиттер (зеркало +# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии +# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands), +# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип). +$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } +$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } +$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale') +function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } } +function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } } +function Emit-ChartNode { + param([string]$name, $val, [string]$ind) + if ($script:CHART_ML_FIELDS.Contains($name)) { + if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } + X "$ind"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind"; return + } + if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) { + foreach ($e in $val) { Emit-ChartNode $name $e $ind } + return + } + if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) { + $keys = Get-Keys $val + if ($script:CHART_ATTR_FIELDS.Contains($name)) { + $attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" } + X "$ind"; return + } + if ($keys -contains 'gap') { + $w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style' + X "$ind" + X "$ind`t$(Esc-Xml "$st")" + X "$ind"; return + } + if (($keys -contains 'style') -and ($keys -contains 'width')) { + $w = Get-Prop $val 'width'; $st = Get-Prop $val 'style' + X "$ind" + X "$ind`t$(Esc-Xml "$st")" + X "$ind"; return + } + $isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } } + if ($isFont) { + $attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } } + X "$ind"; return + } + if (@($keys).Count -eq 0) { X "$ind"; return } + X "$ind" + foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" } + X "$ind" + return + } + if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } + if ($val -is [bool]) { X "$ind$(PL-Bool $val)"; return } + X "$ind$(Esc-Xml "$val")" +} +function Emit-ChartSettings { + param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart') + X "$ind" + foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" } + X "$ind" +} + +function Emit-Appearance { + param($el, [string]$indent, [string]$profile = 'field') + if ($null -eq $el) { return } + $order = switch ($profile) { + 'decoration' { $script:appOrderDecoration } + 'button' { $script:appOrderButton } + default { $script:appOrderField } + } + foreach ($key in $order) { + $val = Get-AppearanceValue -el $el -canonical $key + if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } + $spec = $script:appearanceSpec[$key] + switch ($spec.kind) { + 'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")" } + 'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent } + 'border' { Emit-BorderTag -val $val -indent $indent } + } + } +} + +function Emit-Layout { + param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false) + # CommandSet (отключённые команды редактора) — общее свойство поля (input/label/check/ + # spreadsheet/html/formatted/picture); в схеме рано (после TitleLocation, перед скалярами). + if ($el.excludedCommands -and @($el.excludedCommands).Count -gt 0) { + X "$indent" + foreach ($cmd in $el.excludedCommands) { X "$indent`t$cmd" } + X "$indent" + } + Emit-CommonElementProps -el $el -indent $indent + $amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth') + if ($amwExplicit) { + if ($el.autoMaxWidth -eq $false) { X "$indentfalse" } + } elseif ($multiLineDefault) { + X "$indentfalse" + } + if ($null -ne $el.maxWidth) { X "$indent$($el.maxWidth)" } + if ($el.autoMaxHeight -eq $false) { X "$indentfalse" } + if ($null -ne $el.maxHeight) { X "$indent$($el.maxHeight)" } + if ($el.width) { X "$indent$($el.width)" } + if (-not $skipHeight -and $el.height) { X "$indent$($el.height)" } + if ($null -ne $el.horizontalStretch) { X "$indent$(if ($el.horizontalStretch){'true'}else{'false'})" } + if ($null -ne $el.verticalStretch) { X "$indent$(if ($el.verticalStretch){'true'}else{'false'})" } + if ($el.groupHorizontalAlign) { X "$indent$($el.groupHorizontalAlign)" } + if ($el.groupVerticalAlign) { X "$indent$($el.groupVerticalAlign)" } + if ($el.horizontalAlign) { X "$indent$($el.horizontalAlign)" } + Emit-GenericScalars -el $el -indent $indent +} + +function Title-FromName { + param([string]$name) + if (-not $name) { return '' } + $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') + $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') + $parts = $s -split ' ' + if ($parts.Count -eq 0) { return $s } + $out = New-Object System.Collections.ArrayList + [void]$out.Add($parts[0]) + for ($i = 1; $i -lt $parts.Count; $i++) { + $p = $parts[$i] + if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { + [void]$out.Add($p) + } else { + [void]$out.Add($p.ToLower()) + } + } + return ($out -join ' ') +} + +function Emit-Title { + # Нет ключа title → авто-вывод из имени (помощь модели). + # Явный title: "" (или null) → подавить (заголовок не эмитим). + # Явный непустой → эмитим как есть. + param($el, [string]$name, [string]$indent, [switch]$auto) + $hasKey = $null -ne $el.PSObject.Properties['title'] + if ($hasKey) { + if ($el.title) { Emit-MLText -tag "Title" -text $el.title -indent $indent } + } elseif ($auto -and $name) { + Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent + } + # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. + if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } + # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. + if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } +} + +function Map-TitleLoc { + param([string]$v) + switch ("$v".ToLower()) { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + "auto" { "Auto" } + default { "$v" } + } +} + +# TitleLocation у check/radio: нет ключа → умный дефолт (Right/None), эмитится; +# "" → подавить (= дефолт платформы, она его сама не пишет); значение → эмитить (маппинг регистра). +function Emit-TitleLocation { + param($el, [string]$indent, [string]$smartDefault) + if ($null -ne $el.PSObject.Properties['titleLocation']) { + if ($el.titleLocation) { X "$indent$(Map-TitleLoc "$($el.titleLocation)")" } + } elseif ($smartDefault) { + X "$indent$smartDefault" + } +} + +function Warn-Unrecognized { + # drop-on-miss enum: значение не распознано → тег не эмитится. Громко, чтобы автор увидел потерю. + param([string]$key, $raw, [string[]]$valid, [string]$owner) + Write-Warning "Unrecognized $key '$raw' on '$owner'. Valid values: $($valid -join ', '). Value ignored." +} + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. + $groupVal = "$($el.group)".ToLower() + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwayshorizontal" { "AlwaysHorizontal" } + "alwaysvertical" { "AlwaysVertical" } + "horizontalifpossible" { "HorizontalIfPossible" } + "collapsible" { "Vertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + elseif ($groupVal) { Warn-Unrecognized 'group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name } + + # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). + # Legacy: group:'collapsible' эквивалентно behavior:'collapsible'. + $behaviorVal = if ($el.behavior) { "$($el.behavior)".ToLower() } elseif ($groupVal -eq "collapsible") { "collapsible" } else { $null } + $bmap = @{ "usual"="Usual"; "collapsible"="Collapsible"; "popup"="PopUp" } + if ($behaviorVal -and $bmap.ContainsKey($behaviorVal)) { + X "$inner$($bmap[$behaviorVal])" + } elseif ($el.behavior -and -not $bmap.ContainsKey($behaviorVal)) { + Warn-Unrecognized 'behavior' $el.behavior @('collapsible','popup') $name + } + # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) + if ($el.collapsed -eq $true) { X "$innertrue" } + + # Representation + if ($el.representation) { + $repr = switch ("$($el.representation)") { + "none" { "None" } + "normal" { "NormalSeparation" } + "weak" { "WeakSeparation" } + "strong" { "StrongSeparation" } + default { "$($el.representation)" } + } + X "$inner$repr" + } + + # Использование текущей строки группы (после Representation, порядок XSD) + if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } + + # ShowTitle + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст + if ($el.collapsedTitle) { Emit-MLText -tag "CollapsedRepresentationTitle" -text $el.collapsedTitle -indent $inner } + + # United + if ($el.united -eq $false) { X "$innerfalse" } + + # Формат значения пути к данным заголовка (; парный к titleDataPath группы) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-ColumnGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation (horizontal / vertical / inCell — последнее только здесь) + $groupVal = "$($el.columnGroup)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "inCell" { "InCell" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + elseif ($groupVal) { Warn-Unrecognized 'columnGroup orientation' $el.columnGroup @('vertical','horizontal','inCell') $name } + + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # showInHeader эмитится общим Emit-CommonElementProps (через Emit-Layout) + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) + Emit-ColumnPics -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + if ($null -ne $el.multiLine) { X "$inner$(if ($el.multiLine){'true'}else{'false'})" } + if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } + # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, + # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). + if ($null -ne $el.choiceButton) { X "$inner$(if ($el.choiceButton){'true'}else{'false'})" } + # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) + if ($null -ne $el.clearButton) { X "$inner$(if ($el.clearButton){'true'}else{'false'})" } + if ($null -ne $el.spinButton) { X "$inner$(if ($el.spinButton){'true'}else{'false'})" } + if ($null -ne $el.dropListButton) { X "$inner$(if ($el.dropListButton){'true'}else{'false'})" } + if ($null -ne $el.choiceListButton) { X "$inner$(if ($el.choiceListButton){'true'}else{'false'})" } + if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + if ($el.textEdit -eq $false) { X "$innerfalse" } + # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) + foreach ($p in @( + @('wrap','Wrap'), @('openButton','OpenButton'), @('listChoiceMode','ListChoiceMode'), + @('extendedEditMultipleValues','ExtendedEditMultipleValues'), @('chooseType','ChooseType'), + @('quickChoice','QuickChoice'), @('autoChoiceIncomplete','AutoChoiceIncomplete') + )) { + if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } + } + # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. + # availableTypes — формат типа реквизита (§type); Emit-Type сам разбирает мультитип "a | b". + if ($null -ne $el.typeDomainEnabled) { X "$inner$(if ($el.typeDomainEnabled){'true'}else{'false'})" } + if ($el.availableTypes) { Emit-Type -typeStr $el.availableTypes -indent $inner -tag 'AvailableTypes' } + # InputField-специфичные value-скаляры + foreach ($p in @( + @('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'), + @('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath') + )) { + if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")" } + } + # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). + foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) { + if ($null -ne $el.($p[0])) { + $mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' } + X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")" + } + } + if ($el.choiceButtonRepresentation) { X "$inner$($el.choiceButtonRepresentation)" } + Emit-PictureRef -val $el.choiceButtonPicture -picTag 'ChoiceButtonPicture' -indent $inner + Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) + + if ($el.inputHint) { + Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner + } + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + Emit-ChoiceList -el $el -indent $inner + + # Связи по типу / связи параметров выбора / параметры выбора + Emit-TypeLink -el $el -indent $inner + Emit-ChoiceParameterLinks -el $el -indent $inner + Emit-ChoiceParameters -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" + + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг + if ($null -ne $el.PSObject.Properties['checkBoxType']) { + if ($el.checkBoxType) { + $cbt = switch ("$($el.checkBoxType)".ToLower()) { 'auto' {'Auto'} 'checkbox' {'CheckBox'} 'switcher' {'Switcher'} 'tumbler' {'Tumbler'} default {"$($el.checkBoxType)"} } + X "$inner$cbt" + } + } else { X "$innerAuto" } + + Emit-TitleLocation -el $el -indent $inner -smartDefault "Right" + + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) + if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" + + X "$indent" +} + +# Maps Russian/English root of a typed reference path to canonical English root. +# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". +$script:refRootSynonyms = @{ + "Перечисление" = "Enum" + "Справочник" = "Catalog" + "Документ" = "Document" + "ПланСчетов" = "ChartOfAccounts" + "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" + "ПланВидовРасчета" = "ChartOfCalculationTypes" + "ПланВидовРасчёта" = "ChartOfCalculationTypes" + "ПланОбмена" = "ExchangePlan" + "БизнесПроцесс" = "BusinessProcess" + "Задача" = "Task" + "РегистрСведений" = "InformationRegister" + "РегистрНакопления" = "AccumulationRegister" + "РегистрБухгалтерии" = "AccountingRegister" + "РегистрРасчета" = "CalculationRegister" + "РегистрРасчёта" = "CalculationRegister" + "ЖурналДокументов" = "DocumentJournal" + "КритерийОтбора" = "FilterCriterion" +} +$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") + +# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты". +# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений. +function Normalize-MetaTypeRef { + param([string]$ref) + if ([string]::IsNullOrEmpty($ref)) { return $ref } + $dot = $ref.IndexOf('.') + if ($dot -lt 1) { return $ref } + $root = $ref.Substring(0, $dot) + if ($script:refRootSynonyms.ContainsKey($root)) { + return $script:refRootSynonyms[$root] + $ref.Substring($dot) + } + return $ref +} + +# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } +function Normalize-ChoiceValue { + param($value) + + # Booleans + if ($value -is [bool]) { + return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } + } + # Numbers (int / decimal / double) + if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { + return @{ XsiType = "xs:decimal"; Text = "$value" } + } + + $s = "$value" + if ([string]::IsNullOrEmpty($s)) { + return @{ XsiType = "xs:string"; Text = "" } + } + + # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime + if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { + return @{ XsiType = "xs:dateTime"; Text = $s } + } + + # Raw-ссылка по GUID (метаданные.значение, оба GUID): "GUID.GUID" → xr:DesignTimeRef + # (всегда ссылка, не строка; named-ссылки Enum.X.Y детектятся ниже). + if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F]{8}-[0-9a-fA-F-]+$') { + return @{ XsiType = "xr:DesignTimeRef"; Text = $s } + } + + # Try to detect typed reference path: ".[..]" + $parts = $s -split '\.' + if ($parts.Count -ge 2) { + $root = $parts[0] + $canonRoot = $null + if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } + elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } + + if ($canonRoot) { + $typeName = $parts[1] + $normalized = $null + + if ($canonRoot -eq "Enum") { + if ($parts.Count -eq 2) { + # "Enum.X" alone — not a value, treat as string + } elseif ($parts.Count -eq 3) { + # "Enum.X.Y" — insert .EnumValue. ("EmptyRef" — пустая ссылка, БЕЗ вставки) + if ($parts[2] -eq 'EmptyRef') { $normalized = "Enum.$typeName.EmptyRef" } + else { $normalized = "Enum.$typeName.EnumValue.$($parts[2])" } + } else { + # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) + $member = $parts[2] + if ($script:enumValueSynonyms -contains $member) { + $rest = $parts[3..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } else { + $rest = $parts[2..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } + } + } else { + # Other ref roots: just translate root, keep tail as-is + if ($parts.Count -ge 3) { + $tail = $parts[1..($parts.Count-1)] -join '.' + $normalized = "$canonRoot.$tail" + } + } + + if ($normalized) { + return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } + } + } + } + + return @{ XsiType = "xs:string"; Text = $s } +} + +# Emit Presentation block for a choiceList item. +# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. +# Empty/null → emits empty . +function Emit-ChoicePresentation { + param($pres, [string]$indent) + if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { + X "$indent" + return + } + + $pairs = @() + if ($pres -is [string]) { + $pairs += ,@("ru", $pres) + } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { + foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } + } elseif ($pres.PSObject -and $pres.PSObject.Properties) { + foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } + } else { + $pairs += ,@("ru", "$pres") + } + + X "$indent" + foreach ($pair in $pairs) { + X "$indent`t" + X "$indent`t`t$($pair[0])" + X "$indent`t`t$(Esc-Xml $pair[1])" + X "$indent`t" + } + X "$indent" +} + +# для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). +function Get-ChoiceValueTag { + param($norm) + if ([string]::IsNullOrEmpty($norm.Text)) { return "" } + return "$(Esc-Xml $norm.Text)" +} + +# Emit (список выбора) — у RadioButtonField и InputField. +# Элемент: { value, presentation?/title? } (+ рус. синонимы значение/представление). +function Emit-ChoiceList { + param($el, [string]$indent) + if (-not $el.choiceList -or $el.choiceList.Count -eq 0) { return } + X "$indent" + $itemIndent = "$indent`t" + foreach ($item in $el.choiceList) { + # value (+ рус. синоним "значение") + $valRaw = $null + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("value")) { $valRaw = $item["value"] } + elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } + } else { + if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } + elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } + } + + # presentation (presentation OR title синоним) + $presRaw = $null + $hasPres = $false + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } + elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } + elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } + } else { + if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } + elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } + elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } + } + + # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — + # переопределяет авто-детект (Normalize-ChoiceValue вывела бы xs:string). + $vtRaw = $null + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("valueType")) { $vtRaw = "$($item["valueType"])" } + } elseif ($item.PSObject.Properties["valueType"]) { $vtRaw = "$($item.valueType)" } + + if ($vtRaw -eq 'nil') { $norm = @{ XsiType = $null; Text = $null; Nil = $true } } + elseif ($vtRaw) { $norm = @{ XsiType = $vtRaw; Text = "$valRaw" } } + else { $norm = Normalize-ChoiceValue -value $valRaw } + + # авто-вывод presentation, если не задан + if (-not $hasPres) { + if ($norm.XsiType -eq "xr:DesignTimeRef") { + $tail = ($norm.Text -split '\.')[-1] + $presRaw = Title-FromName -name $tail + } else { + $presRaw = $norm.Text + } + } + + X "$itemIndent" + $valIndent = "$itemIndent`t" + X "$valIndent" + X "$valIndent0" + X "$valIndent" + Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" + X "$valIndent`t$(if ($norm.Nil) { '' } else { Get-ChoiceValueTag $norm })" + X "$valIndent" + X "$itemIndent" + } + X "$indent" +} + +# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null). +function Get-ElProp { + param($obj, [string[]]$names) + if ($null -eq $obj) { return $null } + foreach ($n in $names) { + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Contains($n)) { return $obj[$n] } + } elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { + return $obj.PSObject.Properties[$n].Value + } + } + return $null +} + +# Приведение строкового литерала shorthand к типу: true/false → bool, целое/дробное → число, +# иначе строка (ref-путь нормализует уже Normalize-ChoiceValue). +function ConvertTo-ScalarLiteral { + param([string]$s) + $t = "$s".Trim() + if ($t -match '^(?i:true)$') { return $true } + if ($t -match '^(?i:false)$') { return $false } + if ($t -match '^-?\d+$') { return [int]$t } + if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) } + return $t +} + +# Shorthand параметра выбора: "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. +function ConvertFrom-ChoiceParamShorthand { + param([string]$s) + $eq = $s.IndexOf('=') + if ($eq -lt 0) { return @{ name = $s.Trim() } } + $name = $s.Substring(0, $eq).Trim() + $rest = $s.Substring($eq + 1) + if ($rest -match ',') { + $vals = @() + foreach ($part in ($rest -split ',')) { $vals += ,(ConvertTo-ScalarLiteral $part) } + return @{ name = $name; value = $vals } + } + return @{ name = $name; value = (ConvertTo-ScalarLiteral $rest) } +} + +# Shorthand связи параметров выбора: "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. +function ConvertFrom-ChoiceParamLinkShorthand { + param([string]$s) + $eq = $s.IndexOf('=') + if ($eq -lt 0) { return @{ name = $s.Trim() } } + $o = @{ name = $s.Substring(0, $eq).Trim() } + $rest = $s.Substring($eq + 1).Trim() + if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { + $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] + } else { + $o['dataPath'] = $rest + } + return $o +} + +# Shorthand связи по типу: "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. +function ConvertFrom-TypeLinkShorthand { + param([string]$s) + if ($s -match '^(.*)#(\d+)$') { return @{ dataPath = $matches[1].Trim(); linkItem = [int]$matches[2] } } + return @{ dataPath = "$s".Trim() } +} + +# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . +# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. +function Emit-ChoiceParamValue { + # $isArray передаётся ЯВНО из вызывающего кода: PowerShell разворачивает одноэлементный массив + # при биндинге параметра ($value становится скаляром), поэтому определять массив тут — ненадёжно + # (1-элементный список `["X"]` эмитился бы скаляром вместо FixedArray). foreach по скаляру = 1 итерация. + param($value, [string]$indent, [bool]$isArray) + X "$indent" + if ($isArray) { + X "$indent" + foreach ($v in $value) { + $norm = Normalize-ChoiceValue -value $v + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t$(Get-ChoiceValueTag $norm)" + X "$indent`t" + } + X "$indent" + } else { + $norm = Normalize-ChoiceValue -value $value + X "$indent$(Get-ChoiceValueTag $norm)" + } +} + +# (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue; +# массив значений → FixedArray. Рус. синонимы имя/значение. +function Emit-ChoiceParameters { + param($el, [string]$indent) + $cp = $el.choiceParameters + if (-not $cp -or @($cp).Count -eq 0) { return } + X "$indent" + foreach ($item in @($cp)) { + if ($item -is [string]) { $item = ConvertFrom-ChoiceParamShorthand $item } + $name = Get-ElProp $item @('name','имя') + # Наличие ключа value (≠ значения) + ПРЯМОЙ доступ к значению (без Get-ElProp): его return + # разворачивает 1-элементный массив (PS unwrap), теряя массив-ность → FixedArray не эмитится. + # Индексер/member-доступ массив сохраняет; if-выражение/функция-return — нет. + $hasVal = $false; $val = $null + if ($item -is [System.Collections.IDictionary]) { + if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] } + elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] } + } else { + if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value } + elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value } + } + $valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string]) + X "$indent`t" + # Параметр выбора без значения → (платформа, 13 в корпусе); + # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. + if (-not $hasVal) { + X "$indent`t`t" + } else { + X "$indent`t`t" + Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" -isArray $valIsArray + X "$indent`t`t" + } + X "$indent`t" + } + X "$indent" +} + +# (связи параметров выбора) — [{name, dataPath, valueChange?}]. +# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. +function Emit-ChoiceParameterLinks { + param($el, [string]$indent) + $cpl = $el.choiceParameterLinks + if (-not $cpl -or @($cpl).Count -eq 0) { return } + X "$indent" + foreach ($lk in @($cpl)) { + if ($lk -is [string]) { $lk = ConvertFrom-ChoiceParamLinkShorthand $lk } + $name = Get-ElProp $lk @('name','имя') + $dp = Get-ElProp $lk @('dataPath','path','путь') + $vcRaw = Get-ElProp $lk @('valueChange','режимИзменения') + $vc = "Clear" + if ($vcRaw) { + $vc = switch -Regex ("$vcRaw".ToLower()) { + '^(clear|очистить|очистка)$' { "Clear"; break } + '^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break } + default { "$vcRaw" } + } + } + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$name")" + X "$indent`t`t$(Esc-Xml "$dp")" + X "$indent`t`t$vc" + X "$indent`t" + } + X "$indent" +} + +# (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. +function Emit-TypeLink { + param($el, [string]$indent) + $tl = $el.typeLink + if (-not $tl) { return } + if ($tl -is [string]) { $tl = ConvertFrom-TypeLinkShorthand $tl } + $dp = Get-ElProp $tl @('dataPath','path','путь') + $li = Get-ElProp $tl @('linkItem','элементСвязи') + if ($null -eq $li) { $li = 0 } + X "$indent" + X "$indent`t$(Esc-Xml "$dp")" + X "$indent`t$li" + X "$indent" +} + +function Emit-Radio { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-TitleLocation -el $el -indent $inner -smartDefault "None" + + # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. + $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } + $rbt = switch -Regex ($rbtRaw.ToLower()) { + '^(auto|авто)$' { "Auto"; break } + '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } + '^(tumbler|тумблер)$' { "Tumbler"; break } + default { $rbtRaw } + } + X "$inner$rbt" + + if ($null -ne $el.columnsCount) { + X "$inner$($el.columnsCount)" + } + + Emit-ChoiceList -el $el -indent $inner + + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" + + X "$indent" +} + +# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму +# (reuse Resolve-MLFormatted, как у extendedTooltip). Атрибут formatted эмитится ВСЕГДА +# (специфика декораций). Sibling-ключ `formatted` — back-compat override авто-детекта. +function Emit-DecorationTitle { + param($el, [string]$name, [string]$indent, [switch]$auto) + $hasKey = $null -ne $el.PSObject.Properties['title'] + $titleVal = if ($hasKey) { $el.title } elseif ($auto -and $name) { Title-FromName -name $name } else { $null } + if ($titleVal) { + $r = Resolve-MLFormatted $titleVal + $fmt = if ($null -ne $el.PSObject.Properties['formatted']) { [bool]$el.formatted } else { $r.formatted } + X "$indent<Title formatted=`"$(if ($fmt) { 'true' } else { 'false' })`">" + Emit-MLItems -val $r.text -indent "$indent`t" + X "$indent" + } + if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } + if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title + # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + Emit-Appearance -el $el -indent $inner -profile 'decoration' + + Emit-DecorationTitle -el $el -name $name -indent $inner -auto + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" + + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.editMode) { X "$inner$($el.editMode)" } + # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode + if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } + # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение + if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } + Emit-ColumnPics -el $el -indent $inner + # ВНИМАНИЕ: у LabelField платформенный тег именно (опечатка 1С), не . + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" + + X "$indent" +} + +# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). +# Платформа всегда эмитит этот блок на дин-список-таблице; компилятор зеркалит дефолты, +# DSL-ключ переопределяет; декомпилятор инвертирует (опускает значения = дефолту). +function Emit-DynListTableBlock { + param($el, [string]$indent) + # (useAlternationRowColor — общее свойство таблицы, эмитится в Emit-Table) + # Group A (гарант. блок, n=5079): дефолт + override + $ar = if ($el.autoRefresh -eq $true) { "true" } else { "false" } + X "$indent$ar" + $arp = if ($el.PSObject.Properties["autoRefreshPeriod"] -and $null -ne $el.autoRefreshPeriod) { $el.autoRefreshPeriod } else { 60 } + X "$indent$arp" + X "$indent" + X "$indent`tCustom" + X "$indent`t0001-01-01T00:00:00" + X "$indent`t0001-01-01T00:00:00" + X "$indent" + $cfi = if ($el.choiceFoldersAndItems) { $el.choiceFoldersAndItems } else { "Items" } + X "$indent$cfi" + $rcr = if ($el.restoreCurrentRow -eq $true) { "true" } else { "false" } + X "$indent$rcr" + X "$indent" + $sr = if ($el.showRoot -eq $false) { "false" } else { "true" } + X "$indent$sr" + $arc = if ($el.allowRootChoice -eq $true) { "true" } else { "false" } + X "$indent$arc" + $uodc = if ($el.updateOnDataChange) { $el.updateOnDataChange } else { "Auto" } + X "$indent$uodc" + if ($el.userSettingsGroup) { X "$indent$($el.userSettingsGroup)" } + $agcru = if ($el.allowGettingCurrentRowURL -eq $false) { "false" } else { "true" } + X "$indent$agcru" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + + $script:currentTableName = $name # дефолт source для кастомных дополнений в commandBar + X "$indent
" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + # ChangeRowSet/Order — эмитим явное значение (в т.ч. false: платформа пишет его на ValueTable) + if ($el.PSObject.Properties['changeRowSet'] -and $null -ne $el.changeRowSet) { + X "$inner$(if ($el.changeRowSet -eq $true){'true'}else{'false'})" + } + if ($el.PSObject.Properties['changeRowOrder'] -and $null -ne $el.changeRowOrder) { + X "$inner$(if ($el.changeRowOrder -eq $true){'true'}else{'false'})" + } + if ($el.autoInsertNewRow -eq $true) { X "$innertrue" } + # RowFilter — nil-плейсхолдер (всегда пустой); ключ присутствует → эмитим + if ($el.PSObject.Properties['rowFilter']) { X "$inner" } + # Высота в строках таблицы () — отдельное свойство от (высота элемента, + # эмитится generic-ом Emit-Layout ниже). Таблица может нести оба (237 в корпусе). + if ($el.heightInTableRows) { X "$inner$($el.heightInTableRows)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + + if ($el.commandBarLocation) { + X "$inner$($el.commandBarLocation)" + } + if ($el.searchStringLocation) { + X "$inner$($el.searchStringLocation)" + } + if ($el.choiceMode -eq $true) { X "$innertrue" } + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). + if ($null -ne $el.autofill) { X "$inner$(if ($el.autofill){'true'}else{'false'})" } + if ($el.multipleChoice -eq $true) { X "$innertrue" } + if ($el.searchOnInput) { X "$inner$($el.searchOnInput)" } + if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } + # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) + if ($null -ne $el.headerHeight) { X "$inner$($el.headerHeight)" } + if ($null -ne $el.footerHeight) { X "$inner$($el.footerHeight)" } + if ($el.useAlternationRowColor -eq $true) { X "$innertrue" } + if ($el.selectionMode) { X "$inner$($el.selectionMode)" } + if ($el.rowSelectionMode) { X "$inner$($el.rowSelectionMode)" } + if ($el.verticalLines -eq $false) { X "$innerfalse" } + if ($el.horizontalLines -eq $false) { X "$innerfalse" } + if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } + # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) + Emit-PictureRef -val $el.rowsPicture -picTag 'RowsPicture' -indent $inner + # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) + if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } + # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) + if ($el.refreshRequest) { X "$inner$($el.refreshRequest)" } + # Блок свойств дин-список-таблицы (помечена эвристикой 11b.4) + if ($el.PSObject.Properties["_dynList"] -and $el._dynList) { Emit-DynListTableBlock -el $el -indent $inner } + if ($el.viewStatusLocation) { X "$inner$($el.viewStatusLocation)" } + if ($el.searchControlLocation) { X "$inner$($el.searchControlLocation)" } + Emit-Layout -el $el -indent $inner + + # CommandSet таблицы эмитится через Emit-Layout (общий механизм поля) + + # Оформление (цвета/граница таблицы) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + # AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто. + if ($null -ne $el.commandBar) { + Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar + } elseif ($null -ne $el.tableAutofill) { + $acbId = New-Id + X "$inner" + $afVal = if ($el.tableAutofill) { "true" } else { "false" } + X "$inner`t$afVal" + X "$inner" + } else { + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + } + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + $adds = $el.additions + Emit-TableAddition -typeKey 'searchString' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchString') + Emit-TableAddition -typeKey 'viewStatus' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'viewStatus') + Emit-TableAddition -typeKey 'searchControl' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchControl') + + # Columns + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { + Emit-Element -el $col -indent "$inner`t" + } + X "$inner" + } + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" + + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.pagesRepresentation) { + X "$inner$($el.pagesRepresentation)" + } + # Использование текущей строки (после PagesRepresentation, порядок XSD) + if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) заголовка группы страниц — TitleFont/TitleTextColor/… (как у Page) + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" + + # Children (pages) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). + # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. + Emit-PictureRef -val $el.picture -picTag 'Picture' -indent $inner + + if ($el.group) { + # Доступные значения страницы/обычной группы: Vertical / HorizontalIfPossible / AlwaysHorizontal + # (InCell — только у columnGroup). Horizontal/AlwaysVertical оставлены forgiving (legacy). + $orientation = switch ("$($el.group)") { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + "horizontalIfPossible" { "HorizontalIfPossible" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + else { Warn-Unrecognized 'page group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name } + } + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + Emit-Layout -el $el -indent $inner + + # Оформление страницы (BackColor / TitleTextColor / TitleFont) — после ShowTitle, перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) + + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-DecorationTitle -el $el -name $name -indent $inner + # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) + if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } + Emit-CommonFlags -el $el -indent $inner + + # Источник картинки — ТОЛЬКО $el.src (у PictureDecoration ключ 'picture' = тип/имя элемента, не источник). + # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . + if ($el.src) { + $srcStr = "$($el.src)" + $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } + X "$inner" + if ($srcStr -match '^abs:(.*)$') { X "$inner`t$(Esc-Xml $matches[1])" } + else { X "$inner`t$(Esc-Xml $srcStr)" } + X "$inner`t$lt" + if ($el.transparentPixel) { X "$inner`t" } + X "$inner" + } + + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + # EnableDrag — фактическое значение (декорация-картинка перетаскиваема; декомпилятор ловит generic-ом) + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + + # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) + Emit-Appearance -el $el -indent $inner -profile 'decoration' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" + + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.hyperlink -eq $true) { X "$innertrue" } + + Emit-Layout -el $el -indent $inner + # EnableDrag — фактическое значение (поле картинки перетаскиваемо; декомпилятор ловит generic-ом) + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) + if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # ValuesPicture — picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. + Emit-PictureRef -val $el.valuesPicture -picTag 'ValuesPicture' -indent $inner + if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" + + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + "auto" { "Auto" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + Emit-Layout -el $el -indent $inner + + # Календарно-специфичные свойства (порядок схемы: после layout, до companions) + if ($el.selectionMode) { X "$inner$($el.selectionMode)" } + if ($null -ne $el.showCurrentDate) { $v = if ($el.showCurrentDate) { "true" } else { "false" }; X "$inner$v" } + if ($null -ne $el.widthInMonths) { X "$inner$($el.widthInMonths)" } + if ($null -ne $el.heightInMonths) { X "$inner$($el.heightInMonths)" } + if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner$v" } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" + + X "$indent" +} + +# Спец-поля «документ/датчик» (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): +# единый скелет поля (path/title/flags/titleLocation/editMode/layout/companions/events). +# Типоспец. enum/bool скаляры — через generic (Emit-Layout→Emit-GenericScalars); +# числовые скаляры датчиков (min/max/шаги) — без xsi:type (≠ типизированных InputField). +# enableDrag/enableStartDrag — общие (Emit-CommonElementProps), фактическое значение. +function Emit-SimpleField { + param($el, [string]$name, [int]$id, [string]$indent, [string]$xmlTag, [string]$typeKey) + + X "$indent<$xmlTag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.editMode) { X "$inner$($el.editMode)" } + + Emit-Layout -el $el -indent $inner + + # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через Emit-Layout. + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + + # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) + foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'), @('largeStep','LargeStep'), @('markingStep','MarkingStep'), @('step','Step'))) { + if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$($el.($p[0]))" } + } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey $typeKey + + X "$indent" +} + +# GanttChartField — скелет поля + вложенная (полноценная таблица, через Emit-Element). +# Порядок (по корпусу): path/title/flags/titleLocation/layout/appearance, companions, Table, events. +function Emit-GanttChart { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + Emit-Layout -el $el -indent $inner + Emit-Appearance -el $el -indent $inner -profile 'field' + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем Emit-Element) + if ($el.ganttTable) { Emit-Element -el $el.ganttTable -indent $inner } + Emit-Events -el $el -elementName $name -indent $inner -typeKey "ganttChart" + X "$indent" +} + +function Emit-CommandBar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.commandSource) { X "$inner$($el.commandSource)" } + + if ($el.autofill -eq $true) { X "$innertrue" } + + # CommandBar хранит HorizontalLocation фактически (включая Auto — декомпилятор ловит только при наличии); + # ≠ дополнениям, где Auto = умолчание-скип (Get-HLocation). + if ($el.horizontalLocation) { + $hlv = switch ("$($el.horizontalLocation)".ToLower()) { 'auto' {'Auto'} 'left' {'Left'} 'right' {'Right'} 'center' {'Center'} default {"$($el.horizontalLocation)"} } + X "$inner$hlv" + } + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-ButtonGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.commandSource) { X "$inner$($el.commandSource)" } + + if ($el.representation) { + X "$inner$($el.representation)" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children (кнопки в контексте командной панели) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + # Источник команд попапа (после Title/ToolTip, перед компаньоном) — как у ButtonGroup/CommandBar + if ($el.commandSource) { X "$inner$($el.commandSource)" } + + Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + Emit-Layout -el $el -indent $inner + + # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +# --- 8. Attribute emitter --- + +# FunctionalOption.X — у Attribute/Command/Column. +# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. +function Emit-FunctionalOptions { + param($fo, [string]$indent) + if (-not $fo -or @($fo).Count -eq 0) { return } + X "$indent" + foreach ($opt in @($fo)) { + $v = "$opt" + if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть + elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом + else { $v = "FunctionalOption.$v" } + X "$indent`t$v" + } + X "$indent" +} + +# Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. +function Emit-AttrColumn { + param($col, [string]$indent) + $colId = New-Id + X "$indent" + if ($col.title) { Emit-MLText -tag "Title" -text $col.title -indent "$indent`t" } + Emit-Type -typeStr "$($col.type)" -indent "$indent`t" + # Проверка заполнения колонки → (как у реквизита; bool true→ShowError / строка verbatim) + $cfcRaw = if ($null -ne $col.PSObject.Properties['fillCheck']) { $col.fillCheck } elseif ($null -ne $col.PSObject.Properties['fillChecking']) { $col.fillChecking } else { $null } + if ($null -ne $cfcRaw) { $cfcv = if ($cfcRaw -is [bool]) { if ($cfcRaw) { 'ShowError' } else { $null } } else { "$cfcRaw" }; if ($cfcv) { X "$indent`t$cfcv" } } + Emit-FunctionalOptions -fo $col.functionalOptions -indent "$indent`t" + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита + if ($null -ne $col.view) { Emit-XrFlag -tag 'View' -val $col.view -indent "$indent`t" } + if ($null -ne $col.edit) { Emit-XrFlag -tag 'Edit' -val $col.edit -indent "$indent`t" } + X "$indent" +} + +# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- +# Та же сущность, что параметры СКД (см. skd-compile), но в форме: обёртка +# + дети с префиксом dcssch:. DSL переиспользует грамматику параметров СКД (shorthand + +# объект). Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега»): +# useRestriction — эмитим ВСЕГДА, дефолт TRUE (в СКД дефолт false); +# title — авто из имени (Title-FromName), если ключ не задан. +# Канон. порядок детей (по корпусу acc/erp 8.3.24): name, title, valueType, value, +# useRestriction, expression, availableValue*, valueListAllowed, availableAsField, +# inputParameters, denyIncompleteValues, use. + +# Многоязычный текст в DCS-контексте — с xsi:type="v8:LocalStringType" (form-compile +# Emit-MLText его НЕ ставит, т.к. формовые голые; в dcssch:* — обязателен). +function Emit-DLMLText { + param([string]$tag, $text, [string]$indent) + X "$indent<$tag xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $text -indent "$indent`t" + X "$indent</$tag>" +} + +function Has-DLProp { + param($obj, [string]$name) + if ($null -eq $obj) { return $false } + if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } + if ($obj.PSObject -and $obj.PSObject.Properties[$name]) { return $true } + return $false +} + +function Split-DLValueListCsv { + param([string]$s) + $result = @() + if ($null -eq $s) { return ,$result } + $items = @(); $buf = New-Object System.Text.StringBuilder; $inQuote = $null + for ($i = 0; $i -lt $s.Length; $i++) { + $ch = $s[$i] + if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } + elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } + elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } + else { [void]$buf.Append($ch) } + } + if ($buf.Length -gt 0) { $items += $buf.ToString() } + foreach ($raw in $items) { + $t = $raw.Trim() + if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } + if ($t -ne "") { $result += $t } + } + return ,$result +} + +# Shorthand: "Имя [Заголовок]: Тип = Значение @valueList @hidden" +function Parse-DLParamShorthand { + param([string]$s) + $result = @{ name = ""; type = ""; value = $null; title = $null } + if ($s -match '@valueList') { $result.valueListAllowed = $true; $s = $s -replace '\s*@valueList', '' } + if ($s -match '@hidden') { $result.hidden = $true; $s = $s -replace '\s*@hidden', '' } + if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim(); $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } + # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). + if ($s -match '^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$') { + $result.name = $Matches[1].Trim() + $typeRaw = $Matches[2].Trim() + if ($typeRaw -match '[|+]') { + $result.type = (($typeRaw -split '\s*[|+]\s*') | ForEach-Object { Resolve-TypeStr ($_.Trim()) }) -join ' | ' + } else { + $result.type = Resolve-TypeStr $typeRaw + } + if ($Matches[4]) { + $rhs = $Matches[4].Trim() + $items = Split-DLValueListCsv $rhs + if ($items.Count -ge 2) { $result.value = $items; $result.valueListAllowed = $true } + elseif ($items.Count -eq 1) { $result.value = $items[0] } + else { $result.value = $rhs } + } + } else { $result.name = $s.Trim() } + return $result +} + +function Test-DLEmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "" -or $s -eq "_" -or $s.ToLowerInvariant() -eq "null") { return $true } + return $false +} + +# Эмиссия <dcssch:value> по типу/значению (xs:* или dcscor:DesignTimeValue для ссылок). +function Emit-DLValue { + param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) + if (Test-DLEmptyValue $val) { + # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil, даже при известном + # типе (в отличие от типизированного пустого в параметрах отчёта СКД). + if ($valueListAllowed) { return } + X "$indent<dcssch:value xsi:nil=`"true`"/>" + return + } + $valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" } + if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type + elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } + else { + if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } + else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } + } +} + +# <dcssch:valueType> — обёртка + тело через Emit-SingleType (ref-типы → cfg:, как в форме). +function Emit-DLValueType { + param($typeStr, [string]$indent) + if (-not $typeStr) { return } + X "$indent<dcssch:valueType>" + $parts = "$typeStr" -split '\s*[|+]\s*' + foreach ($part in $parts) { Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" } + X "$indent</dcssch:valueType>" +} + +function Emit-DLAvailableValue { + param($av, [string]$type, [string]$indent) + X "$indent<dcssch:availableValue>" + $avVal = if (Has-DLProp $av 'value') { $av.value } else { $null } + Emit-DLValue -type $type -val $avVal -indent "$indent`t" -valueListAllowed $false + $pres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { $null } + if ($pres) { Emit-DLMLText -tag "dcssch:presentation" -text $pres -indent "$indent`t" } + X "$indent</dcssch:availableValue>" +} + +# <dcssch:inputParameters> — ChoiceParameters / ChoiceParameterLinks / простое значение (порт из skd). +function Emit-DLInputParameters { + param($ip, [string]$indent) + if ($null -eq $ip) { return } + $items = @($ip) + if ($items.Count -eq 0) { return } + X "$indent<dcssch:inputParameters>" + foreach ($item in $items) { + X "$indent`t<dcscor:item>" + if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" + if (Has-DLProp $item 'choiceParameters') { + $cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() } + if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" } + else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" + foreach ($cpItem in $cpItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" + foreach ($v in @($cpItem.values)) { + if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" } + else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" } + } + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-DLProp $item 'choiceParameterLinks') { + $cplItems = if ($null -ne $item.choiceParameterLinks) { @($item.choiceParameterLinks) } else { @() } + if ($cplItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" } + else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" + foreach ($cplItem in $cplItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" + X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" + $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } + X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-DLProp $item 'typeLink') { + # Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра). + $tl = $item.typeLink + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">" + $tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" } + $tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" } + X "$indent`t`t</dcscor:value>" + } elseif (Has-DLProp $item 'value') { + $val = $item.value + if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" } + elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" } + elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } + else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" } + } + X "$indent`t</dcscor:item>" + } + X "$indent</dcssch:inputParameters>" +} + +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ── +# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект +# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}. +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} +function Emit-EmptyValue { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) + if ($valueListAllowed) { return } + $t = if ($null -eq $type) { "" } else { "$type" } + $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } + $pf = $tagPrefix + if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } + elseif ($t -eq "StandardPeriod") { + X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" + X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + X "$indent</${pf}value>" + } + elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } + elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" } + elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" } + elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" } + else { X "$indent<${pf}value xsi:nil=`"true`"/>" } +} +function Parse-DataParamShorthand { + param([string]$s) + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } + if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } + if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } + if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } + $s = $s.Trim() + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } + elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } + elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } + else { $result.value = $valStr } + } else { $result.parameter = $s } + return $result +} +function Emit-DataParameters { + param($items, [string]$indent, $blockViewMode = $null) + if (-not $items -or @($items).Count -eq 0) { return } + X "$indent<dcsset:dataParameters>" + foreach ($dp in @($items)) { + if ($dp -is [string]) { + $parsed = Parse-DataParamShorthand $dp + $dpObj = New-Object PSObject + $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter + if ($null -ne $parsed.value) { $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value } + if ($parsed.use -eq $false) { $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false } + if ($parsed.userSettingID) { $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID } + if ($parsed.viewMode) { $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode } + $dp = $dpObj + } + X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" + $dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string]) + if ($dpValIsArr) { + # Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое. + $avtype = "$($dp.valueType)" + foreach ($v in @($dp.value)) { + $vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" } + if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" } + elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" } + else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" } + } + } elseif ($dp.nilValue -eq $true) { + X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" + } elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) { + # Явный типизированный пустой (xs:string-плейсхолдер и т.п.) + Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false + } elseif (Test-EmptyValue $dp.value) { + # Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер). + # (В отличие от skd-settings, где значение всегда присутствует.) + } elseif ($null -ne $dp.value) { + $vtype = "$($dp.valueType)" + if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) { + $_hasDate = $false; $_hasSD = $false + if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] } + else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') } + $_variantStr = "$($dp.value.variant)" + $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') + if ($_isSBD) { + $_d = $null + if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } + elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" } + X "$indent`t`t</dcscor:value>" + } else { + $_sd = $null; $_ed = $null + if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } + else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" } + X "$indent`t`t</dcscor:value>" + } + } elseif ($vtype -match '^[a-zA-Z]+:') { + $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } + X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" + } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { + X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>" + } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^decimal') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^string') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } + } + if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" } + if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" } + if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } + X "$indent`t</dcscor:item>" + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + X "$indent</dcsset:dataParameters>" +} + +function Emit-DLParameter { + param($p, $parsed, [string]$indent) + X "$indent<Parameter>" + $ci = "$indent`t" + X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>" + # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. + $title = $null + if ($parsed.title) { $title = $parsed.title } + elseif ($p -isnot [string] -and (Has-DLProp $p 'title') -and $p.title) { $title = $p.title } + elseif ($p -isnot [string] -and (Has-DLProp $p 'presentation') -and $p.presentation) { $title = $p.presentation } + if ($null -eq $title -or ($title -is [string] -and $title -eq '')) { $title = Title-FromName -name $parsed.name } + Emit-DLMLText -tag "dcssch:title" -text $title -indent $ci + # valueType + if ($parsed.type) { Emit-DLValueType -typeStr $parsed.type -indent $ci } + # value (дефолт nil; при valueListAllowed пустое — опускаем) + $vla = [bool]$parsed.valueListAllowed + $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) + if ($valIsArray) { + foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false } + } elseif ($parsed.valueExplicit -and ($null -ne $parsed.value) -and ("$($parsed.value)" -eq '') -and (("$($parsed.type)" -eq '') -or ("$($parsed.type)" -match '^string'))) { + # Явный пустой СТРОКОВЫЙ параметр (value:"" от декомпилятора) → типизированный пустой + # <dcssch:value xsi:type="xs:string"/>, НЕ nil. Решается ФОРМОЙ value (""→typed-empty, + # null/отсутствие→nil), независимо от valueListAllowed; декомпилятор различает ""/null + # (Convert-TypedValue пустого xs:string → "", nil → value опущен/null). Корпус: 26 xs:string. + X "$ci<dcssch:value xsi:type=`"xs:string`"/>" + } elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) { + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil + X "$ci<dcssch:value xsi:nil=`"true`"/>" + } else { + Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla + } + # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. + $ur = $true + if ($p -isnot [string] -and (Has-DLProp $p 'useRestriction')) { $ur = [bool]$p.useRestriction } + X "$ci<dcssch:useRestriction>$(if ($ur) { 'true' } else { 'false' })</dcssch:useRestriction>" + # expression + $expr = $null + if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" } + if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" } + # availableValues + if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) { + foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci } + } + # valueListAllowed + if ($vla) { X "$ci<dcssch:valueListAllowed>true</dcssch:valueListAllowed>" } + # availableAsField=false (hidden или явный) + $aaf = $null + if ($parsed.hidden -eq $true) { $aaf = $false } + if ($p -isnot [string] -and (Has-DLProp $p 'availableAsField')) { $aaf = [bool]$p.availableAsField } + if ($aaf -eq $false) { X "$ci<dcssch:availableAsField>false</dcssch:availableAsField>" } + # inputParameters + if ($p -isnot [string] -and (Has-DLProp $p 'inputParameters') -and $p.inputParameters) { Emit-DLInputParameters -ip $p.inputParameters -indent $ci } + # denyIncompleteValues + if ($p -isnot [string] -and (Has-DLProp $p 'denyIncompleteValues') -and $p.denyIncompleteValues -eq $true) { X "$ci<dcssch:denyIncompleteValues>true</dcssch:denyIncompleteValues>" } + # use + $useVal = $null + if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" } + if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" } + X "$indent</Parameter>" +} + +function Emit-DLParameters { + param($params, [string]$indent) + if (-not $params) { return } + foreach ($p in @($params)) { + if ($p -is [string]) { + $parsed = Parse-DLParamShorthand $p + } else { + $resolvedType = "" + if ((Has-DLProp $p 'type') -and $p.type) { + if ($p.type -is [array] -or ($p.type -is [System.Collections.IList] -and $p.type -isnot [string])) { + $resolvedType = (@($p.type | ForEach-Object { Resolve-TypeStr "$_" })) -join ' | ' + } else { $resolvedType = Resolve-TypeStr "$($p.type)" } + } elseif ((Has-DLProp $p 'valueType') -and $p.valueType) { + $resolvedType = Resolve-TypeStr "$($p.valueType)" + } + $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null } + if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } + if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true } + } + Emit-DLParameter -p $p -parsed $parsed -indent $indent + } +} + +function Emit-Attributes { + param($attrs, [string]$indent, $conditionalAppearance = $null) + + $hasCA = $conditionalAppearance -and @($conditionalAppearance).Count -gt 0 + # Платформа ВСЕГДА эмитит <Attributes> (100% корпуса; 162 формы — пустой <Attributes/>). + if ((-not $attrs -or $attrs.Count -eq 0) -and -not $hasCA) { X "$indent<Attributes/>"; return } + if (-not $attrs -or $attrs.Count -eq 0) { + # Нет реквизитов, но есть условное оформление (последний child <Attributes>) + X "$indent<Attributes>" + Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' + X "$indent</Attributes>" + return + } + + X "$indent<Attributes>" + $seenAttrs = @{} + foreach ($attr in $attrs) { + $attrId = New-Id + $attrName = "$($attr.name)" + Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute' + + X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">" + $inner = "$indent`t`t" + + # Title атрибута (зеркало Emit-Title): нет ключа → авто-вывод из имени (кроме main); + # title "" → подавить; непустой → эмитить как есть. + $hasTitleKey = $null -ne $attr.PSObject.Properties['title'] + if ($hasTitleKey) { + if ($attr.title) { Emit-MLText -tag "Title" -text $attr.title -indent $inner } + } elseif ($attr.main -ne $true) { + Emit-MLText -tag "Title" -text (Title-FromName -name $attrName) -indent $inner + } + + # Type + if ($attr.type) { + Emit-Type -typeStr "$($attr.type)" -indent $inner + } else { + X "$inner<Type/>" + } + # valueType: уточнение типа значений ValueList → <Settings xsi:type="v8:TypeDescription"> + # (та же грамматика типа, что и Type, включая составной "A | B"). Forgiving-синонимы. + # Три состояния: нет ключа → нет Settings; "" → пустой <Settings…/>; тип → с типом. + $vtSpec = $null; $hasVt = $false + foreach ($k in @('valueType','typeDescription','описаниеТипов','типЗначений')) { + if ($attr.PSObject.Properties[$k]) { $vtSpec = $attr.$k; $hasVt = $true; break } + } + if ($hasVt) { + Emit-Type -typeStr "$vtSpec" -indent $inner -tag "Settings" -tagAttrs ' xsi:type="v8:TypeDescription"' + } + # Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика). + # Идёт сразу после <Type> (как valueType/DynamicList Settings — взаимоисключающи). + if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) { + Emit-PlannerSettings -pl $attr.planner -ind $inner + } + # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart">. + # Тип Settings выводится из типа реквизита (d5p1:GanttChart → d4p1:GanttChart). + if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) { + $ctype = if ("$($attr.type)" -match 'GanttChart') { 'd4p1:GanttChart' } else { 'd4p1:Chart' } + Emit-ChartSettings -chart $attr.chart -ind $inner -ctype $ctype + } + + if ($attr.main -eq $true) { + X "$inner<MainAttribute>true</MainAttribute>" + } + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner } + if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner } + $mainSaved = $false + if ($attr.main -eq $true -and $attr.type) { + $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' + } + # Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто. + $emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved } + if ($emitSaved) { + X "$inner<SavedData>true</SavedData>" + } + # Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>; + # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). + # Нет ключа или false → не эмитим. + if ($null -ne $attr.PSObject.Properties['save'] -and $null -ne $attr.save) { + $saveFields = New-Object System.Collections.ArrayList + if ($attr.save -is [bool]) { + if ($attr.save) { [void]$saveFields.Add($attrName) } + } else { + foreach ($e in @($attr.save)) { + $fld = "$e" + if ([string]::IsNullOrEmpty($fld)) { continue } + if ($fld -ne $attrName -and $fld -notmatch '\.' -and $fld -notmatch '^\d+/\d+') { $fld = "$attrName.$fld" } + if (-not $saveFields.Contains($fld)) { [void]$saveFields.Add($fld) } + } + } + if ($saveFields.Count -gt 0) { + X "$inner<Save>" + foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" } + X "$inner</Save>" + } + } + # Проверка заполнения реквизита → <FillCheck> (реальный тег; <FillChecking> в схеме нет). + # bool true → ShowError (единственное значение в корпусе); строка → verbatim. Синоним fillChecking. + $fcRaw = if ($null -ne $attr.PSObject.Properties['fillCheck']) { $attr.fillCheck } elseif ($null -ne $attr.PSObject.Properties['fillChecking']) { $attr.fillChecking } else { $null } + if ($fcRaw) { + $fcv = if ($fcRaw -is [bool]) { 'ShowError' } else { "$fcRaw" } + X "$inner<FillCheck>$fcv</FillCheck>" + } + + # UseAlways: поля, всегда читаемые (дин-список/таблица). Две формы DSL сливаются: + # attr.useAlways[] (короткие имена) + columns с useAlways:true → <Field>ИмяРеквизита.Поле</Field>. + $uaFields = New-Object System.Collections.ArrayList + if ($attr.useAlways) { + foreach ($e in @($attr.useAlways)) { + $fld = "$e" + # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" + # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. + # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). + if ($fld.StartsWith('~')) { + $bare = $fld.Substring(1) + if ($bare -notmatch "^$([regex]::Escape($attrName))\.") { $bare = "$attrName.$bare" } + $fld = "~$bare" + } elseif ($fld -notmatch "^$([regex]::Escape($attrName))\." -and $fld -notmatch '^\d+/\d+') { + # UUID-ссылка (1/0:GUID) — НЕ префиксуем (платформа хранит её без "имя.") + $fld = "$attrName.$fld" + } + if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } + } + } + if ($attr.columns) { + foreach ($col in $attr.columns) { + if ($col.useAlways -eq $true) { + $fld = "$attrName.$($col.name)" + if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } + } + } + } + if ($uaFields.Count -gt 0) { + X "$inner<UseAlways>" + foreach ($f in $uaFields) { X "$inner`t<Field>$f</Field>" } + X "$inner</UseAlways>" + } + + Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner + + # Columns: прямые <Column> (ValueTable/Tree) + <AdditionalColumns table="X"> (доп. колонки + # табличных частей объекта). Порядок схемы: прямые сначала, затем AdditionalColumns-группы. + # Для дин-списка (есть settings) прямые колонки НЕ эмитим (служат лишь для UseAlways). + $hasDirectCols = $attr.columns -and $attr.columns.Count -gt 0 -and -not $attr.settings + $hasAddCols = $attr.additionalColumns -and @($attr.additionalColumns).Count -gt 0 + if ($hasDirectCols -or $hasAddCols) { + X "$inner<Columns>" + if ($hasDirectCols) { + $seenCols = @{} # колонки уникальны в пределах своего реквизита + foreach ($col in $attr.columns) { + Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t" + } + } + if ($hasAddCols) { + foreach ($ac in @($attr.additionalColumns)) { + $acCols = @($ac.columns) + if ($acCols.Count -eq 0) { + # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) + X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>" + continue + } + X "$inner`t<AdditionalColumns table=`"$($ac.table)`">" + $seenAcCols = @{} # уникальность в пределах группы AdditionalColumns + foreach ($col in $acCols) { + Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t`t" + } + X "$inner`t</AdditionalColumns>" + } + } + X "$inner</Columns>" + } + + # Settings (динамический список) + if ($attr.settings) { + $st = $attr.settings + X "$inner<Settings xsi:type=`"DynamicList`">" + $si = "$inner`t" + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" } + $hasQuery = $st.query -and "$($st.query)".Trim() + # Явный ключ manualQuery (в т.ч. false) ПОБЕЖДАЕТ эвристику hasQuery (платформа изредка + # хранит QueryText при ManualQuery=false — декомпилятор фиксирует это отклонение). + $hasMQKey = ($st.PSObject.Properties['manualQuery']) -and ($null -ne $st.manualQuery) + $mq = if ($hasMQKey) { if ($st.manualQuery) { "true" } else { "false" } } elseif ($hasQuery) { "true" } else { "false" } + X "$si<ManualQuery>$mq</ManualQuery>" + # DynamicDataRead: дефолт true; false только при явном отключении + $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" } + X "$si<DynamicDataRead>$ddr</DynamicDataRead>" + if ($hasQuery) { + $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir + X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>" + } + # Явные поля набора (редко): override title/dataPath + if ($st.fields) { + foreach ($fld in $st.fields) { + # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet + # (поле-вложенный набор = реквизит табличной части; маркер nested). + # folder = папка-группировка полей (DataSetFieldFolder, без <field>); nested = вложенный набор. + $isFolder = [bool](Get-Prop $fld 'folder') + $ftype = if ($fld.nested) { "DataSetFieldNestedDataSet" } elseif ($isFolder) { "DataSetFieldFolder" } else { "DataSetFieldField" } + X "$si<Field xsi:type=`"dcssch:$ftype`">" + # dataPath: явный (включая пустой "" → self-closing <dcssch:dataPath/>) побеждает; иначе fallback на field. + if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" } + elseif ($isFolder) { $dp = "" } + else { $dp = "$($fld.field)" } + if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" } + if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" } + if ($fld.title) { + X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $fld.title -indent "$si`t`t" + X "$si`t</dcssch:title>" + } + # Ограничения использования поля — после title, перед presentationExpression (порядок исходника) + Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t" + Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t" + # presentationExpression поля — перед valueType (порядок исходника) + if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" } + # valueType поля набора (тип значения; вычисляемые/кастомные поля) + if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" } + # appearance поля (формат/оформление) — после valueType (порядок исходника) + if ($fld.appearance) { + X "$si`t<dcssch:appearance>" + foreach ($prop in $fld.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$si`t`t" } + X "$si`t</dcssch:appearance>" + } + # inputParameters поля (связь по параметрам выбора) — в конце + if ($fld.inputParameters) { Emit-DLInputParameters -ip $fld.inputParameters -indent "$si`t" } + X "$si</Field>" + } + } + # Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*. + Emit-CalcFields -calcFields $st.calculatedFields -indent $si + # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. + Emit-DLParameters -params $st.parameters -indent $si + # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) + # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. + if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" } + if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } } + if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" } + # GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false). + if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" } + # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). + if ($null -ne $st.autoSaveUserSettings) { X "$si<AutoSaveUserSettings>$(if ($st.autoSaveUserSettings){'true'}else{'false'})</AutoSaveUserSettings>" } + # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. + # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. + $lsi = "$si`t" + $lsOpenLen = $script:xml.Length + X "$si<ListSettings>" + $lsAfterOpenLen = $script:xml.Length # для self-closing, если внутри ничего не эмитнётся + if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) { + # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. + # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. + foreach ($prop in $st.listSettings.PSObject.Properties) { + $tag = $prop.Name; $pv = $prop.Value + # Значение дескриптора: строка-код "vu" ИЛИ объект { meta:"vu", presentation:<текст/ML> } + # (контейнер несёт собственный userSettingPresentation — кастомную подпись настройки). + if (($pv -is [PSCustomObject]) -or ($pv -is [System.Collections.IDictionary])) { + $meta = "$(Get-Prop $pv 'meta')"; $bpres = Get-Prop $pv 'presentation' + } else { $meta = "$pv"; $bpres = $null } + $bvm = if ($meta -match 'v') { 'Normal' } else { $null } + switch ($tag) { + 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } + 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } + 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } + 'itemsViewMode' { X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" } + 'itemsUserSettingID' { X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" } + 'itemsUserSettingPresentation' { Emit-USPresentation -val $pv -tag "dcsset:itemsUserSettingPresentation" -indent $lsi } + 'dataParameters' { Emit-DataParameters -items $st.dataParameters -indent $lsi } + 'structure' { Emit-ListGrouping (Get-ListGroupingValue $st) $lsi } + } + } + } else { + # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. + Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi } + Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID + Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID + # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode + Emit-ListGrouping (Get-ListGroupingValue $st) $lsi + X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" + X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" + } + if ($script:xml.Length -eq $lsAfterOpenLen) { + # Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing. + $script:xml.Length = $lsOpenLen + X "$si<ListSettings/>" + } else { + X "$si</ListSettings>" + } + X "$inner</Settings>" + } + + X "$indent`t</Attribute>" + } + # Условное оформление формы — последний child <Attributes> (та же DCS-грамматика, что settings CA) + Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' + X "$indent</Attributes>" +} + +# --- 9. Parameter emitter --- + +function Emit-Parameters { + param($params, [string]$indent) + + if (-not $params -or $params.Count -eq 0) { return } + + X "$indent<Parameters>" + $seenParams = @{} + foreach ($param in $params) { + Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter' + X "$indent`t<Parameter name=`"$($param.name)`">" + $inner = "$indent`t`t" + + Emit-Type -typeStr "$($param.type)" -indent $inner + + if ($param.key -eq $true) { + X "$inner<KeyParameter>true</KeyParameter>" + } + + X "$indent`t</Parameter>" + } + X "$indent</Parameters>" +} + +# --- 10. Command emitter --- + +function Emit-Commands { + param($cmds, [string]$indent) + + if (-not $cmds -or $cmds.Count -eq 0) { return } + + X "$indent<Commands>" + $seenCmds = @{} + foreach ($cmd in $cmds) { + $cmdId = New-Id + Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command' + X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">" + $inner = "$indent`t`t" + + # Заголовок команды (зеркало Emit-Title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс + # (в оригинале <Title> нет — не додумывать); ключ отсутствует → авто-вывод из имени (помощь модели). + if ($null -ne $cmd.PSObject.Properties['title']) { + if ($cmd.title) { Emit-MLText -tag "Title" -text $cmd.title -indent $inner } + } else { + $cmdTitle = Title-FromName -name "$($cmd.name)" + if ($cmdTitle) { Emit-MLText -tag "Title" -text $cmdTitle -indent $inner } + } + + if ($cmd.tooltip) { + Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner + } + + # Доступность команды по ролям (после ToolTip, до Action) + if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner } + + if ($cmd.action) { + X "$inner<Action>$($cmd.action)</Action>" + } + + if ($cmd.modifiesSavedData -eq $true) { X "$inner<ModifiesSavedData>true</ModifiesSavedData>" } + + Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner + + if ($cmd.currentRowUse) { + X "$inner<CurrentRowUse>$($cmd.currentRowUse)</CurrentRowUse>" + } + + # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). + # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) + $cmdTable = $cmd.table + if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId } + if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица } + if ($cmdTable) { + X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>" + } + + if ($cmd.shortcut) { + X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" + } + + Emit-CommandPicture -pic $cmd.picture -elemLt $cmd.loadTransparent -indent $inner + + if ($cmd.representation) { + X "$inner<Representation>$($cmd.representation)</Representation>" + } + + X "$indent`t</Command>" + } + X "$indent</Commands>" +} + +# Резолв ключа-группы древовидной формы → CommandGroup (зависит от панели). Дружелюбные +# алиасы стандартных form-групп; любой иной ключ (CommandGroup.X / GUID / "") — verbatim. +function Resolve-CommandGroupKey { + param([string]$key, [string]$panelTag) + $k = ($key -replace '\s','').ToLower() + if ($panelTag -eq 'NavigationPanel') { + switch ($k) { + 'important' { return 'FormNavigationPanelImportant' } + 'важное' { return 'FormNavigationPanelImportant' } + 'goto' { return 'FormNavigationPanelGoTo' } + 'перейти' { return 'FormNavigationPanelGoTo' } + 'seealso' { return 'FormNavigationPanelSeeAlso' } + 'смтакже' { return 'FormNavigationPanelSeeAlso' } + } + } else { + switch ($k) { + 'important' { return 'FormCommandBarImportant' } + 'важное' { return 'FormCommandBarImportant' } + 'createbasedon' { return 'FormCommandBarCreateBasedOn' } + 'создатьнаосновании' { return 'FormCommandBarCreateBasedOn' } + } + } + return $key # verbatim +} + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. +# Значение панели: МАССИВ (плоская форма; элемент может нести group) ИЛИ ОБЪЕКТ +# (древовидная форма: {группа: [команды]}, group берётся из ключа, элементы его не дублируют). +# Элемент: строка (голый command, Type=Auto) или объект. Порядок тегов: +# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). +function Emit-CommandInterface { + param($ci, [string]$indent) + if (-not $ci) { return } + $inner = "$indent`t" + $panels = @( + @{ Tag='CommandBar'; Syns=@('commandBar','команднаяПанель','КоманднаяПанель') }, + @{ Tag='NavigationPanel'; Syns=@('navigationPanel','панельНавигации','ПанельНавигации') } + ) + $present = @() + foreach ($p in $panels) { + $items = $null + foreach ($syn in $p.Syns) { if ($null -ne $ci.PSObject.Properties[$syn]) { $items = $ci.($syn); break } } + if ($null -ne $items) { $present += ,@{ Tag=$p.Tag; Items=$items } } + } + if ($present.Count -eq 0) { return } + X "$indent<CommandInterface>" + foreach ($p in $present) { + X "$inner<$($p.Tag)>" + # Нормализация: плоский список пар (элемент, group-из-дерева). Объект → дерево. + $flat = New-Object System.Collections.ArrayList + if ($p.Items -is [System.Management.Automation.PSCustomObject]) { + foreach ($prop in $p.Items.PSObject.Properties) { + $grpFromTree = Resolve-CommandGroupKey -key $prop.Name -panelTag $p.Tag + foreach ($it in @($prop.Value)) { [void]$flat.Add(@{ item=$it; treeGroup=$grpFromTree }) } + } + } else { + foreach ($it in @($p.Items)) { [void]$flat.Add(@{ item=$it; treeGroup=$null }) } + } + foreach ($fi in $flat) { + $item = $fi.item; $treeGroup = $fi.treeGroup + if ($item -is [string]) { + $cmd = $item; $type = 'Auto'; $attr = $null; $grp = $null; $idx = $null; $dv = $null; $vis = $null + } else { + $cmd = Get-ElProp $item @('command','команда') + $type = Get-ElProp $item @('type','тип'); if (-not $type) { $type = 'Auto' } + $attr = Get-ElProp $item @('attribute','реквизит') + $grp = Get-ElProp $item @('group','группа','группаКоманд') + $idx = Get-ElProp $item @('index','индекс') + $dv = Get-ElProp $item @('defaultVisible','видимость','видимостьПоУмолчанию') + $vis = Get-ElProp $item @('visible','видимостьПоРолям','настройкаВидимости') + } + # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк + if ($treeGroup) { $grp = $treeGroup } + X "$inner`t<Item>" + X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>" + X "$inner`t`t<Type>$type</Type>" + if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" } + if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" } + if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" } + if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" } + if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" } + X "$inner`t</Item>" + } + X "$inner</$($p.Tag)>" + } + X "$indent</CommandInterface>" +} + +# --- 11. Properties emitter --- + +function Emit-Properties { + param($props, [string]$indent) + + if (-not $props) { return } + + # camelCase -> PascalCase mapping for known properties + $propMap = @{ + "autoTitle" = "AutoTitle" + "windowOpeningMode" = "WindowOpeningMode" + "commandBarLocation" = "CommandBarLocation" + "saveDataInSettings" = "SaveDataInSettings" + "autoSaveDataInSettings" = "AutoSaveDataInSettings" + "autoTime" = "AutoTime" + "usePostingMode" = "UsePostingMode" + "repostOnWrite" = "RepostOnWrite" + "autoURL" = "AutoURL" + "autoFillCheck" = "AutoFillCheck" + "customizable" = "Customizable" + "enterKeyBehavior" = "EnterKeyBehavior" + "verticalScroll" = "VerticalScroll" + "scalingMode" = "ScalingMode" + "useForFoldersAndItems" = "UseForFoldersAndItems" + "reportResult" = "ReportResult" + "detailsData" = "DetailsData" + "reportFormType" = "ReportFormType" + "autoShowState" = "AutoShowState" + "width" = "Width" + "height" = "Height" + "group" = "Group" + } + + foreach ($p in $props.PSObject.Properties) { + $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { + # Auto PascalCase: first letter uppercase + $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) + } + # Convert boolean to lowercase string (PS renders as True/False) + $val = $p.Value + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if ($val -is [string] -and $val -eq '') { continue } + if ($val -is [bool]) { + $val = if ($val) { "true" } else { "false" } + } + X "$indent<$xmlName>$val</$xmlName>" + } +} + +# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + +# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый +# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами +# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство. +function Normalize-PanelSynonyms { + param($el) + if ($null -eq $el) { return } + $panelSyns = @{ + 'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель') + 'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню') + } + foreach ($canon in $panelSyns.Keys) { + foreach ($syn in $panelSyns[$canon]) { + $p = $el.PSObject.Properties[$syn] + if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) { + if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) { + $v = $p.Value + $el.PSObject.Properties.Remove($syn) | Out-Null + $el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force + } + break + } + } + } +} + +function Normalize-ElementSynonyms { + param($el) + if ($null -eq $el) { return } + Normalize-PanelSynonyms $el + # Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому + # значению (имя элемента); объект/массив уже отнесён к панель-свойству выше. + $typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } + foreach ($pair in $typeSyn.GetEnumerator()) { + $src = $el.PSObject.Properties[$pair.Key] + if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) { + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) { + $val = $el.extTooltip + $el.PSObject.Properties.Remove("extTooltip") | Out-Null + $el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force + } + # Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри + foreach ($pk in @('commandBar','contextMenu')) { + $pp = $el.PSObject.Properties[$pk] + if ($null -ne $pp) { + $kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null } + if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } } + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { Normalize-ElementSynonyms $child } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } + } +} + +function HasCmdBarRecursive { + param($el) + if ($null -eq $el) { return $false } + if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } + } + return $false +} + +function ApplyDynamicListTableHeuristic { + param($el, [string]$listName, [bool]$hasMainTable) + if ($null -eq $el) { return } + if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { + # Маркер дин-список-таблицы → Emit-Table эмитит блок свойств (Group A defaults) + $el | Add-Member -NotePropertyName "_dynList" -NotePropertyValue $true -Force + if ($null -eq $el.PSObject.Properties["tableAutofill"]) { + $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force + } + if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { + $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force + } + # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. + # Декомпилятор опускает ключ при rpdp == smart-default (ждёт реинъекции); реальное отсутствие + # фиксирует ""-маркером (НЕ перезатирается). Гейт hasMainTable снят: дин-список без mainTable + # (напр. query-based) тоже несёт RowPictureDataPath. + if ($null -eq $el.PSObject.Properties["rowPictureDataPath"]) { + $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } + } +} + +function Test-IsObjectLikeType { + param([string]$type) + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } + $objectSuffixes = @( + "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", + "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", + "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", + "ChartOfCalculationTypesObject", "ExchangePlanObject" + ) + $recordSetPrefixes = @( + "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", + "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", + "InformationRegisterRecordManager" + ) + foreach ($suffix in $objectSuffixes) { + if ($type -like "$suffix.*") { return $true } + } + foreach ($prefix in $recordSetPrefixes) { + if ($type -like "$prefix.*") { return $true } + } + return $false +} + +# 11b.1: Normalize synonyms recursively +if ($def.elements) { + foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } +} + +# 11b.2: Extract autoCmdBar element from def.elements +$script:mainAcbDef = $null +if ($def.elements) { + $autoBars = @() + $rest = @() + foreach ($el in $def.elements) { + if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { + $autoBars += $el + } else { + $rest += $el + } + } + if ($autoBars.Count -gt 1) { + Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." + exit 1 + } + if ($autoBars.Count -eq 1) { + $script:mainAcbDef = $autoBars[0] + # Replace def.elements with the filtered list + $def.PSObject.Properties.Remove("elements") | Out-Null + $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force + } +} + +# 11b.3: Infer main attribute (only if no attribute has main:true) +if ($def.attributes) { + $hasExplicitMain = $false + foreach ($attr in $def.attributes) { + if ($attr.main -eq $true) { $hasExplicitMain = $true; break } + } + if (-not $hasExplicitMain) { + $candidates = @() + foreach ($attr in $def.attributes) { + # Skip if user explicitly opted out via main:false + if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } + if (Test-IsObjectLikeType "$($attr.type)") { + $candidates += $attr + } + } + if ($candidates.Count -eq 1) { + $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force + Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" + } elseif ($candidates.Count -gt 1) { + $names = ($candidates | ForEach-Object { $_.name }) -join ", " + Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" + } + } +} + +# 11b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) +if ($def.attributes -and $def.elements) { + foreach ($attr in $def.attributes) { + if ("$($attr.type)" -ne "DynamicList") { continue } + $mt = $null + if ($attr.PSObject.Properties["settings"] -and $null -ne $attr.settings) { + if ($attr.settings -is [hashtable]) { + if ($attr.settings.ContainsKey("mainTable")) { $mt = $attr.settings["mainTable"] } + } elseif ($attr.settings.PSObject.Properties["mainTable"]) { + $mt = $attr.settings.mainTable + } + } + $hasMt = -not [string]::IsNullOrEmpty("$mt") + foreach ($el in $def.elements) { + ApplyDynamicListTableHeuristic $el $attr.name $hasMt + } + } +} + +# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 +function Compute-MainAcbAutofill { + if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { + return [bool]$script:mainAcbDef.autofill + } + return $true + } + if ($def.elements) { + foreach ($el in $def.elements) { + if (HasCmdBarRecursive $el) { return $false } + } + } + return $true +} + +# --- 12. Main compilation --- + +# Title +if ($def.title) { + Emit-MLText -tag "Title" -text $def.title -indent "`t" +} + +# Header +X '<?xml version="1.0" encoding="UTF-8"?>' +X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" + +# Oops — Title was emitted before header. Need to fix the order. +# Actually, let me restructure: build the body into a separate buffer, then assemble + +# Reset and rebuild properly +$script:xml = New-Object System.Text.StringBuilder 8192 +$script:nextId = 1 +$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме) + +X '<?xml version="1.0" encoding="UTF-8"?>' +X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" + +# 12a. Title (from def.title or properties.title — must be multilingual XML) +$formTitle = $def.title +if (-not $formTitle -and $def.properties -and $def.properties.title) { + $formTitle = $def.properties.title +} +if ($formTitle) { + Emit-MLText -tag "Title" -text $formTitle -indent "`t" +} + +# 12b. Properties (skip 'title' — handled above as multilingual) +# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; +# otherwise platform appends synonym → "Title: Synonym" double-titles). +$propsClone = New-Object PSObject +$hasAutoTitle = $false +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } + } +} +if ($formTitle -and -not $hasAutoTitle) { + $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false +} +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -ne "title") { + $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value + } + } +} +Emit-Properties -props $propsClone -indent "`t" + +# 12c. CommandSet (excluded commands) +if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { + X "`t<CommandSet>" + foreach ($cmd in $def.excludedCommands) { + X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>" + } + X "`t</CommandSet>" +} + +# 12c2. MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок +# (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). +# $null-проверка (не truthy): одноэлементный массив с пустой строкой @("") разворачивается +# в boolean-контексте в "" → falsy; 12 форм корпуса несут один пустой item (Value=""). +if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).Count -gt 0) { + X "`t<MobileDeviceCommandBarContent>" + foreach ($nm in @($def.mobileCommandBarContent)) { + X "`t`t<xr:Item>" + X "`t`t`t<xr:Presentation/>" + X "`t`t`t<xr:CheckState>0</xr:CheckState>" + # пустое значение → самозакрывающийся тег (зеркало платформы) + if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" } + else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" } + X "`t`t</xr:Item>" + } + X "`t</MobileDeviceCommandBarContent>" +} + +# 12d. AutoCommandBar (always present, id=-1) +$acbAutofill = Compute-MainAcbAutofill +$acbName = "ФормаКоманднаяПанель" +$acbHAlign = $null +if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { + $acbName = "$($script:mainAcbDef.autoCmdBar)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { + $acbName = "$($script:mainAcbDef.name)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { + $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" + } +} +$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) +$acbDIAttr = if ($script:mainAcbDef) { DI-Attr $script:mainAcbDef } else { "" } # DisplayImportance форменной панели +$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) +if ($acbHasInner) { + X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr>" + if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" } + if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" } + if ($hasAcbChildren) { + X "`t`t<ChildItems>" + foreach ($child in $script:mainAcbDef.children) { + Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true + } + X "`t`t</ChildItems>" + } + X "`t</AutoCommandBar>" +} else { + X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr/>" +} + +# 12e. Events +if ($def.events) { + foreach ($p in $def.events.PSObject.Properties) { + if ($script:knownFormEvents -notcontains $p.Name) { + Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" + } + } + X "`t<Events>" + foreach ($p in $def.events.PSObject.Properties) { + X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>" + } + X "`t</Events>" +} + +# 12f. ChildItems (elements) +if ($def.elements -and $def.elements.Count -gt 0) { + X "`t<ChildItems>" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent "`t`t" + } + X "`t</ChildItems>" +} + +# 12g. Attributes +Emit-Attributes -attrs $def.attributes -indent "`t" -conditionalAppearance $def.conditionalAppearance + +# 12h. Parameters +Emit-Parameters -params $def.parameters -indent "`t" + +# 12i. Commands +Emit-Commands -cmds $def.commands -indent "`t" + +# 12i2. CommandInterface (командный интерфейс формы — последний дочерний Form) +Emit-CommandInterface -ci $def.commandInterface -indent "`t" + +# 12j. Close +X '</Form>' + +# --- 13. Write output --- + +$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +$outDir = [System.IO.Path]::GetDirectoryName($outPath) +if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) + +# --- 13b. Auto-register form in parent object XML --- + +# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml +$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) +$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) +$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) +$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) +$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) + +$formName = [System.IO.Path]::GetFileName($formNameDir) +$objectName = [System.IO.Path]::GetFileName($objectDir) +$formsLeaf = [System.IO.Path]::GetFileName($formsDir) + +if ($formsLeaf -eq 'Forms') { + $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" + if (Test-Path $objectXmlPath) { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objectXmlPath) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) + if ($childObjects) { + $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) + if (-not $existing) { + $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formElem.InnerText = $formName + + $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) + if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } + + if ($insertBefore) { + $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null + $ws = $objDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertBefore($ws, $insertBefore) | Out-Null + } else { + $lastChild = $childObjects.LastChild + if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null + $childObjects.InsertBefore($formElem, $lastChild) | Out-Null + } else { + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null + $childObjects.AppendChild($formElem) | Out-Null + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null + } + } + + $regEnc = New-Object System.Text.UTF8Encoding($true) + $regSettings = New-Object System.Xml.XmlWriterSettings + $regSettings.Encoding = $regEnc + $regSettings.Indent = $false + $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) + $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) + $objDoc.Save($regWriter) + $regWriter.Close() + $regStream.Close() + + Write-Host " Registered: <Form>$formName</Form> in $objectName.xml" + } + } + } +} + +# --- 14. Summary --- + +$elCount = $script:nextId - 1 +Write-Host "[OK] Compiled: $OutputPath" +Write-Host " Elements+IDs: $elCount" +if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } +if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } +if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index e33cfc4e..d0da6b0e 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,2958 +1,2958 @@ -# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$FormPath, - - [string]$OutputPath -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- 0. Resolve and validate input --- -if (-not (Test-Path $FormPath)) { - Write-Error "Form not found: $FormPath" - exit 1 -} -$FormPath = (Resolve-Path $FormPath).Path - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load($FormPath) -$root = $xmlDoc.DocumentElement - -# Второй документ с сохранением whitespace — только для восстановления ТОЧНОГО числа пробелов -# в whitespace-only <v8:content> (декорации-распорки без width: число пробелов = ширина). Основной -# парс (PreserveWhitespace=false) не трогаем; элементная структура обоих документов идентична → -# навигация по индекс-пути элементов (Resolve-WS). Загрузка лениво-безопасная. -$script:xmlDocWS = $null -try { $script:xmlDocWS = New-Object System.Xml.XmlDocument; $script:xmlDocWS.PreserveWhitespace = $true; $script:xmlDocWS.Load($FormPath) } catch { $script:xmlDocWS = $null } - -# Ring 2: not a managed Form -if ($root.LocalName -ne 'Form') { - [Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.") - exit 2 -} - -# --- 1. Namespaces --- -$NS_LF = "http://v8.1c.ru/8.3/xcf/logform" -$NS_V8 = "http://v8.1c.ru/8.1/data/core" -$NS_XR = "http://v8.1c.ru/8.3/xcf/readable" -$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" - -$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" -$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" -$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" -$NS_V8UI = "http://v8.1c.ru/8.1/data/ui" -$NS_APP = "http://v8.1c.ru/8.2/managed-application/core" - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("lf", $NS_LF) -$ns.AddNamespace("v8", $NS_V8) -$ns.AddNamespace("xr", $NS_XR) -$ns.AddNamespace("xsi", $NS_XSI) -$ns.AddNamespace("dcsset", $NS_DCSSET) -$ns.AddNamespace("dcssch", $NS_DCSSCH) -$ns.AddNamespace("dcscor", $NS_DCSCOR) -$ns.AddNamespace("v8ui", $NS_V8UI) -$ns.AddNamespace("app", $NS_APP) - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, -# компилятор регенерит тот же скелет → чистый раундтрип. -$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - -# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) --- -$script:outputDir = $null -$script:outputBasename = $null -if ($OutputPath) { - $od = Split-Path -Parent $OutputPath - if (-not $od) { $od = (Get-Location).Path } - $script:outputDir = $od - $script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath) -} -$script:queryFilesAccumulator = @() -$script:queryFileNamesUsed = @{} - -# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file". -function Maybe-ExternalizeQuery { - param([string]$queryText, [string]$listName) - if (-not $queryText) { return $queryText } - if (-not $script:outputDir) { return $queryText } - $lineCount = ([regex]::Matches($queryText, "`n")).Count + 1 - if ($lineCount -lt 3) { return $queryText } - $safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' } - $prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' } - $fileName = "$prefix$safe.sql" - $suffix = 1 - while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" } - $script:queryFileNamesUsed[$fileName] = $true - $script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText } - return "@$fileName" -} -function Save-QueryFiles { - if ($script:queryFilesAccumulator.Count -eq 0) { return } - if (-not $script:outputDir) { return } - $enc = New-Object System.Text.UTF8Encoding($false) - foreach ($qf in $script:queryFilesAccumulator) { - [System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc) - } - [Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)") -} - -# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/ -# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false: -# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness -# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже). -function Test-ListSettingsHasContent { - param($lsNode) - if (-not $lsNode) { return $false } - foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) { - $cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns) - if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true } - } - return $false -} - -# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance → -# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма == -# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы -# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора. -function Get-ListSettingsShape { - param($lsNode, [bool]$hasGrouping = $false) - if (-not $lsNode) { return $null } - $shape = [ordered]@{} - foreach ($child in $lsNode.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - $tag = $child.LocalName - if ($tag -in @('filter','order','conditionalAppearance')) { - $hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns) - $hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns) - $code = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" - # Контейнер может нести собственный <dcsset:userSettingPresentation> (кастомная подпись - # настройки) — сохраняем форму по xsi:type (Get-PresByType: ru-only LocalString ≠ xs:string). - $uspNode = $child.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($uspNode) { - $usp = Get-PresByType $uspNode - $shape[$tag] = [ordered]@{ meta = $code; presentation = $usp } - } else { $shape[$tag] = $code } - } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } - elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } - elseif ($tag -eq 'itemsUserSettingPresentation') { $shape['itemsUserSettingPresentation'] = Get-PresByType $child } # items-уровневая подпись (форма по xsi:type) - elseif ($tag -eq 'dataParameters') { $shape['dataParameters'] = $true } # значения параметров запроса (контент в settings.dataParameters) - elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } } - else { return $null } # неизвестный top-level → канон-fallback - } - # Полный каноничный скелет → опускаем (компилятор регенерит) - if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and ` - $shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null } - return $shape -} - -# Группировка строк динамического списка: цепочка <dcsset:item StructureItemGroup> (каждый уровень = -# один groupItems-field; вложенность через дочерний <dcsset:item>). Возвращает ПЛОСКИЙ массив уровней -# (string для дефолтного поля; объект {field,groupType?,periodAdditionType?,periodAdditionBegin?,periodAdditionEnd?} -# для нестандартного) ИЛИ $null, если структура не «чистая линейная цепочка одно-польных уровней» -# (ветвление/мультиполе/доп.содержимое) → честный fallback на канон (LOST), а не тихая порча. -function Build-GroupLevel { - param($fn) - $field = Get-Child $fn 'field' - $gt = Get-Child $fn 'groupType' - $pat = Get-Child $fn 'periodAdditionType' - $pabN = $fn.SelectSingleNode("dcsset:periodAdditionBegin", $ns) - $paeN = $fn.SelectSingleNode("dcsset:periodAdditionEnd", $ns) - $pab = $null; $pae = $null - if ($pabN) { $pt = $pabN.GetAttribute("type", $NS_XSI); $pv = $pabN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv } } - if ($paeN) { $pt = $paeN.GetAttribute("type", $NS_XSI); $pv = $paeN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv } } - $isDefault = ((-not $gt) -or $gt -eq 'Items') -and ((-not $pat) -or $pat -eq 'None') -and (-not $pab) -and (-not $pae) - if ($isDefault) { return $field } - $o = [ordered]@{ field = $field } - if ($gt -and $gt -ne 'Items') { $o['groupType'] = $gt } - if ($pat -and $pat -ne 'None') { $o['periodAdditionType'] = $pat } - if ($pab) { $o['periodAdditionBegin'] = $pab } - if ($pae) { $o['periodAdditionEnd'] = $pae } - return $o -} - -function Build-ListGrouping { - param($itemNode) - $levels = New-Object System.Collections.ArrayList - $cur = $itemNode - while ($cur) { - if (($cur.GetAttribute("type", $NS_XSI)) -notmatch 'StructureItemGroup$') { return $null } - $gi = $null; $nested = @() - foreach ($ch in $cur.ChildNodes) { - if ($ch.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - switch ($ch.LocalName) { - 'groupItems' { if ($gi) { return $null }; $gi = $ch } - 'item' { $nested += $ch } - default { return $null } # use/name/filter/order/… — здесь не поддержано - } - } - if (-not $gi) { return $null } - $fieldItems = @($gi.SelectNodes("dcsset:item", $ns)) - if ($fieldItems.Count -ne 1) { return $null } - $fn = $fieldItems[0] - if (($fn.GetAttribute("type", $NS_XSI)) -notmatch 'GroupItemField$') { return $null } - [void]$levels.Add((Build-GroupLevel $fn)) - if ($nested.Count -eq 0) { break } - if ($nested.Count -gt 1) { return $null } # ветвление — не линейно - $cur = $nested[0] - } - if ($levels.Count -eq 0) { return $null } - return ,@($levels) -} - -# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) --- -function Fail-Ring3 { - param([string]$kind, [string]$loc) - [Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)") - [Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.") - exit 3 -} -# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае. -foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" } -# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… — -# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие -# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча. -foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) { - $st = $s.GetAttribute("type", $NS_XSI) - if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') { - Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings" - } - # Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с - # префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые - # дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны. - elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) { - Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings" - } -} - -# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) --- -function Convert-StringToJsonLiteral { - param([string]$s) - if ($null -eq $s) { return 'null' } - $sb = New-Object System.Text.StringBuilder - [void]$sb.Append('"') - foreach ($ch in $s.ToCharArray()) { - $code = [int]$ch - if ($code -eq 0x22) { [void]$sb.Append('\"') } - elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } - elseif ($code -eq 0x08) { [void]$sb.Append('\b') } - elseif ($code -eq 0x09) { [void]$sb.Append('\t') } - elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } - elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } - elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } - elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } - else { [void]$sb.Append($ch) } - } - [void]$sb.Append('"') - return $sb.ToString() -} -function Try-InlineJson { - param($obj) - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - if ($obj -is [System.Collections.IDictionary]) { - if ($obj.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $obj.Keys) { - $v = Try-InlineJson $obj[$k] - if ($null -eq $v) { return $null } - $parts += "$(Convert-StringToJsonLiteral "$k"): $v" - } - return '{ ' + ($parts -join ', ') + ' }' - } - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @() - foreach ($it in $items) { - $v = Try-InlineJson $it - if ($null -eq $v) { return $null } - $parts += $v - } - return '[' + ($parts -join ', ') + ']' - } - return $null -} -function ConvertTo-CompactJson { - param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120) - $indent = $indentUnit * $depth - $childIndent = $indentUnit * ($depth + 1) - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) - if ($isContainer) { - $inlineAttempt = Try-InlineJson $obj - if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } - } - if ($obj -is [System.Collections.IDictionary]) { - $keys = @($obj.Keys) - if ($keys.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $keys) { - $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit - $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" - } - return "{`n" + ($parts -join ",`n") + "`n$indent}" - } - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) - return "[`n" + ($parts -join ",`n") + "`n$indent]" - } - return (Convert-StringToJsonLiteral "$obj") -} - -# --- 2. Helpers --- - -# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. -# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы -# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). -$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar') - -# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} -function Get-LangText { - param($node) - if ($null -eq $node) { return $null } - $items = @($node.SelectNodes("v8:item", $ns)) - if ($items.Count -eq 0) { return $null } - $map = [ordered]@{} - foreach ($it in $items) { - $lang = $it.SelectSingleNode("v8:lang", $ns) - $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } - } - if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] } - return $map -} - -# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает -# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой -# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue). -# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель -# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст. -# Точное число пробелов whitespace-only <v8:content> из PreserveWhitespace-документа (основной -# парс стрипает его в ""). Навигация по индекс-пути элементов (структура обоих документов идентична). -function Resolve-WS { - param($contentNode) - if (-not $contentNode -or -not $script:xmlDocWS) { return $null } - $idxs = New-Object System.Collections.ArrayList - $cur = $contentNode - while ($cur.ParentNode -and $cur.ParentNode.NodeType -eq [System.Xml.XmlNodeType]::Element) { - $i = 0; $sib = $cur.PreviousSibling - while ($sib) { if ($sib.NodeType -eq [System.Xml.XmlNodeType]::Element) { $i++ }; $sib = $sib.PreviousSibling } - [void]$idxs.Insert(0, $i) - $cur = $cur.ParentNode - } - $wcur = $script:xmlDocWS.DocumentElement - foreach ($ix in $idxs) { - $els = @(); foreach ($ch in $wcur.ChildNodes) { if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $els += $ch } } - if ($ix -ge $els.Count) { return $null } - $wcur = $els[$ix] - } - return $wcur.InnerText -} - -# Точное восстановление пробела (число): whitespace-only content → реальная строка пробелов из WS-дока. -function Restore-WSContent { - param($contentNode) - $ws = Resolve-WS $contentNode - if ($ws -and $ws.Trim() -eq '') { return $ws } # только если действительно whitespace - return ' ' -} - -function Get-LangTextWS { - param($node) - $t = Get-LangText $node - if ($null -eq $t) { return $null } - if ($t -is [string]) { - $cn = $node.SelectSingleNode("v8:item/v8:content", $ns) - if ($t -eq '' -and $cn) { return (Restore-WSContent $cn) } - return $t - } - foreach ($it in @($node.SelectNodes("v8:item", $ns))) { - $lang = $it.SelectSingleNode("v8:lang", $ns) - $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') { - $t[$lang.InnerText] = (Restore-WSContent $content) - } - } - return $t -} - -# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. -# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам. -function Title-FromName { - param([string]$name) - if (-not $name) { return '' } - $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') - $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') - $parts = $s -split ' ' - if ($parts.Count -eq 0) { return $s } - $out = New-Object System.Collections.ArrayList - [void]$out.Add($parts[0]) - for ($i = 1; $i -lt $parts.Count; $i++) { - $p = $parts[$i] - if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) } - else { [void]$out.Add($p.ToLower()) } - } - return ($out -join ' ') -} - -# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). -$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' -function Test-HasRealMarkup { - param($text) - if ($null -eq $text) { return $false } - $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") } - foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } - return $false -} -# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted -# совпал с атрибутом; иначе явный {text, formatted}. -function Get-MLFormattedValue { - param($titleNode) - if (-not $titleNode) { return $null } - # Get-LangTextWS восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и - # мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "". - $text = Get-LangTextWS $titleNode - if ($null -eq $text) { return $null } - $fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true') - if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text } - $o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o -} - -# Прочитать дочерний скаляр (по local-name, без namespace) -function Get-Child { - param($node, [string]$name) - $c = $node.SelectSingleNode("*[local-name()='$name']") - if ($c) { return $c.InnerText } else { return $null } -} -function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") } -function To-Bool { param([string]$v) return ($v -eq 'true') } - -# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). -# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type. -function Convert-TypedValue { - param([string]$raw, [string]$xsiType) - switch -regex ($xsiType) { - 'decimal$' { - if ($raw -match '^-?\d+$') { return [int]$raw } - return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture) - } - 'boolean$' { return ($raw -eq 'true') } - default { return $raw } - } -} - -# ===================================================================== -# Захват настроек компоновщика динамического списка (ListSettings): -# filter / order / conditionalAppearance. Логика портирована из навыка -# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance -# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd -# заменён на запись в stderr + пропуск элемента (form-decompile — draft, -# скрипт не падает на непокрытых конструкциях). -# ===================================================================== - -# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text. -function Get-Text { - param($node, [string]$xpath) - if (-not $node) { return $null } - if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText } - $n = $node.SelectSingleNode($xpath, $ns) - if ($n) { return $n.InnerText } else { return $null } -} - -# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. -# Алиас на уже существующий Get-LangText (тот же контракт). -function Get-MLText { param($node) return (Get-LangText $node) } - -# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. -# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText. -function Get-PresText { - param($node) - if (-not $node) { return $null } - $ml = Get-MLText $node - if ($null -ne $ml) { return $ml } - if ($node.InnerText) { return $node.InnerText } - return $null -} - -# Presentation, сохраняющий ФОРМУ по xsi:type (не схлопывает ru-only LocalStringType в строку): -# <… xsi:type="v8:LocalStringType"> → объект {lang:text} (даже один ru), иначе xs:string → плоская строка. -# Нужно, чтобы компилятор воспроизвёл точный xsi:type (LocalStringType vs xs:string). Пустой LocalStringType → $null. -function Get-PresByType { - param($node) - if (-not $node) { return $null } - $xt = $node.GetAttribute("type", $NS_XSI) - if ($xt -match 'LocalStringType$') { - $d = [ordered]@{} - foreach ($it in $node.SelectNodes("v8:item", $ns)) { - $lang = Get-Text $it "v8:lang"; $content = Get-Text $it "v8:content" - if ($lang) { $d[$lang] = $content } - } - if ($d.Count -gt 0) { return $d } - return $null - } - if ($node.InnerText) { return $node.InnerText } - return $null -} - -# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") -function Get-LocalXsiType { - param($node) - if (-not $node) { return $null } - $t = $node.GetAttribute("type", $NS_XSI) - if ($t -match ':(.+)$') { return $matches[1] } - return $t -} - -# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile). -function Get-FontValue { - param($valNode) - $f = [ordered]@{ '@type' = 'Font' } - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $a = $valNode.Attributes[$attrName] - if ($null -ne $a) { $f[$attrName] = $a.Value } - } - return $f -} - -# Линия (граница) оформления → объект {@type:Line, width, gap, style}. -function Get-LineValue { - param($valNode) - $obj = [ordered]@{ '@type' = 'Line' } - $w = $valNode.GetAttribute("width") - $g = $valNode.GetAttribute("gap") - if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } - if ($g -ne '') { $obj['gap'] = ($g -eq 'true') } - $styleNode = $valNode.SelectSingleNode("v8ui:style", $ns) - if ($styleNode) { $obj['style'] = $styleNode.InnerText } - return $obj -} - -# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text. -function Read-AppearanceValueNode { - param($valNode) - if (-not $valNode) { return $null } - $vt = Get-LocalXsiType $valNode - if ($vt -eq 'LocalStringType') { - # НЕ схлопываем одноязычный в строку: значение параметра оформления различает - # xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы - # одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType. - $map = [ordered]@{} - foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) { - $lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } - } - return $map - } - if ($vt -eq 'Font') { return (Get-FontValue $valNode) } - if ($vt -eq 'Line') { return (Get-LineValue $valNode) } - # dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь} - if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } } - return $valNode.InnerText -} - -# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). -$script:filterOpMap = @{ - 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>='; - 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn'; - 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy'; - 'Contains'='contains'; 'NotContains'='notContains'; - 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith'; - 'Like'='like'; 'NotLike'='notLike'; - 'Filled'='filled'; 'NotFilled'='notFilled' -} - -# Render filter value node → shorthand-acceptable scalar string -function Get-FilterValue { - param($valNode) - if (-not $valNode) { return '_' } - $nil = $valNode.GetAttribute("nil", $NS_XSI) - if ($nil -eq 'true') { return '_' } - $vType = Get-LocalXsiType $valNode - if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText } - if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) } - $txt = $valNode.InnerText - if (-not $txt) { return '_' } - return $txt -} - -# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). -function Get-FilterValueWithType { - param($valNode) - if (-not $valNode) { return @{ value = '_'; type = $null } } - $rawType = $valNode.GetAttribute("type", $NS_XSI) - $nil = $valNode.GetAttribute("nil", $NS_XSI) - if ($nil -eq 'true') { return @{ value = '_'; type = $null } } - $vType = Get-LocalXsiType $valNode - if ($vType -eq 'LocalStringType') { - return @{ value = (Get-MLText $valNode); type = $rawType } - } - # Стандартная дата начала/окончания. Формы (от самой компактной): - # SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт - # даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType; - # SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал. - if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') { - $variantN = $valNode.SelectSingleNode("v8:variant", $ns) - $dateN = $valNode.SelectSingleNode("v8:date", $ns) - $variantStr = if ($variantN) { $variantN.InnerText } else { '' } - if ($dateN) { - if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') { - return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат - } - return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType } - } - return @{ value = $variantStr; type = $rawType } # именованный вариант - } - $txt = $valNode.InnerText - if (-not $txt) { return @{ value = '_'; type = $rawType } } - if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } } - if ($vType -eq 'decimal') { - if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } } - return @{ value = [double]$txt; type = $rawType } - } - return @{ value = $txt; type = $rawType } -} - -# Convert filter item node → shorthand string или object form (рекурсивно для групп). -function Build-FilterItem { - param($itemNode, [string]$loc) - $xtype = Get-LocalXsiType $itemNode - if ($xtype -eq 'FilterItemGroup') { - $gt = Get-Text $itemNode "dcsset:groupType" - $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } } - $items = @() - foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $c -loc "$loc/item") - if ($null -ne $bi) { $items += $bi } - } - $gObj = [ordered]@{ group = $groupName; items = $items } - if ((Get-Text $itemNode "dcsset:use") -eq 'false') { $gObj['use'] = $false } # группа отключена (@off) - $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) - if ($gPresNode) { - # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) - $gPres = Get-PresByType $gPresNode - if ($null -ne $gPres -and $gPres -ne '') { $gObj['presentation'] = $gPres } - } - $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) - if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText } - $gUSID = Get-Text $itemNode "dcsset:userSettingID" - if ($gUSID) { $gObj['userSettingID'] = 'auto' } - $gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($gUSPN) { - $gUSP = Get-PresText $gUSPN - if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP } - } - return $gObj - } - if ($xtype -ne 'FilterItemComparison') { - [Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)") - return $null - } - $leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns) - $field = if ($leftNode) { $leftNode.InnerText } else { $null } - $ct = Get-Text $itemNode "dcsset:comparisonType" - $op = $script:filterOpMap[$ct] - if (-not $op) { $op = $ct } - - $rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns)) - $value = $null - $valueIsArrayFlag = $false - $valueTypeAttr = $null - if ($rightNodes.Count -eq 1) { - $rn = $rightNodes[0] - if ((Get-LocalXsiType $rn) -eq 'ValueListType') { - $value = @() - $valueIsArrayFlag = $true - } else { - $vt = Get-FilterValueWithType $rn - $value = $vt.value - $autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and ` - ("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') - if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) { - $valueTypeAttr = $vt.type - } elseif ($vt.type -eq 'xs:string' -and ($value -is [string]) -and ($value -match '^(-?\d+(\.\d+)?|\d{4}-\d{2}-\d{2}T)')) { - # Значение-строка "1"/"2020-..." с xsi:type="xs:string": компилятор авто-детектит число/дату - # (теряет xs:string). Когда авто-вывод дал бы ДРУГОЙ тип — фиксируем явный valueType. - $valueTypeAttr = 'xs:string' - } - } - } elseif ($rightNodes.Count -gt 1) { - $arr = @() - $rawTypes = @() - foreach ($rn in $rightNodes) { - $arr += (Get-FilterValue $rn) - $rawTypes += $rn.GetAttribute("type", $NS_XSI) - } - $value = $arr - $valueIsArrayFlag = $true - $uniqTypes = @($rawTypes | Sort-Object -Unique) - if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) { - $autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and ` - ($arr.Count -gt 0) -and ` - (@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0) - if (-not $autoDetectsDTV) { - $valueTypeAttr = $uniqTypes[0] - } - } - } - - $use = Get-Text $itemNode "dcsset:use" - $userId = Get-Text $itemNode "dcsset:userSettingID" - $vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) - $viewMode = if ($vmNode) { $vmNode.InnerText } else { $null } - $userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) - $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) - $fiPres = $null - if ($fiPresNode) { - # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) - $fiPres = Get-PresByType $fiPresNode - } - - $flags = @() - if ($use -eq 'false') { $flags += '@off' } - if ($userId) { $flags += '@user' } - if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' } - elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' } - elseif ($viewMode -eq 'Normal') { $flags += '@normal' } - - $noValueOps = @('filled','notFilled') - - # Пустой xs:string right (<dcsset:right xsi:type="xs:string"/>) ≠ отсутствие <right>: - # Get-FilterValueWithType маппит наличие пустого/nil right в value='_' (отсутствие → $null). - # Shorthand `_` неоднозначен (схлопывает оба, компилятор → нет right). Пробельные значения рвут - # shorthand-парсинг (split по пробелам). → форсим объектную форму (value='_' → self-closing; - # пробелы — как есть в <right>). - $valNeedsObj = $false - if ($rightNodes.Count -eq 1 -and -not $valueIsArrayFlag -and $op -notin $noValueOps) { - if ("$value" -eq '_') { - # Truly-empty <right xsi:type="xs:string"/> ИЛИ whitespace-only <right> </right> - # (PreserveWhitespace=false стрипнул пробелы в '' → Get-FilterValueWithType вернул '_'). - # Восстанавливаем реальные пробелы из WS-дока (как у whitespace-заголовков). - $ws = Resolve-WS $rightNodes[0] - if ($ws -and $ws.Length -gt 0 -and $ws.Trim() -eq '') { $value = $ws } - $valNeedsObj = $true - } - elseif ($null -ne $value -and "$value" -match '\s') { $valNeedsObj = $true } - } - - if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres -or $valNeedsObj) { - $obj = [ordered]@{ field = $field; op = $op } - if ($op -notin $noValueOps -and $null -ne $value) { - if ($valueIsArrayFlag) { - $arrAsList = New-Object System.Collections.ArrayList - foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) } - $obj['value'] = $arrAsList - } else { - $obj['value'] = $value - } - } - if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr } - if ($use -eq 'false') { $obj['use'] = $false } - if ($userId) { $obj['userSettingID'] = 'auto' } - if ($fiPres) { $obj['presentation'] = $fiPres } - if ($viewMode) { $obj['viewMode'] = $viewMode } - if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode } - return $obj - } - - $s = $field - if ($op -in $noValueOps) { - $s += " $op" - } else { - $vDisplay = '_' - if ($null -ne $value) { - if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } } - elseif ("$value" -ne '') { $vDisplay = "$value" } - } - $s += " $op $vDisplay" - } - if ($flags) { $s += ' ' + ($flags -join ' ') } - return $s -} - -# Рекурсивный хелпер одного элемента selection (для conditionalAppearance). -function Build-SelectionItem { - param($item, [string]$loc) - $xt = Get-LocalXsiType $item - if (-not $xt) { - $fName = Get-Text $item "dcsset:field" - if ($fName) { return $fName } - $fieldEl = $item.SelectSingleNode("dcsset:field", $ns) - if ($fieldEl) { return 'Auto' } - } - switch ($xt) { - 'SelectedItemAuto' { - $useV = Get-Text $item "dcsset:use" - if ($useV -eq 'false') { - return [ordered]@{ auto = $true; use = $false } - } - return 'Auto' - } - 'SelectedItemField' { - $fName = Get-Text $item "dcsset:field" - $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) - $title = Get-MLText $titleNode - $vmN = $item.SelectSingleNode("dcsset:viewMode", $ns) - $useV = Get-Text $item "dcsset:use" - $useFalse = ($useV -eq 'false') - if ($title -or $vmN -or $useFalse) { - $obj = [ordered]@{ field = $fName } - if ($useFalse) { $obj['use'] = $false } - if ($title) { $obj['title'] = $title } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - return $obj - } - return $fName - } - 'SelectedItemFolder' { - $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) - $folderTitle = Get-MLText $titleNode - $inner = @() - foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-SelectionItem -item $sub -loc "$loc/folder") - if ($null -ne $bi) { $inner += $bi } - } - $entry = [ordered]@{ folder = $folderTitle; items = $inner } - $folderField = Get-Text $item "dcsset:field" - if ($folderField) { $entry['field'] = $folderField } - $plN = $item.SelectSingleNode("dcsset:placement", $ns) - if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') { - $entry['placement'] = $plN.InnerText - } - return $entry - } - default { - [Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)") - return $null - } - } -} - -# Build selection items array (для conditionalAppearance). -function Build-Selection { - param($selNode, [string]$loc) - if (-not $selNode) { return @() } - $out = @() - foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-SelectionItem -item $it -loc $loc) - if ($null -ne $bi) { $out += $bi } - } - return ,$out -} - -# Build order items array. -function Build-Order { - param($ordNode, [string]$loc) - if (-not $ordNode) { return @() } - $out = @() - foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) { - $xt = Get-LocalXsiType $it - switch ($xt) { - 'OrderItemAuto' { $out += 'Auto' } - 'OrderItemField' { - $fn = Get-Text $it "dcsset:field" - $ot = Get-Text $it "dcsset:orderType" - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - $useV = Get-Text $it "dcsset:use" - $useFalse = ($useV -eq 'false') - if ($vmN -or $useFalse) { - $obj = [ordered]@{ field = $fn } - if ($useFalse) { $obj['use'] = $false } - if ($ot -eq 'Desc') { $obj['direction'] = 'desc' } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - $out += $obj - } else { - if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn } - } - } - default { - [Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)") - } - } - } - return ,$out -} - -# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items). -function Get-SettingsAppearance { - param($appNode) - if (-not $appNode) { return $null } - $dict = [ordered]@{} - foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) { - $pName = Get-Text $it "dcscor:parameter" - $val = $it.SelectSingleNode("dcscor:value", $ns) - if (-not $pName -or -not $val) { continue } - $rawVal = Read-AppearanceValueNode $val - $useV = Get-Text $it "dcscor:use" - $nestedItems = [ordered]@{} - foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) { - $subName = Get-Text $sub "dcscor:parameter" - $subVal = $sub.SelectSingleNode("dcscor:value", $ns) - if (-not $subName) { continue } - $subRaw = Read-AppearanceValueNode $subVal - $subUse = Get-Text $sub "dcscor:use" - $subEntry = [ordered]@{ value = $subRaw } - if ($subUse -eq 'false') { $subEntry['use'] = $false } - $nestedItems[$subName] = $subEntry - } - $valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line') - if ($valIsLine) { - if ($useV -eq 'false') { $rawVal['use'] = $false } - if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems } - $dict[$pName] = $rawVal - } elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) { - $wrap = [ordered]@{ value = $rawVal } - if ($useV -eq 'false') { $wrap['use'] = $false } - if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems } - $dict[$pName] = $wrap - } else { - $dict[$pName] = $rawVal - } - } - return $dict -} - -# Build conditionalAppearance array. -function Build-ConditionalAppearance { - param($caNode, [string]$loc) - if (-not $caNode) { return @() } - $out = @() - $i = 0 - foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) { - $entry = [ordered]@{} - $scopeNode = $it.SelectSingleNode("dcsset:scope", $ns) - if ($scopeNode -and $scopeNode.HasChildNodes) { - [Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)") - } - $selNode = $it.SelectSingleNode("dcsset:selection", $ns) - if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { - $entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection" - } - $filterNode = $it.SelectSingleNode("dcsset:filter", $ns) - if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { - $f = @() - foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter") - if ($null -ne $bi) { $f += $bi } - } - $entry['filter'] = $f - } - $appNode = $it.SelectSingleNode("dcsset:appearance", $ns) - $ap = Get-SettingsAppearance $appNode - if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap } - $presNode = $it.SelectSingleNode("dcsset:presentation", $ns) - if ($presNode) { - # Сохраняем форму по xsi:type: LocalStringType (даже ru-only) → объект → компилятор эмитит LocalStringType. - $pres = Get-PresByType $presNode - if ($null -ne $pres -and $pres -ne '') { $entry['presentation'] = $pres } - } - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - if ($vmN) { $entry['viewMode'] = $vmN.InnerText } - $usid = Get-Text $it "dcsset:userSettingID" - if ($usid) { $entry['userSettingID'] = 'auto' } - $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($uspN) { - $usp = Get-PresText $uspN - if ($usp) { $entry['userSettingPresentation'] = $usp } - } - $useV = Get-Text $it "dcsset:use" - if ($useV -eq 'false') { $entry['use'] = $false } - $useInDontUse = @() - foreach ($ch in $it.ChildNodes) { - if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue } - if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') { - $shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1) - $useInDontUse += $shortName - } - } - if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse } - $out += $entry - $i++ - } - return ,$out -} - -# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора). -# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>), -# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows. -function Add-Layout { - param($obj, $node) - # Общие свойства элемента (любой тип): default/drag/skip - if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true } - $soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') } - # EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - $esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') } - $edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') } - $fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm } - # AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault). - # Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики). - $amwNode = Get-Child $node 'AutoMaxWidth' - if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false } - elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true } - elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true } - $mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw } - if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false } - $mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh } - $w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w } - $h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h } - # Stretch: захват фактического значения (true И false — платформа эмитит явное) - $hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') } - $vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') } - $gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha } - $gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva } - $ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha } - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') } - } - $fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha } - $hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha } - # ColumnGroup: динамический заголовок из данных + формат заголовка (ML-текст) - $hdp = Get-Child $node 'HeaderDataPath'; if ($hdp) { $obj['headerDataPath'] = $hdp } - $hfNode = $node.SelectSingleNode("lf:HeaderFormat", $ns); if ($hfNode) { $hf = Get-LangText $hfNode; if ($null -ne $hf) { $obj['headerFormat'] = $hf } } -} - -# TitleLocation у check/radio (зеркало Emit-TitleLocation): -# тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем. -function Add-TitleLocation { - param($obj, $node, [string]$smartDefault) - $tl = Get-Child $node 'TitleLocation' - if ($null -eq $tl) { $obj['titleLocation'] = '' } - elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() } -} - -# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } -# в порядке документа. Имена обработчиков всегда явные (как у событий формы) — -# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим. -function Get-Events { - param($node, [string]$elName) - $ev = $node.SelectSingleNode("lf:Events", $ns) - if (-not $ev) { return $null } - $events = [ordered]@{} - foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) { - $events[$e.GetAttribute("name")] = $e.InnerText - } - if ($events.Count -eq 0) { return $null } - return $events -} - -# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). -# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }. -# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует. -function Decompile-XrFlag { - param($node, [string]$tag) - $el = $node.SelectSingleNode("*[local-name()='$tag']") - if (-not $el) { return $null } - $commonNode = $el.SelectSingleNode("*[local-name()='Common']") - $common = ($commonNode -and $commonNode.InnerText -eq 'true') - $valNodes = @($el.SelectNodes("*[local-name()='Value']")) - if ($valNodes.Count -eq 0) { return $common } - $roles = [ordered]@{} - foreach ($v in $valNodes) { - $rn = $v.GetAttribute("name") - if ($rn -match '^Role\.') { $rn = $rn.Substring(5) } - $roles[$rn] = ($v.InnerText -eq 'true') - } - $o = [ordered]@{} - $o['common'] = $common - $o['roles'] = $roles - return $o -} - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel, -# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки). -# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index + -# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible). -# Голый элемент (только команда, Type=Auto) → строковый shorthand. -function Decompile-CommandInterface { - $ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns) - if (-not $ciNode) { return $null } - $ci = [ordered]@{} - foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) { - $pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns) - if (-not $pn) { continue } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) { - $o = [ordered]@{} - $cmd = Get-Child $it 'Command' - $o['command'] = "$cmd" - $ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty } - $at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at } - $cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg } - $idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx } - $dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') } - $vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis } - # Голый элемент (только command) → строка-shorthand; иначе объект - if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) } - } - if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) } - } - if ($ci.Count -gt 0) { return $ci } - return $null -} - -# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть). -function Decompile-FunctionalOptions { - param($node) - $foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns) - if (-not $foNode) { return $null } - $opts = New-Object System.Collections.ArrayList - foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) { - $t = $it.InnerText.Trim() -replace '^FunctionalOption\.', '' - [void]$opts.Add($t) - } - if ($opts.Count -gt 0) { return ,@($opts) } - return $null -} - -# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions. -function Decompile-AttrColumn { - param($c) - $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") - $cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty } - $ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangTextWS $ctNode; if ($null -ne $t) { $co['title'] = $t } } - $cfc = Get-Child $c 'FillCheck'; if ($cfc) { $co['fillCheck'] = $cfc } # проверка заполнения колонки (как у реквизита) - $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles}) - $cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv } - $ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce } - return $co -} - -# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash -# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). -# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false. -# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:"). -# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект -# {src, loadTransparent?, transparentPixel?}. -function Get-PictureRef { - param($node, [string]$picTag) - $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns) - if (-not $ref -and -not $abs) { return $null } - $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } - $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) - $ltTrue = ($lt -and $lt.InnerText -eq 'true') - $tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns) - if (-not $ltTrue -and -not $tpx) { return $src } - $o = [ordered]@{ src = $src } - if ($ltTrue) { $o['loadTransparent'] = $true } - if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } - return $o -} - -# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values): -# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии -# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture -# + отдельный loadTransparent:false. -function Set-CommandPicture { - param($obj, $node) - $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) - if (-not $ref -and -not $abs) { return } - $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } - $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns) - $ltFalse = ($lt -and $lt.InnerText -eq 'false') - $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) - if ($tpx) { - $o = [ordered]@{ src = $src } - if ($ltFalse) { $o['loadTransparent'] = $false } - $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } - $obj['picture'] = $o - } else { - $obj['picture'] = $src - if ($ltFalse) { $obj['loadTransparent'] = $false } - } -} - -# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты. -function Build-FontValue { - param($f) - $present = @() - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - if ($f.HasAttribute($a)) { $present += $a } - } - # Чистый style-ref (ref + kind=StyleItem) → строка-шорткат - if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') { - return $f.GetAttribute('ref') - } - $o = [ordered]@{} - foreach ($k in $present) { - $v = $f.GetAttribute($k) - if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v } - elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') } - else { $o[$k] = $v } - } - return $o -} - -# Граница <Border> → строка-ref (из стиля) или объект {width, style}. -function Build-BorderValue { - param($b) - if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') } - $o = [ordered]@{} - if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } - $st = $b.SelectSingleNode("v8ui:style", $ns) - if ($st) { $o['style'] = $st.InnerText } - return $o -} - -# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка. -function Add-Appearance { - param($obj, $node) - $colorMap = @{ - 'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor' - 'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor' - 'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor' - } - foreach ($tag in $colorMap.Keys) { - $c = $node.SelectSingleNode("lf:$tag", $ns) - if ($c) { $obj[$colorMap[$tag]] = $c.InnerText } - } - foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) { - $f = $node.SelectSingleNode("lf:$($pair[0])", $ns) - if ($f) { $obj[$pair[1]] = (Build-FontValue $f) } - } - $b = $node.SelectSingleNode("lf:Border", $ns) - if ($b) { $obj['border'] = (Build-BorderValue $b) } -} - -function Add-CommonProps { - param($obj, $node, [string]$elName) - Add-Appearance $obj $node - if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true } - if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true } - if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true } - $uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv } - $titleNode = $node.SelectSingleNode("lf:Title", $ns) - if ($titleNode) { - $t = Get-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок) - if ($null -ne $t) { $obj['title'] = $t } - # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) - } - $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) - if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } } - $ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr } - # Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon) - $hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp } - $fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp } - $ev = Get-Events $node $elName - if ($ev) { $obj['events'] = $ev } - # CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture): - # список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает. - $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) - if ($csNode) { - $exc = New-Object System.Collections.ArrayList - foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } - if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } - } -} - -# --- 3. Type decompile (inverse of Emit-Type) --- -function Decompile-Type { - param($typeNode) - if (-not $typeNode) { return $null } - $parts = New-Object System.Collections.ArrayList - foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { - $raw = $vt.InnerText.Trim() - $short = $raw - # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). - switch -regex ($raw) { - '^xs:string$' { - $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) - $al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns) - $fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно - if ($len -and [int]$len.InnerText -gt 0) { - $short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" } - } else { $short = "string" } # Length=0 → всегда Variable (корпус) - break - } - '^xs:decimal$' { - $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) - $f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) - $sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns) - $dd = if ($d) { $d.InnerText } else { '0' } - $ff = if ($f) { $f.InnerText } else { '0' } - if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } - break - } - '^xs:boolean$' { $short = "boolean"; break } - '^xs:dateTime$' { - $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) - $dfv = if ($df) { $df.InnerText } else { 'DateTime' } - switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } - break - } - '^cfg:(.+)$' { $short = $matches[1]; break } - '^(v8|v8ui):' { - # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом - # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. - $rev = @{ - 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' - 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' - 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' - 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' - } - if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } - break - } - default { $short = $raw } - } - [void]$parts.Add($short) - } - # TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида». - # Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. - foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) { - $raw = $ts.InnerText.Trim() - $short = $raw -replace '^(v8ui|v8|cfg):', '' - [void]$parts.Add($short) - } - # TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так - # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально - # стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID). - foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) { - [void]$parts.Add("typeid:" + $ti.InnerText.Trim()) - } - if ($parts.Count -eq 0) { return $null } - if ($parts.Count -eq 1) { return $parts[0] } - return ($parts -join ' | ') -} - -# Ограничения использования (useRestriction/attributeUseRestriction) → объект {field?,condition?,group?,order?}. -function Build-RestrictObj { - param($node) - $r = [ordered]@{} - foreach ($k in 'field','condition','group','order') { if ((Get-Child $node $k) -eq 'true') { $r[$k] = $true } } - return $r -} - -# Вычисляемое поле DataSet динамического списка (<CalculatedField>) → объектная модель. -# Зеркало эмиссии form-compile (Emit-CalcFields). Грамматика — как skd calculatedFields, -# + форм-extras presentationExpression/orderExpression (в namespace dcscommon). -function Build-CalcField { - param($cfNode) - $o = [ordered]@{} - $o['dataPath'] = Get-Child $cfNode 'dataPath' - $o['expression'] = Get-Child $cfNode 'expression' - $tn = $cfNode.SelectSingleNode("dcssch:title", $ns) - if ($tn) { $t = Get-LangText $tn; if ($null -ne $t) { $o['title'] = $t } } - $vt = $cfNode.SelectSingleNode("dcssch:valueType", $ns) - if ($vt) { $v = Decompile-Type $vt; if ($v) { $o['valueType'] = $v } } - $ur = $cfNode.SelectSingleNode("dcssch:useRestriction", $ns) - if ($ur) { - $r = [ordered]@{} - foreach ($k in 'field','condition','group','order') { if ((Get-Child $ur $k) -eq 'true') { $r[$k] = $true } } - if ($r.Count -gt 0) { $o['useRestriction'] = $r } - } - $pe = Get-Child $cfNode 'presentationExpression' - if ($null -ne $pe -and $pe -ne '') { $o['presentationExpression'] = $pe } - $oeNodes = @($cfNode.SelectNodes("dcssch:orderExpression", $ns)) - if ($oeNodes.Count -gt 0) { - $oes = New-Object System.Collections.ArrayList - foreach ($oen in $oeNodes) { - $eo = [ordered]@{} - $exprN = $oen.SelectSingleNode("*[local-name()='expression']") - $otN = $oen.SelectSingleNode("*[local-name()='orderType']") - $aoN = $oen.SelectSingleNode("*[local-name()='autoOrder']") - $eo['expression'] = if ($exprN) { $exprN.InnerText } else { '' } - if ($otN -and $otN.InnerText -ne 'Asc') { $eo['orderType'] = $otN.InnerText } - if ($aoN -and $aoN.InnerText -eq 'true') { $eo['autoOrder'] = $true } - [void]$oes.Add($eo) - } - $o['orderExpression'] = @($oes) - } - return $o -} - -# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии -# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и -# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter. -function Build-DLInputParameters { - param($ipNode) - $items = New-Object System.Collections.ArrayList - foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) { - $io = [ordered]@{} - $io['parameter'] = Get-Child $it 'parameter' - $useN = $it.SelectSingleNode("dcscor:use", $ns) - if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false } - $valN = $it.SelectSingleNode("dcscor:value", $ns) - if ($valN) { - $vt = $valN.GetAttribute("type", $NS_XSI) - if ($vt -match 'ChoiceParameters$') { - $cps = New-Object System.Collections.ArrayList - foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { - $cpo = [ordered]@{} - $cpo['name'] = Get-Child $cpi 'choiceParameter' - $vals = New-Object System.Collections.ArrayList - foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) { - [void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI)))) - } - $cpo['values'] = @($vals) - [void]$cps.Add($cpo) - } - $io['choiceParameters'] = @($cps) - } elseif ($vt -match 'ChoiceParameterLinks$') { - $cpls = New-Object System.Collections.ArrayList - foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { - $cpo = [ordered]@{} - $cpo['name'] = Get-Child $cpi 'choiceParameter' - $cpo['value'] = Get-Child $cpi 'value' - $md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md } - [void]$cpls.Add($cpo) - } - $io['choiceParameterLinks'] = @($cpls) - } elseif ($vt -match 'TypeLink$') { - # Связь по типу (dcscor:TypeLink): field + linkItem — структурное значение, - # НЕ склеивать InnerText в строку ("СчетДт"+"1"="СчетДт1"). - $tlo = [ordered]@{} - $tlf = Get-Child $valN 'field'; if ($null -ne $tlf) { $tlo['field'] = $tlf } - $tli = Get-Child $valN 'linkItem'; if ($null -ne $tli) { $tlo['linkItem'] = if ($tli -match '^-?\d+$') { [int]$tli } else { $tli } } - $io['typeLink'] = $tlo - } else { - if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt } - } - } - [void]$items.Add($io) - } - return @($items) -} - -# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного -# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, -# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). -function Build-FormDataParameters { - param($dpNode) - $entries = @() - foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { - $pn = Get-Text $it "dcscor:parameter" - $use = Get-Text $it "dcscor:use" - $valNodes = @($it.SelectNodes("dcscor:value", $ns)) - $valNode = if ($valNodes.Count -ge 1) { $valNodes[0] } else { $null } - $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($valNode -or $usidN -or $vmN -or $uspN) { - $obj = [ordered]@{ parameter = $pn } - if ($valNodes.Count -gt 1) { - # Список значений параметра (valueListAllowed) — захватываем ВСЕ <dcscor:value> массивом - $obj['value'] = @($valNodes | ForEach-Object { $_.InnerText }) - $vt0 = $valNodes[0].GetAttribute("type", $NS_XSI); if ($vt0) { $obj['valueType'] = $vt0 } - } elseif ($valNode) { - if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } - else { - $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText - if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } - elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } - else { $obj['value'] = $vVal } - if ($vType) { $obj['valueType'] = $vType } - } - } - if ($use -eq 'false') { $obj['use'] = $false } - if ($usidN) { $obj['userSettingID'] = 'auto' } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } - $entries += $obj - } else { - $s = $pn; if ($use -eq 'false') { $s += ' @off' } - $entries += $s - } - } - return ,$entries -} - -function Build-DLParameter { - param($pNode) - $name = Get-Child $pNode 'name' - $o = [ordered]@{} - $o['name'] = $name - # title — опускаем, если совпадает с авто-выводом из имени (ru-only) - $titleNode = $pNode.SelectSingleNode("dcssch:title", $ns) - if ($titleNode) { - $t = Get-LangText $titleNode - if ($null -ne $t) { - $auto = Title-FromName -name $name - if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t } - } - } - # valueType - $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) - $typeVal = $null - if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } - # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет <value xsi:nil/> - # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → - # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) - $vNodes = @($pNode.SelectNodes("dcssch:value", $ns)) - if ($vNodes.Count -gt 1) { - # valueListAllowed: список значений — захватываем ВСЕ <dcssch:value> массивом - $o['value'] = @($vNodes | ForEach-Object { Convert-TypedValue -raw $_.InnerText -xsiType ($_.GetAttribute("type", $NS_XSI)) }) - } elseif ($vNodes.Count -eq 1) { - $vNode = $vNodes[0] - if ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true') { - $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) - } elseif ((Get-Child $pNode 'valueListAllowed') -eq 'true') { - $o['value'] = $null - } - } - # useRestriction — опускаем true (дефолт), фиксируем false - if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } - # expression - $expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr } - # availableValues - $avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns)) - if ($avNodes.Count -gt 0) { - $avs = New-Object System.Collections.ArrayList - foreach ($avn in $avNodes) { - $avo = [ordered]@{} - $avv = $avn.SelectSingleNode("dcssch:value", $ns) - if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) } - else { $avo['value'] = $null } - $avp = $avn.SelectSingleNode("dcssch:presentation", $ns) - if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } } - [void]$avs.Add($avo) - } - $o['availableValues'] = @($avs) - } - # valueListAllowed / availableAsField - if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true } - if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false } - # inputParameters - $ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns) - if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } } - # denyIncompleteValues / use - if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true } - $use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use } - - # Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект. - $keys = @($o.Keys) - if ($keys.Count -eq 1) { return $name } - if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) } - return $o -} - -# --- 4. Element dispatch --- -$ELEMENT_KEY = @{ - 'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check'; - 'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField'; - 'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar'; - 'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup'; - 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl'; - 'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc'; - 'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar'; - 'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner'; - 'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart' -} - -# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value. -$GENERIC_SCALARS = @( - @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } - @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } - @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } - @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } - @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } - @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } - @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } - @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } - @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } - @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } - @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } - @{ Tag='Mask'; Key='mask'; Kind='value' } - @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } - @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } - @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } - # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора) - @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } - @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } - @{ Tag='Output'; Key='output'; Kind='value' } - @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } - @{ Tag='PointerType'; Key='pointerType'; Kind='value' } - @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } - @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } - @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } - @{ Tag='Protection'; Key='protection'; Kind='bool' } - @{ Tag='Edit'; Key='edit'; Kind='bool' } - @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } - @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } - @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } - @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } - @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } - @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } - @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } - @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } - @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } - @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } - @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } - @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } - @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } - @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } - # Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление / - # выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка - @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } - @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } - @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } - @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } - @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } - @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } - @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } - @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } - # Сочетание клавиш — общее свойство элемента (команда — отдельный путь) - @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } - # Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно) - @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } - @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } - @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } - @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } - @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } - @{ Tag='Shape'; Key='shape'; Kind='value' } - @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } - @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } - # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр - @{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' } - # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) - @{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' } - @{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' } - @{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' } - @{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' } - # Режим авто-показа кнопок открытия/очистки (input, enum) - @{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' } - @{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' } - # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент) - @{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' } - @{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' } - @{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' } - @{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' } - # Хвост листовых скаляров (по 1): автокоррекция / уникальность команды / пустое множ.значение / гориз.сжатие - @{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' } - @{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' } - @{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' } - @{ Tag='AllowInputEmptyMultipleValues'; Key='allowInputEmptyMultipleValues'; Kind='bool' } - @{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' } -) - -# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает. -function Add-GenericScalars { - param($obj, $node) - foreach ($s in $GENERIC_SCALARS) { - if ($obj.Contains($s.Key)) { continue } - $v = Get-Child $node $s.Tag - if ($null -eq $v) { continue } - if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v } - } -} - -function Decompile-Children { - param($parentNode, [string]$childContainer = 'ChildItems') - $container = $parentNode.SelectSingleNode("lf:$childContainer", $ns) - if (-not $container) { return $null } - $list = New-Object System.Collections.ArrayList - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - if ($COMPANION_TAGS -contains $child.LocalName) { continue } - $el = Decompile-Element $child - if ($el) { [void]$list.Add($el) } - } - if ($list.Count -eq 0) { return $null } - return ,@($list) -} - -# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом -# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing). -# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ: -# autofill=false (нет детей) → совпадает с дефолтом → молчим; -# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта). -function Decompile-CompanionPanel { - param($node, [string]$tag, [bool]$isDynListTable = $false) - $p = $node.SelectSingleNode("lf:$tag", $ns) - if (-not $p) { return $null } - $autofillRaw = Get-Child $p 'Autofill' - $halign = Get-Child $p 'HorizontalAlign' - $kids = Decompile-Children $p - $hasKids = $kids -and @($kids).Count -gt 0 - if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) { - if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим - return [ordered]@{ autofill = $true } # голая панель → отклонение - } - if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null } - $o = [ordered]@{} - if ($halign) { $o['horizontalAlign'] = $halign } - if ($autofillRaw -eq 'false') { $o['autofill'] = $false } - elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true } - if ($hasKids) { $o['children'] = $kids } - return $o -} - -# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null. -# У RadioButtonField и InputField. -function Decompile-ChoiceList { - param($node) - $cl = $node.SelectSingleNode("lf:ChoiceList", $ns) - if (-not $cl) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) { - $valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns) - $presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns) - $ci = [ordered]@{} - if ($valNode) { - if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { - # nil-значение элемента choiceList — компилятор эмитит <Value xsi:nil="true"/> - # (иначе Convert-TypedValue вернул бы "" → typed-empty xs:string). - $ci['valueType'] = 'nil' - } else { - $xsiType = $valNode.GetAttribute("type", $NS_XSI) - $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType - # Системное перечисление (ent:*) / иной не-примитивный тип → сохраняем valueType - # (Normalize-ChoiceValue вывела бы xs:string). DesignTimeRef обычно авто-детектится - # компилятором по named-ref (Enum.X.Y), НО raw-ссылка по GUID (GUID.GUID) — нет → сохраняем. - if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and ` - ($xsiType -ne 'xr:DesignTimeRef' -or "$($valNode.InnerText)" -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-')) { - $ci['valueType'] = $xsiType - } - } - } - # Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер, - # подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения). - if ($presNode) { - $p = Get-LangTextWS $presNode # восстановление значимого пробела (whitespace-presentation) - if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' } - } - [void]$items.Add($ci) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо -# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value). -function Convert-ChoiceParamValue { - param($valNode) - $vt = $valNode.GetAttribute("type", $NS_XSI) - if ($vt -match 'FixedArray$') { - $arr = New-Object System.Collections.ArrayList - foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) { - $inner = $it.SelectSingleNode("lf:Value", $ns) - if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) } - } - return ,@($arr) - } - return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt -} - -# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}]. -function Decompile-ChoiceParameters { - param($node) - $cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns) - if (-not $cpn) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($cpn.SelectNodes("app:item", $ns))) { - $o = [ordered]@{} - $o['name'] = $it.GetAttribute("name") - $valNode = $it.SelectSingleNode("app:value/lf:Value", $ns) - if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode } - [void]$items.Add($o) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> → -# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит). -function Decompile-ChoiceParameterLinks { - param($node) - $cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns) - if (-not $cln) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) { - $o = [ordered]@{} - $o['name'] = Get-Text $lk "xr:Name" - $o['dataPath'] = Get-Text $lk "xr:DataPath" - $vc = Get-Text $lk "xr:ValueChange" - if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc } - [void]$items.Add($o) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}. -function Decompile-TypeLink { - param($node) - $tn = $node.SelectSingleNode("lf:TypeLink", $ns) - if (-not $tn) { return $null } - $o = [ordered]@{} - $o['dataPath'] = Get-Text $tn "xr:DataPath" - $li = Get-Text $tn "xr:LinkItem" - if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li } - return $o -} - -# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}). -function Add-FormatProps { - param($obj, $node) - $fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } } - $efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } } -} - -# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation. -# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно). -function Add-AdditionCore { - param($obj, $node, [string]$elName) - $src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText } - Add-CommonProps $obj $node $elName - $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } -} - -# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту -# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем. -function Decompile-TableAdditions { - param($tableNode, [string]$tableName) - $tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' } - $map = [ordered]@{} - foreach ($child in $tableNode.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - if (-not $tagToKey.ContainsKey($child.LocalName)) { continue } - $key = $tagToKey[$child.LocalName] - $nm = $child.GetAttribute("name") - $o = [ordered]@{}; $o[$key] = $nm - Add-AdditionCore $o $child $nm - Add-Layout $o $child - $o.Remove($key) # имя авто - if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт - if ($o.Count -gt 0) { $map[$key] = $o } - } - if ($map.Count -gt 0) { return $map } - return $null -} - -# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode). -# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост. -function Decompile-SimpleField { - param($obj, $node, [string]$name, [string]$key) - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } -} - -# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField). -function Add-GaugeScalars { - param($obj, $node, $tags) - foreach ($p in $tags) { - $v = Get-Child $node $p - if ($null -eq $v) { continue } - $key = $p.Substring(0,1).ToLower() + $p.Substring(1) - if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v } - } -} - -function Decompile-Element { - param($node) - $tag = $node.LocalName - if (-not $ELEMENT_KEY.ContainsKey($tag)) { - Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag" - } - $key = $ELEMENT_KEY[$tag] - $name = $node.GetAttribute("name") - $obj = [ordered]@{} - - switch ($tag) { - 'UsualGroup' { - # group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем). - # group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление - # не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical' - # — иначе компилятор додумает <Group>Vertical</Group> там, где его нет. - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } - if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } - $behavior = Get-Child $node 'Behavior' - if ($behavior) { - $bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' } - if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior } - } - $obj['name'] = $name - Add-CommonProps $obj $node $name - $rep = Get-Child $node 'Representation' - if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } } - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } # использование текущей строки группы - $crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } } - if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false } - if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true } - # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы) - Add-FormatProps $obj $node - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'ColumnGroup' { - # columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление - # не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет. - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' } - if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } - $obj['name'] = $name - Add-CommonProps $obj $node $name - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - $sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'InputField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - # MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true») - $mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') } - # PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true») - $pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') } - $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangTextWS $ih; if ($t) { $obj['inputHint'] = $t } } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } - } - # InputField-специфичные bool-скаляры (захват «как есть») - foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } - } - # InputField-специфичные value-скаляры (захват «как есть») - foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v } - } - # MinValue/MaxValue — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип: - # xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string). - foreach ($p in @('MinValue','MaxValue')) { - $mn = $node.SelectSingleNode("lf:$p", $ns) - if ($mn) { - $xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText - $key = $p.Substring(0,1).ToLower() + $p.Substring(1) - if ($xt -match 'decimal|int') { - if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt } - } else { $obj[$key] = $txt } - } - } - # Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор. - # availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type. - $tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) } - $atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } } - $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } - $cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp } - if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false } - $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } - Add-FormatProps $obj $node - # Параметры выбора / Связи параметров выбора / Связь по типу - $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } - $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } - $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } - } - 'CheckBoxField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - # CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение - $cbt = Get-Child $node 'CheckBoxType' - if ($null -eq $cbt) { $obj['checkBoxType'] = '' } - elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) } - Add-TitleLocation $obj $node 'Right' - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) - $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - Add-FormatProps $obj $node - } - 'RadioButtonField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - Add-TitleLocation $obj $node 'None' - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } - $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } - } - 'LabelDecoration' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - # title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip) - $tiNode = $node.SelectSingleNode("lf:Title", $ns) - if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } - } - 'LabelField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - # LabelField: тег <Hiperlink> (опечатка платформы), не <Hyperlink> - if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true } - # PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения - $pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input - $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - Add-FormatProps $obj $node - } - 'PictureDecoration' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - # title декорации — единая ML-text форма с formatted (атрибут <Title formatted> у PictureDecoration) - $tiNode = $node.SelectSingleNode("lf:Title", $ns) - if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } - $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } - $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) - if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs: - $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } - # Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности - $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) - if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - } - 'PictureField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - $vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp } - $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) - $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - } - 'CalendarField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm } - $scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') } - $wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim } - $him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him } - $smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') } - } - 'Table' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') } - $cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') } - if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true } - # enableDrag — теперь общий (Add-Layout, фактическое значение) - if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null } - if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } - if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } - # Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout) - $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr } - # Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table) - $hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh } - $fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh } - # Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through - $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } - # Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through - $rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr } - # CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем - # (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват). - $cbl = Get-Child $node 'CommandBarLocation' - if (Has-Child $node 'UpdateOnDataChange') { - if ($null -eq $cbl) { $obj['commandBarLocation'] = '' } - elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl } - } elseif ($cbl) { $obj['commandBarLocation'] = $cbl } - $ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl } - $vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl } - $scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl } - # --- Общие свойства таблицы (любой тип таблицы, не только динсписок) --- - if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true } - $selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm } - $rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm } - if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false } - if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false } - if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true } - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill). - $taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') } - if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true } - $soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin } - $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } - $itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv } - # RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel) - $rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp } - $rpdp = Get-Child $node 'RowPictureDataPath' - # --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) --- - if (Has-Child $node 'UpdateOnDataChange') { - if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true } - $arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp } - $cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi } - if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true } - if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false } - if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true } - $uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc } - if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false } - # RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture - if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' } - elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp } - $usg = Get-Child $node 'UserSettingsGroup' - if ($usg) { if ($usg -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: UserSettingsGroup '$usg' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['userSettingsGroup'] = $usg } } - } elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp } - $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) - if ($csNode) { - $exc = New-Object System.Collections.ArrayList - foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } - if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } - } - $cols = Decompile-Children $node - if ($cols) { $obj['columns'] = $cols } - # Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions - $addMap = Decompile-TableAdditions $node $name - if ($addMap) { $obj['additions'] = $addMap } - } - 'Pages' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr } - $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Page' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } - if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } - # Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false) - $pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp } - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы) - Add-FormatProps $obj $node - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Button' { - $obj[$key] = $name - $cmd = Get-Child $node 'CommandName' - if ($cmd) { - if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] } - elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] } - elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" } - else { $obj['commandName'] = $cmd } - } - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - # Parameter команды: xr:MDObjectRef (объект метаданных, строка) или v8:TypeDescription (тип → {type}) - $btnParam = $node.SelectSingleNode("lf:Parameter", $ns) - if ($btnParam) { - $pxt = $btnParam.GetAttribute("type", $NS_XSI) - if ($pxt -match 'TypeDescription$') { $pt = Decompile-Type $btnParam; if ($pt) { $obj['parameter'] = [ordered]@{ type = $pt } } } - elseif ($btnParam.InnerText) { $obj['parameter'] = $btnParam.InnerText } - } - Add-CommonProps $obj $node $name - $type = Get-Child $node 'Type' - if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } } - if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true } - if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true } - Set-CommandPicture $obj $node - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic } - } - 'ButtonGroup' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $cs = Get-Child $node 'CommandSource' - if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'CommandBar' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $cs = Get-Child $node 'CommandSource' - if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } - $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } - if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Popup' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - Set-CommandPicture $obj $node - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - # Источник команд попапа (Form / FormCommandPanelGlobalCommands / Item.X) — как у ButtonGroup/CommandBar - $cs = Get-Child $node 'CommandSource' - if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'TextDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') } - 'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') } - 'ChartField' { Decompile-SimpleField $obj $node $name $key } - 'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key } - 'PlannerField' { Decompile-SimpleField $obj $node $name $key } - 'PeriodField' { Decompile-SimpleField $obj $node $name $key } - 'DendrogramField' { Decompile-SimpleField $obj $node $name $key } - 'GanttChartField' { - Decompile-SimpleField $obj $node $name $key - # Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element. - # Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере). - $tblNode = $node.SelectSingleNode("lf:Table", $ns) - if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode } - } - } - # DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть». - $di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di } - # title: "" — подавление авто-вывода: для типов, где компилятор вывел бы - # заголовок из имени, а в оригинале <Title> отсутствует. - if (-not $obj.Contains('title')) { - $autoTitle = $false - if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true } - elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) } - elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') } - if ($autoTitle) { $obj['title'] = '' } - } - Add-Layout $obj $node - Add-GenericScalars $obj $node - # extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) → - # строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }. - $etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns) - if ($etNode) { - $etTitle = $etNode.SelectSingleNode("lf:Title", $ns) - $textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null } - $etObj = [ordered]@{} - Add-Layout $etObj $etNode - Add-GenericScalars $etObj $etNode - Add-Appearance $etObj $etNode - # ToolTip самого компаньона (подсказка расширенной подсказки) — реальный текст (ML), не пустой Title - $etTT = $etNode.SelectSingleNode("lf:ToolTip", $ns) - if ($etTT) { $ttVal = Get-LangText $etTT; if ($null -ne $ttVal) { $etObj['tooltip'] = $ttVal } } - if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true } - if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true } - if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true } - # События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента - $etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv } - if ($etObj.Count -gt 0) { - if ($null -ne $textVal) { - if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } } - else { $etObj['text'] = $textVal } - # formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте - if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true } - } - $obj['extendedTooltip'] = $etObj - } elseif ($null -ne $textVal) { - $obj['extendedTooltip'] = $textVal - } - } - # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент) - $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange') - $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable - if ($null -ne $cb) { $obj['commandBar'] = $cb } - $cm = Decompile-CompanionPanel $node 'ContextMenu' - if ($null -ne $cm) { $obj['contextMenu'] = $cm } - return $obj -} - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. -# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings. -function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') } -function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v } -# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null. -function Get-PlannerValue { - param($node) - if (-not $node) { return $null } - if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null } - if ($node.InnerText) { return $node.InnerText } else { return $null } -} -function Build-PlannerFont { - param($node) - if (-not $node) { return $null } - $o = [ordered]@{} - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av } - } - if ($o.Count -eq 0) { return $null } - return $o -} -function Build-PlannerBorder { - param($node) - if (-not $node) { return $null } - $o = [ordered]@{} - $w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } - $st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText } - return $o -} -function Build-PlannerItem { - param($itn) - $o = [ordered]@{} - $valNode = $itn.SelectSingleNode("*[local-name()='value']") - if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText } - $o['text'] = (Get-Child $itn 'text') - $tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt } - $o['begin'] = (Get-Child $itn 'begin') - $o['end'] = (Get-Child $itn 'end') - $o['borderColor'] = (Get-Child $itn 'borderColor') - $o['backColor'] = (Get-Child $itn 'backColor') - $o['textColor'] = (Get-Child $itn 'textColor') - $fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $o['replacementDate'] = (Get-Child $itn 'replacementDate') - $o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted')) - $o['id'] = (Get-Child $itn 'id') - $o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted')) - $brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd } - $o['editMode'] = (Get-Child $itn 'editMode') - return $o -} -function Build-PlannerDimElement { - param($eln) - $o = [ordered]@{} - $v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } - $o['text'] = (Get-Child $eln 'text') - $o['borderColor'] = (Get-Child $eln 'borderColor') - $o['backColor'] = (Get-Child $eln 'backColor') - $o['textColor'] = (Get-Child $eln 'textColor') - $fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $subs = New-Object System.Collections.ArrayList - foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) } - if ($subs.Count -gt 0) { $o['elements'] = @($subs) } - $sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') } - $o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted')) - return $o -} -function Build-PlannerDimension { - param($dn) - $o = [ordered]@{} - $v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } - $o['text'] = (Get-Child $dn 'text') - $o['borderColor'] = (Get-Child $dn 'borderColor') - $o['backColor'] = (Get-Child $dn 'backColor') - $o['textColor'] = (Get-Child $dn 'textColor') - $fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $els = New-Object System.Collections.ArrayList - foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) } - if ($els.Count -gt 0) { $o['elements'] = @($els) } - $o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted')) - return $o -} -function Build-PlannerLevel { - param($lvn) - $o = [ordered]@{} - $o['measure'] = (Get-Child $lvn 'measure') - $o['interval'] = (PLD-Int (Get-Child $lvn 'interval')) - $o['show'] = (PLD-Bool (Get-Child $lvn 'show')) - $lineNode = $lvn.SelectSingleNode("*[local-name()='line']") - if ($lineNode) { - $ln = [ordered]@{} - $w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w } - $g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') } - $st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText } - $o['line'] = $ln - } - $o['scaleColor'] = (Get-Child $lvn 'scaleColor') - $o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule') - $fmtNode = $lvn.SelectSingleNode("*[local-name()='format']") - if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } } - $labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']") - if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } } - $o['backColor'] = (Get-Child $lvn 'backColor') - $o['textColor'] = (Get-Child $lvn 'textColor') - $o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels')) - return $o -} -function Build-PlannerTimeScale { - param($tsn) - $o = [ordered]@{} - $o['placement'] = (Get-Child $tsn 'placement') - $levels = New-Object System.Collections.ArrayList - foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) } - $o['levels'] = @($levels) - $o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent')) - $o['backColor'] = (Get-Child $tsn 'backColor') - $o['textColor'] = (Get-Child $tsn 'textColor') - $o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel')) - return $o -} -function Build-PlannerSettings { - param($setNode) - $pl = [ordered]@{} - $itemNodes = @($setNode.SelectNodes("*[local-name()='item']")) - if ($itemNodes.Count -gt 0) { - $items = New-Object System.Collections.ArrayList - foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) } - $pl['items'] = @($items) - } - $dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']")) - if ($dimNodes.Count -gt 0) { - $dims = New-Object System.Collections.ArrayList - foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) } - $pl['dimensions'] = @($dims) - } - $pl['borderColor'] = (Get-Child $setNode 'borderColor') - $pl['backColor'] = (Get-Child $setNode 'backColor') - $pl['textColor'] = (Get-Child $setNode 'textColor') - $pl['lineColor'] = (Get-Child $setNode 'lineColor') - $fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt } - $pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod') - $pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod') - $pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale')) - $pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders')) - $pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders')) - $wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']") - if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } } - $pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit') - $pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition')) - $pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent')) - $pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent')) - $tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']") - if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) } - $perNode = $setNode.SelectSingleNode("*[local-name()='period']") - if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } } - $pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate')) - $pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation') - $pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient') - $pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth')) - $pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight')) - $pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth')) - $pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight')) - $pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader') - $pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader') - $brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd } - $pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType') - return $pl -} - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок: -# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии) -# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML), -# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип). -$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } -$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 } -$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } -function Conv-ChartScalar { - param([string]$v) - if ($v -eq 'true') { return $true } - if ($v -eq 'false') { return $false } - return $v -} -function Build-ChartNode { - param($n, [string]$name) - # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) - if ($CHART_ML_FIELDS.Contains($name)) { - $ml = Get-LangText $n - if ($null -eq $ml) { return '' } else { return $ml } - } - $kids = @($n.SelectNodes("*")) - if ($kids.Count -eq 0) { - # лист: attrs-only (шрифт/gaugeQualityBands) или текст - $attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' }) - if ($attrs.Count -gt 0) { - $o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o - } - return (Conv-ChartScalar $n.InnerText) - } - # line/border: дочерний v8ui:style (+ width[/gap]) - $styleChild = $n.SelectSingleNode("*[local-name()='style']") - if ($styleChild) { - $o = [ordered]@{} - $w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } - $g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') } - $o['style'] = $styleChild.InnerText - return $o - } - # вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени - $o = [ordered]@{} - foreach ($c in $kids) { - $ln = $c.LocalName - $val = Build-ChartNode $c $ln - if ($CHART_SERIES_FIELDS.Contains($ln)) { - if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList } - [void]$o[$ln].Add($val) - } elseif ($o.Contains($ln)) { - if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp } - [void]$o[$ln].Add($val) - } else { - $o[$ln] = $val - } - } - # нормализуем ArrayList → @() для сериализации - foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } } - return $o -} -function Build-ChartSettings { - param($setNode) - return (Build-ChartNode $setNode '') -} - -# --- 5. Form-level assembly --- -$dsl = [ordered]@{} - -$titleNode = $root.SelectSingleNode("lf:Title", $ns) -if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } - -# properties (прямые скаляры под <Form>, PascalCase → camelCase) -$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing','CustomSettingsFolder','SettingsStorage','Enabled','Scale') -$props = [ordered]@{} -foreach ($pn in $KNOWN_FORM_PROPS) { - $v = Get-Child $root $pn - if ($null -ne $v) { - $camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1) - if ($v -eq 'true') { $props[$camel] = $true } - elseif ($v -eq 'false') { $props[$camel] = $false } - elseif ($v -match '^\d+$') { $props[$camel] = [int]$v } - else { $props[$camel] = $v } - } -} -# Ссылка на члена формы по id ("N:uuid") в groupList/customSettingsFolder НЕ воспроизводима: наш -# компилятор переназначает id, и ссылка указала бы не туда (либо вовсе dangling — платформа сама их -# часто не разрешает). Резолв в имя ненадёжен (N не всегда соответствует named-элементу). Игнорируем -# с предупреждением (имя задаётся вручную через form-edit). Имя-форма захватывается как есть. -foreach ($refKey in 'groupList','customSettingsFolder') { - if ($props.Contains($refKey) -and "$($props[$refKey])" -match '^\d+:[0-9a-fA-F]{8}-') { - [Console]::Error.WriteLine("form-decompile: $refKey = '$($props[$refKey])' — ссылка на члена формы по id, не воспроизводима (id переназначаются), опущена. Задайте по имени через form-edit.") - $props.Remove($refKey) - } -} -# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим: -# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title); -# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить). -if ($dsl.Contains('title')) { - if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' } - elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } -} -if ($props.Count -gt 0) { $dsl['properties'] = $props } - -# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок -$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns) -if ($mdcb) { - $names = New-Object System.Collections.ArrayList - foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) { - $v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) } - } - if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) } -} - -# excludedCommands (form-level <CommandSet>) -$csForm = $root.SelectSingleNode("lf:CommandSet", $ns) -if ($csForm) { - $excForm = New-Object System.Collections.ArrayList - foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) } - if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) } -} - -# events (form-level) -$evForm = Get-Events $root $null -if ($evForm) { - # form-level: компилятор хранит как {Event: handler} напрямую - $evMap = [ordered]@{} - $evNode = $root.SelectSingleNode("lf:Events", $ns) - foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText } - if ($evMap.Count -gt 0) { $dsl['events'] = $evMap } -} - -# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо -# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить -# отклонение (голый корень при наличии cmdBar). -function Test-AnyCmdBar { - param($list) - if (-not $list) { return $false } - foreach ($e in $list) { - if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true } - if ($e -is [System.Collections.IDictionary]) { - if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true } - if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true } - } - } - return $false -} - -# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение) -$elemList = New-Object System.Collections.ArrayList -$elements = Decompile-Children $root -$formHasCmdBar = Test-AnyCmdBar $elements -$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns) -if ($acb) { - $haln = Get-Child $acb 'HorizontalAlign' - $acbAutofill = Get-Child $acb 'Autofill' - $acbDI = $acb.GetAttribute("DisplayImportance") # адаптивная важность форменной панели (атрибут тега) - $acbKids = Decompile-Children $acb - $acbObj = $null - if ($haln -or ($acbAutofill -eq 'false') -or $acbKids -or $acbDI) { - $acbObj = [ordered]@{} - $acbObj['autoCmdBar'] = $acb.GetAttribute("name") - if ($acbDI) { $acbObj['displayImportance'] = $acbDI } - if ($haln) { $acbObj['horizontalAlign'] = $haln } - if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false } - if ($acbKids) { $acbObj['children'] = $acbKids } - } elseif ($formHasCmdBar -and $null -eq $acbAutofill) { - # Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер - $acbObj = [ordered]@{} - $acbObj['autoCmdBar'] = $acb.GetAttribute("name") - $acbObj['autofill'] = $true - } - if ($acbObj) { [void]$elemList.Add($acbObj) } -} -if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } -if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } - -# attributes -# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. -function Test-IsObjectLikeTypeDec([string]$type) { - if ([string]::IsNullOrEmpty($type)) { return $false } - if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } - return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') -} -$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) -if ($attrsNode) { - $attrs = New-Object System.Collections.ArrayList - # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно - # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) - # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. - $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) - $anyMainAttr = $false; $objLikeNodes = @() - foreach ($an in $allAttrNodes) { - if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } - $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) - if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } - } - $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } - foreach ($a in $allAttrNodes) { - $ao = [ordered]@{} - $ao['name'] = $a.GetAttribute("name") - $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } - # valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList - # (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно. - $setNode = $a.SelectSingleNode("lf:Settings", $ns) - if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') { - $vt = Decompile-Type $setNode - $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" - } - # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват). - elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') { - $ao['planner'] = Build-PlannerSettings $setNode - } - # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик). - elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') { - $ao['chart'] = Build-ChartSettings $setNode - } - if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } - elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } - $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } - $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } - # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок - # из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем - # ключ (компилятор воспроизведёт); иначе → явный заголовок. - $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title - $tNode = $a.SelectSingleNode("lf:Title", $ns) - if ($tNode) { - $t = Get-LangTextWS $tNode # восстановление значимого пробела (whitespace-заголовок реквизита) - if ($null -ne $t) { - if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t } - } - } elseif (-not $isMain) { - $ao['title'] = '' - } - # SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved: - # Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал - # тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute). - if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } - elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false } - # Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true; - # иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив. - $saveNode = $a.SelectSingleNode("lf:Save", $ns) - if ($saveNode) { - $nm = "$($ao['name'])" - $flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText }) - if ($flds.Count -eq 1 -and $flds[0] -eq $nm) { - $ao['save'] = $true - } elseif ($flds.Count -gt 0) { - # Снимаем префикс "имя." ТОЛЬКО когда остаток — простое подполе, которое компилятор - # реинъектит (зеркало его условия: ≠имя, без точки, не UUID-ссылка ^N/M). Многоуровневый - # путь "имя.Settings.Filter" и UUID-ссылка "имя.1/0:GUID" компилятор эмитит как есть - # (префикс не вернёт) → храним ПОЛНЫЙ путь, иначе префикс теряется. - $stripped = @($flds | ForEach-Object { - if ($_ -match "^$([regex]::Escape($nm))\.([^.]+)$" -and $matches[1] -notmatch '^\d+/\d+') { $matches[1] } else { $_ } - }) - if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped } - } - } - $fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc } - $afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo } - $colsNode = $a.SelectSingleNode("lf:Columns", $ns) - if ($colsNode) { - $cols = New-Object System.Collections.ArrayList - foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) } - if ($cols.Count -gt 0) { $ao['columns'] = @($cols) } - # AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть) - $addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns)) - if ($addNodes.Count -gt 0) { - $addList = New-Object System.Collections.ArrayList - foreach ($an in $addNodes) { - $acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table") - $acCols = New-Object System.Collections.ArrayList - foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) } - $acObj['columns'] = @($acCols) - [void]$addList.Add($acObj) - } - $ao['additionalColumns'] = @($addList) - } - } - # UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем. - # ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута. - # Дин-список/прочие (нет columns): массив useAlways на атрибуте. - $uaNode = $a.SelectSingleNode("lf:UseAlways", $ns) - if ($uaNode) { - $prefix = "$($ao['name'])." - $shorts = New-Object System.Collections.ArrayList - foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) { - $t = $fn.InnerText.Trim() - # Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем, - # префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно). - if ($t.StartsWith('~')) { - $rest = $t.Substring(1) - if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) } - $t = "~$rest" - } elseif ($t.StartsWith($prefix)) { - $t = $t.Substring($prefix.Length) - } - [void]$shorts.Add($t) - } - if ($ao.Contains('columns')) { - $rest = New-Object System.Collections.ArrayList - foreach ($s in $shorts) { - $col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1 - if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) } - } - if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) } - } elseif ($shorts.Count -gt 0) { - $ao['useAlways'] = @($shorts) - } - } - # Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше) - $setNode = $a.SelectSingleNode("lf:Settings", $ns) - if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') { - $so = [ordered]@{} - # AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть». - $afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') } - $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt } - # GetInvisibleFieldPresentations — дефолт true, платформа эмитит только отклонение (false, корпус 20/20). Факт. значение. - $gifp = Get-Child $setNode 'GetInvisibleFieldPresentations'; if ($null -ne $gifp) { $so['getInvisibleFieldPresentations'] = ($gifp -eq 'true') } - # Ключ набора (query-based список): KeyType (RowNumber/FieldValue/RowKey) + KeyField* (0+). - $kt = Get-Child $setNode 'KeyType'; if ($kt) { $so['keyType'] = $kt } - $kfNodes = @($setNode.SelectNodes("lf:KeyField", $ns) | ForEach-Object { $_.InnerText }) - if ($kfNodes.Count -gt 0) { $so['keyFields'] = @($kfNodes) } - # AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false; - # дефолт true → платформа эмитит отклонение). Захват факт. значения. - $asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') } - $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) - $hasQ = [bool]($qtNode -and $qtNode.InnerText) - if ($hasQ) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } - # ManualQuery: компилятор выводит из наличия query (hasQuery → true). Платформа в редких - # случаях (корпус 16) хранит QueryText при ManualQuery=false → фиксируем отклонение от эвристики. - $mqV = Get-Child $setNode 'ManualQuery' - if ($null -ne $mqV) { $mqActual = ($mqV -eq 'true'); if ($mqActual -ne $hasQ) { $so['manualQuery'] = $mqActual } } - # DynamicDataRead: дефолт true → эмитим только false - if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false } - # Явные поля набора (редко, ~4.5%) — захват только при наличии Field - $fieldNodes = @($setNode.SelectNodes("lf:Field", $ns)) - if ($fieldNodes.Count -gt 0) { - $fields = New-Object System.Collections.ArrayList - foreach ($fn in $fieldNodes) { - $fo = [ordered]@{} - $fld = Get-Child $fn 'field' - $dp = Get-Child $fn 'dataPath' - if ($fld) { $fo['field'] = $fld } - if ((Has-Child $fn 'dataPath') -and $dp -ne $fld) { $fo['dataPath'] = $dp } - # Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs - # DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested. - $fTypeAttr = $fn.GetAttribute("type", $NS_XSI) - if ($fTypeAttr -match 'NestedDataSet$') { $fo['nested'] = $true } - elseif ($fTypeAttr -match 'Folder$') { $fo['folder'] = $true } - $ftn = $fn.SelectSingleNode("dcssch:title", $ns) - if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } - # valueType поля набора (тип значения; вычисляемые/кастомные поля). Грамматика типа. - $fvt = $fn.SelectSingleNode("dcssch:valueType", $ns) - if ($fvt) { $fvtVal = Decompile-Type $fvt; if ($fvtVal) { $fo['valueType'] = $fvtVal } } - # presentationExpression (выражение представления поля) + appearance (формат/оформление поля) - $fpe = Get-Child $fn 'presentationExpression' - if ($null -ne $fpe -and $fpe -ne '') { $fo['presentationExpression'] = $fpe } - $fappNode = $fn.SelectSingleNode("dcssch:appearance", $ns) - if ($fappNode) { $fap = Get-SettingsAppearance $fappNode; if ($fap -and $fap.Count -gt 0) { $fo['appearance'] = $fap } } - $furNode = $fn.SelectSingleNode("dcssch:useRestriction", $ns) - if ($furNode) { $fur = Build-RestrictObj $furNode; if ($fur.Count -gt 0) { $fo['useRestriction'] = $fur } } - $faurNode = $fn.SelectSingleNode("dcssch:attributeUseRestriction", $ns) - if ($faurNode) { $faur = Build-RestrictObj $faurNode; if ($faur.Count -gt 0) { $fo['attributeUseRestriction'] = $faur } } - $fipNode = $fn.SelectSingleNode("dcssch:inputParameters", $ns) - if ($fipNode) { $fip = Build-DLInputParameters $fipNode; if (@($fip).Count -gt 0) { $fo['inputParameters'] = $fip } } - [void]$fields.Add($fo) - } - $so['fields'] = @($fields) - } - # Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*. - $calcNodes = @($setNode.SelectNodes("lf:CalculatedField", $ns)) - if ($calcNodes.Count -gt 0) { - $cfs = New-Object System.Collections.ArrayList - foreach ($cn in $calcNodes) { [void]$cfs.Add((Build-CalcField $cn)) } - $so['calculatedFields'] = @($cfs) - } - # Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings) - $paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns)) - if ($paramNodes.Count -gt 0) { - $dlPars = New-Object System.Collections.ArrayList - foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) } - $so['parameters'] = @($dlPars) - } - # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор - # регенерит каноничный скелет. Захватываем только контейнеры с реальными - # dcsset:item (filter/order/conditionalAppearance) в формат компилятора. - $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns) - if ($lsNode) { - $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns) - if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) { - $flt = @() - foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter") - if ($null -ne $bi) { $flt += $bi } - } - if ($flt.Count -gt 0) { $so['filter'] = @($flt) } - } - $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns) - if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) { - $ord = Build-Order -ordNode $oNode -loc "settings/order" - if (@($ord).Count -gt 0) { $so['order'] = @($ord) } - } - $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) - if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) { - $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" - if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } - } - # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в - # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. - $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) - if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { - $dp = Build-FormDataParameters $dpNode - if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } - } - # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). - # Канон → $null (компилятор регенерит полный скелет, как раньше). - # Группировка строк списка: прямой <dcsset:item> ListSettings (не в filter/order/CA). - $grpItemNode = $lsNode.SelectSingleNode("dcsset:item", $ns) - $grouping = $null - if ($grpItemNode) { $grouping = Build-ListGrouping $grpItemNode } - if ($null -ne $grouping) { $so['grouping'] = $grouping } - $lsShape = Get-ListSettingsShape $lsNode ($null -ne $grouping) - if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } - } - if ($so.Count -gt 0) { $ao['settings'] = $so } - } - [void]$attrs.Add($ao) - } - if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) } -} - -# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>; -# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance) -if ($attrsNode) { - $caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns) - if ($caNode) { - $ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance" - if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) } - } -} - -# parameters -$parsNode = $root.SelectSingleNode("lf:Parameters", $ns) -if ($parsNode) { - $pars = New-Object System.Collections.ArrayList - foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) { - $po = [ordered]@{}; $po['name'] = $p.GetAttribute("name") - $ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty } - if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true } - [void]$pars.Add($po) - } - if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) } -} - -# commands -$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns) -if ($cmdsNode) { - $cmds = New-Object System.Collections.ArrayList - foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) { - $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") - $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } - if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true } - # Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор - # додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале). - $tNode = $c.SelectSingleNode("lf:Title", $ns) - if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } - else { $co['title'] = '' } - $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } - $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } - $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } - $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } - # Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>) - $ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate } - $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } - Set-CommandPicture $co $c - $rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep } - [void]$cmds.Add($co) - } - if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) } -} - -# commandInterface (форменный <CommandInterface> — последний дочерний Form) -$ci = Decompile-CommandInterface -if ($null -ne $ci) { $dsl['commandInterface'] = $ci } - -# --- 6. Output --- -$json = ConvertTo-CompactJson -obj $dsl -if ($OutputPath) { - [System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false))) - Save-QueryFiles - Write-Host "form-decompile: $OutputPath" -} else { - Write-Output $json -} +# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$FormPath, + + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 0. Resolve and validate input --- +if (-not (Test-Path $FormPath)) { + Write-Error "Form not found: $FormPath" + exit 1 +} +$FormPath = (Resolve-Path $FormPath).Path + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load($FormPath) +$root = $xmlDoc.DocumentElement + +# Второй документ с сохранением whitespace — только для восстановления ТОЧНОГО числа пробелов +# в whitespace-only <v8:content> (декорации-распорки без width: число пробелов = ширина). Основной +# парс (PreserveWhitespace=false) не трогаем; элементная структура обоих документов идентична → +# навигация по индекс-пути элементов (Resolve-WS). Загрузка лениво-безопасная. +$script:xmlDocWS = $null +try { $script:xmlDocWS = New-Object System.Xml.XmlDocument; $script:xmlDocWS.PreserveWhitespace = $true; $script:xmlDocWS.Load($FormPath) } catch { $script:xmlDocWS = $null } + +# Ring 2: not a managed Form +if ($root.LocalName -ne 'Form') { + [Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.") + exit 2 +} + +# --- 1. Namespaces --- +$NS_LF = "http://v8.1c.ru/8.3/xcf/logform" +$NS_V8 = "http://v8.1c.ru/8.1/data/core" +$NS_XR = "http://v8.1c.ru/8.3/xcf/readable" +$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" + +$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" +$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" +$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" +$NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +$NS_APP = "http://v8.1c.ru/8.2/managed-application/core" + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("lf", $NS_LF) +$ns.AddNamespace("v8", $NS_V8) +$ns.AddNamespace("xr", $NS_XR) +$ns.AddNamespace("xsi", $NS_XSI) +$ns.AddNamespace("dcsset", $NS_DCSSET) +$ns.AddNamespace("dcssch", $NS_DCSSCH) +$ns.AddNamespace("dcscor", $NS_DCSCOR) +$ns.AddNamespace("v8ui", $NS_V8UI) +$ns.AddNamespace("app", $NS_APP) + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, +# компилятор регенерит тот же скелет → чистый раундтрип. +$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + +# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) --- +$script:outputDir = $null +$script:outputBasename = $null +if ($OutputPath) { + $od = Split-Path -Parent $OutputPath + if (-not $od) { $od = (Get-Location).Path } + $script:outputDir = $od + $script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath) +} +$script:queryFilesAccumulator = @() +$script:queryFileNamesUsed = @{} + +# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file". +function Maybe-ExternalizeQuery { + param([string]$queryText, [string]$listName) + if (-not $queryText) { return $queryText } + if (-not $script:outputDir) { return $queryText } + $lineCount = ([regex]::Matches($queryText, "`n")).Count + 1 + if ($lineCount -lt 3) { return $queryText } + $safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' } + $prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' } + $fileName = "$prefix$safe.sql" + $suffix = 1 + while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" } + $script:queryFileNamesUsed[$fileName] = $true + $script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText } + return "@$fileName" +} +function Save-QueryFiles { + if ($script:queryFilesAccumulator.Count -eq 0) { return } + if (-not $script:outputDir) { return } + $enc = New-Object System.Text.UTF8Encoding($false) + foreach ($qf in $script:queryFilesAccumulator) { + [System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc) + } + [Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)") +} + +# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/ +# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false: +# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness +# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже). +function Test-ListSettingsHasContent { + param($lsNode) + if (-not $lsNode) { return $false } + foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) { + $cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns) + if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true } + } + return $false +} + +# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance → +# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма == +# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы +# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора. +function Get-ListSettingsShape { + param($lsNode, [bool]$hasGrouping = $false) + if (-not $lsNode) { return $null } + $shape = [ordered]@{} + foreach ($child in $lsNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + $tag = $child.LocalName + if ($tag -in @('filter','order','conditionalAppearance')) { + $hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns) + $hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns) + $code = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" + # Контейнер может нести собственный <dcsset:userSettingPresentation> (кастомная подпись + # настройки) — сохраняем форму по xsi:type (Get-PresByType: ru-only LocalString ≠ xs:string). + $uspNode = $child.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($uspNode) { + $usp = Get-PresByType $uspNode + $shape[$tag] = [ordered]@{ meta = $code; presentation = $usp } + } else { $shape[$tag] = $code } + } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } + elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } + elseif ($tag -eq 'itemsUserSettingPresentation') { $shape['itemsUserSettingPresentation'] = Get-PresByType $child } # items-уровневая подпись (форма по xsi:type) + elseif ($tag -eq 'dataParameters') { $shape['dataParameters'] = $true } # значения параметров запроса (контент в settings.dataParameters) + elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } } + else { return $null } # неизвестный top-level → канон-fallback + } + # Полный каноничный скелет → опускаем (компилятор регенерит) + if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and ` + $shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null } + return $shape +} + +# Группировка строк динамического списка: цепочка <dcsset:item StructureItemGroup> (каждый уровень = +# один groupItems-field; вложенность через дочерний <dcsset:item>). Возвращает ПЛОСКИЙ массив уровней +# (string для дефолтного поля; объект {field,groupType?,periodAdditionType?,periodAdditionBegin?,periodAdditionEnd?} +# для нестандартного) ИЛИ $null, если структура не «чистая линейная цепочка одно-польных уровней» +# (ветвление/мультиполе/доп.содержимое) → честный fallback на канон (LOST), а не тихая порча. +function Build-GroupLevel { + param($fn) + $field = Get-Child $fn 'field' + $gt = Get-Child $fn 'groupType' + $pat = Get-Child $fn 'periodAdditionType' + $pabN = $fn.SelectSingleNode("dcsset:periodAdditionBegin", $ns) + $paeN = $fn.SelectSingleNode("dcsset:periodAdditionEnd", $ns) + $pab = $null; $pae = $null + if ($pabN) { $pt = $pabN.GetAttribute("type", $NS_XSI); $pv = $pabN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv } } + if ($paeN) { $pt = $paeN.GetAttribute("type", $NS_XSI); $pv = $paeN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv } } + $isDefault = ((-not $gt) -or $gt -eq 'Items') -and ((-not $pat) -or $pat -eq 'None') -and (-not $pab) -and (-not $pae) + if ($isDefault) { return $field } + $o = [ordered]@{ field = $field } + if ($gt -and $gt -ne 'Items') { $o['groupType'] = $gt } + if ($pat -and $pat -ne 'None') { $o['periodAdditionType'] = $pat } + if ($pab) { $o['periodAdditionBegin'] = $pab } + if ($pae) { $o['periodAdditionEnd'] = $pae } + return $o +} + +function Build-ListGrouping { + param($itemNode) + $levels = New-Object System.Collections.ArrayList + $cur = $itemNode + while ($cur) { + if (($cur.GetAttribute("type", $NS_XSI)) -notmatch 'StructureItemGroup$') { return $null } + $gi = $null; $nested = @() + foreach ($ch in $cur.ChildNodes) { + if ($ch.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + switch ($ch.LocalName) { + 'groupItems' { if ($gi) { return $null }; $gi = $ch } + 'item' { $nested += $ch } + default { return $null } # use/name/filter/order/… — здесь не поддержано + } + } + if (-not $gi) { return $null } + $fieldItems = @($gi.SelectNodes("dcsset:item", $ns)) + if ($fieldItems.Count -ne 1) { return $null } + $fn = $fieldItems[0] + if (($fn.GetAttribute("type", $NS_XSI)) -notmatch 'GroupItemField$') { return $null } + [void]$levels.Add((Build-GroupLevel $fn)) + if ($nested.Count -eq 0) { break } + if ($nested.Count -gt 1) { return $null } # ветвление — не линейно + $cur = $nested[0] + } + if ($levels.Count -eq 0) { return $null } + return ,@($levels) +} + +# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) --- +function Fail-Ring3 { + param([string]$kind, [string]$loc) + [Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)") + [Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.") + exit 3 +} +# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае. +foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" } +# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… — +# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие +# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча. +foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) { + $st = $s.GetAttribute("type", $NS_XSI) + if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') { + Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings" + } + # Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с + # префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые + # дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны. + elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) { + Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings" + } +} + +# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) --- +function Convert-StringToJsonLiteral { + param([string]$s) + if ($null -eq $s) { return 'null' } + $sb = New-Object System.Text.StringBuilder + [void]$sb.Append('"') + foreach ($ch in $s.ToCharArray()) { + $code = [int]$ch + if ($code -eq 0x22) { [void]$sb.Append('\"') } + elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } + elseif ($code -eq 0x08) { [void]$sb.Append('\b') } + elseif ($code -eq 0x09) { [void]$sb.Append('\t') } + elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } + elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } + elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } + elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } + else { [void]$sb.Append($ch) } + } + [void]$sb.Append('"') + return $sb.ToString() +} +function Try-InlineJson { + param($obj) + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $obj.Keys) { + $v = Try-InlineJson $obj[$k] + if ($null -eq $v) { return $null } + $parts += "$(Convert-StringToJsonLiteral "$k"): $v" + } + return '{ ' + ($parts -join ', ') + ' }' + } + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @() + foreach ($it in $items) { + $v = Try-InlineJson $it + if ($null -eq $v) { return $null } + $parts += $v + } + return '[' + ($parts -join ', ') + ']' + } + return $null +} +function ConvertTo-CompactJson { + param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120) + $indent = $indentUnit * $depth + $childIndent = $indentUnit * ($depth + 1) + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) + if ($isContainer) { + $inlineAttempt = Try-InlineJson $obj + if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } + } + if ($obj -is [System.Collections.IDictionary]) { + $keys = @($obj.Keys) + if ($keys.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $keys) { + $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit + $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" + } + return "{`n" + ($parts -join ",`n") + "`n$indent}" + } + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) + return "[`n" + ($parts -join ",`n") + "`n$indent]" + } + return (Convert-StringToJsonLiteral "$obj") +} + +# --- 2. Helpers --- + +# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. +# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы +# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). +$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar') + +# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} +function Get-LangText { + param($node) + if ($null -eq $node) { return $null } + $items = @($node.SelectNodes("v8:item", $ns)) + if ($items.Count -eq 0) { return $null } + $map = [ordered]@{} + foreach ($it in $items) { + $lang = $it.SelectSingleNode("v8:lang", $ns) + $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } + } + if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] } + return $map +} + +# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает +# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой +# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue). +# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель +# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст. +# Точное число пробелов whitespace-only <v8:content> из PreserveWhitespace-документа (основной +# парс стрипает его в ""). Навигация по индекс-пути элементов (структура обоих документов идентична). +function Resolve-WS { + param($contentNode) + if (-not $contentNode -or -not $script:xmlDocWS) { return $null } + $idxs = New-Object System.Collections.ArrayList + $cur = $contentNode + while ($cur.ParentNode -and $cur.ParentNode.NodeType -eq [System.Xml.XmlNodeType]::Element) { + $i = 0; $sib = $cur.PreviousSibling + while ($sib) { if ($sib.NodeType -eq [System.Xml.XmlNodeType]::Element) { $i++ }; $sib = $sib.PreviousSibling } + [void]$idxs.Insert(0, $i) + $cur = $cur.ParentNode + } + $wcur = $script:xmlDocWS.DocumentElement + foreach ($ix in $idxs) { + $els = @(); foreach ($ch in $wcur.ChildNodes) { if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $els += $ch } } + if ($ix -ge $els.Count) { return $null } + $wcur = $els[$ix] + } + return $wcur.InnerText +} + +# Точное восстановление пробела (число): whitespace-only content → реальная строка пробелов из WS-дока. +function Restore-WSContent { + param($contentNode) + $ws = Resolve-WS $contentNode + if ($ws -and $ws.Trim() -eq '') { return $ws } # только если действительно whitespace + return ' ' +} + +function Get-LangTextWS { + param($node) + $t = Get-LangText $node + if ($null -eq $t) { return $null } + if ($t -is [string]) { + $cn = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($t -eq '' -and $cn) { return (Restore-WSContent $cn) } + return $t + } + foreach ($it in @($node.SelectNodes("v8:item", $ns))) { + $lang = $it.SelectSingleNode("v8:lang", $ns) + $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') { + $t[$lang.InnerText] = (Restore-WSContent $content) + } + } + return $t +} + +# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. +# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам. +function Title-FromName { + param([string]$name) + if (-not $name) { return '' } + $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') + $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') + $parts = $s -split ' ' + if ($parts.Count -eq 0) { return $s } + $out = New-Object System.Collections.ArrayList + [void]$out.Add($parts[0]) + for ($i = 1; $i -lt $parts.Count; $i++) { + $p = $parts[$i] + if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) } + else { [void]$out.Add($p.ToLower()) } + } + return ($out -join ' ') +} + +# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). +$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' +function Test-HasRealMarkup { + param($text) + if ($null -eq $text) { return $false } + $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") } + foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } + return $false +} +# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted +# совпал с атрибутом; иначе явный {text, formatted}. +function Get-MLFormattedValue { + param($titleNode) + if (-not $titleNode) { return $null } + # Get-LangTextWS восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и + # мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "". + $text = Get-LangTextWS $titleNode + if ($null -eq $text) { return $null } + $fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true') + if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text } + $o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o +} + +# Прочитать дочерний скаляр (по local-name, без namespace) +function Get-Child { + param($node, [string]$name) + $c = $node.SelectSingleNode("*[local-name()='$name']") + if ($c) { return $c.InnerText } else { return $null } +} +function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") } +function To-Bool { param([string]$v) return ($v -eq 'true') } + +# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). +# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type. +function Convert-TypedValue { + param([string]$raw, [string]$xsiType) + switch -regex ($xsiType) { + 'decimal$' { + if ($raw -match '^-?\d+$') { return [int]$raw } + return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture) + } + 'boolean$' { return ($raw -eq 'true') } + default { return $raw } + } +} + +# ===================================================================== +# Захват настроек компоновщика динамического списка (ListSettings): +# filter / order / conditionalAppearance. Логика портирована из навыка +# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance +# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd +# заменён на запись в stderr + пропуск элемента (form-decompile — draft, +# скрипт не падает на непокрытых конструкциях). +# ===================================================================== + +# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text. +function Get-Text { + param($node, [string]$xpath) + if (-not $node) { return $null } + if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText } + $n = $node.SelectSingleNode($xpath, $ns) + if ($n) { return $n.InnerText } else { return $null } +} + +# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. +# Алиас на уже существующий Get-LangText (тот же контракт). +function Get-MLText { param($node) return (Get-LangText $node) } + +# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. +# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText. +function Get-PresText { + param($node) + if (-not $node) { return $null } + $ml = Get-MLText $node + if ($null -ne $ml) { return $ml } + if ($node.InnerText) { return $node.InnerText } + return $null +} + +# Presentation, сохраняющий ФОРМУ по xsi:type (не схлопывает ru-only LocalStringType в строку): +# <… xsi:type="v8:LocalStringType"> → объект {lang:text} (даже один ru), иначе xs:string → плоская строка. +# Нужно, чтобы компилятор воспроизвёл точный xsi:type (LocalStringType vs xs:string). Пустой LocalStringType → $null. +function Get-PresByType { + param($node) + if (-not $node) { return $null } + $xt = $node.GetAttribute("type", $NS_XSI) + if ($xt -match 'LocalStringType$') { + $d = [ordered]@{} + foreach ($it in $node.SelectNodes("v8:item", $ns)) { + $lang = Get-Text $it "v8:lang"; $content = Get-Text $it "v8:content" + if ($lang) { $d[$lang] = $content } + } + if ($d.Count -gt 0) { return $d } + return $null + } + if ($node.InnerText) { return $node.InnerText } + return $null +} + +# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") +function Get-LocalXsiType { + param($node) + if (-not $node) { return $null } + $t = $node.GetAttribute("type", $NS_XSI) + if ($t -match ':(.+)$') { return $matches[1] } + return $t +} + +# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile). +function Get-FontValue { + param($valNode) + $f = [ordered]@{ '@type' = 'Font' } + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $a = $valNode.Attributes[$attrName] + if ($null -ne $a) { $f[$attrName] = $a.Value } + } + return $f +} + +# Линия (граница) оформления → объект {@type:Line, width, gap, style}. +function Get-LineValue { + param($valNode) + $obj = [ordered]@{ '@type' = 'Line' } + $w = $valNode.GetAttribute("width") + $g = $valNode.GetAttribute("gap") + if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } + if ($g -ne '') { $obj['gap'] = ($g -eq 'true') } + $styleNode = $valNode.SelectSingleNode("v8ui:style", $ns) + if ($styleNode) { $obj['style'] = $styleNode.InnerText } + return $obj +} + +# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text. +function Read-AppearanceValueNode { + param($valNode) + if (-not $valNode) { return $null } + $vt = Get-LocalXsiType $valNode + if ($vt -eq 'LocalStringType') { + # НЕ схлопываем одноязычный в строку: значение параметра оформления различает + # xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы + # одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType. + $map = [ordered]@{} + foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) { + $lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } + } + return $map + } + if ($vt -eq 'Font') { return (Get-FontValue $valNode) } + if ($vt -eq 'Line') { return (Get-LineValue $valNode) } + # dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь} + if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } } + return $valNode.InnerText +} + +# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). +$script:filterOpMap = @{ + 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>='; + 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn'; + 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy'; + 'Contains'='contains'; 'NotContains'='notContains'; + 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith'; + 'Like'='like'; 'NotLike'='notLike'; + 'Filled'='filled'; 'NotFilled'='notFilled' +} + +# Render filter value node → shorthand-acceptable scalar string +function Get-FilterValue { + param($valNode) + if (-not $valNode) { return '_' } + $nil = $valNode.GetAttribute("nil", $NS_XSI) + if ($nil -eq 'true') { return '_' } + $vType = Get-LocalXsiType $valNode + if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText } + if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) } + $txt = $valNode.InnerText + if (-not $txt) { return '_' } + return $txt +} + +# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). +function Get-FilterValueWithType { + param($valNode) + if (-not $valNode) { return @{ value = '_'; type = $null } } + $rawType = $valNode.GetAttribute("type", $NS_XSI) + $nil = $valNode.GetAttribute("nil", $NS_XSI) + if ($nil -eq 'true') { return @{ value = '_'; type = $null } } + $vType = Get-LocalXsiType $valNode + if ($vType -eq 'LocalStringType') { + return @{ value = (Get-MLText $valNode); type = $rawType } + } + # Стандартная дата начала/окончания. Формы (от самой компактной): + # SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт + # даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType; + # SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал. + if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') { + $variantN = $valNode.SelectSingleNode("v8:variant", $ns) + $dateN = $valNode.SelectSingleNode("v8:date", $ns) + $variantStr = if ($variantN) { $variantN.InnerText } else { '' } + if ($dateN) { + if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') { + return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат + } + return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType } + } + return @{ value = $variantStr; type = $rawType } # именованный вариант + } + $txt = $valNode.InnerText + if (-not $txt) { return @{ value = '_'; type = $rawType } } + if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } } + if ($vType -eq 'decimal') { + if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } } + return @{ value = [double]$txt; type = $rawType } + } + return @{ value = $txt; type = $rawType } +} + +# Convert filter item node → shorthand string или object form (рекурсивно для групп). +function Build-FilterItem { + param($itemNode, [string]$loc) + $xtype = Get-LocalXsiType $itemNode + if ($xtype -eq 'FilterItemGroup') { + $gt = Get-Text $itemNode "dcsset:groupType" + $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } } + $items = @() + foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $c -loc "$loc/item") + if ($null -ne $bi) { $items += $bi } + } + $gObj = [ordered]@{ group = $groupName; items = $items } + if ((Get-Text $itemNode "dcsset:use") -eq 'false') { $gObj['use'] = $false } # группа отключена (@off) + $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) + if ($gPresNode) { + # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) + $gPres = Get-PresByType $gPresNode + if ($null -ne $gPres -and $gPres -ne '') { $gObj['presentation'] = $gPres } + } + $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) + if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText } + $gUSID = Get-Text $itemNode "dcsset:userSettingID" + if ($gUSID) { $gObj['userSettingID'] = 'auto' } + $gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($gUSPN) { + $gUSP = Get-PresText $gUSPN + if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP } + } + return $gObj + } + if ($xtype -ne 'FilterItemComparison') { + [Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)") + return $null + } + $leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns) + $field = if ($leftNode) { $leftNode.InnerText } else { $null } + $ct = Get-Text $itemNode "dcsset:comparisonType" + $op = $script:filterOpMap[$ct] + if (-not $op) { $op = $ct } + + $rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns)) + $value = $null + $valueIsArrayFlag = $false + $valueTypeAttr = $null + if ($rightNodes.Count -eq 1) { + $rn = $rightNodes[0] + if ((Get-LocalXsiType $rn) -eq 'ValueListType') { + $value = @() + $valueIsArrayFlag = $true + } else { + $vt = Get-FilterValueWithType $rn + $value = $vt.value + $autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and ` + ("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') + if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) { + $valueTypeAttr = $vt.type + } elseif ($vt.type -eq 'xs:string' -and ($value -is [string]) -and ($value -match '^(-?\d+(\.\d+)?|\d{4}-\d{2}-\d{2}T)')) { + # Значение-строка "1"/"2020-..." с xsi:type="xs:string": компилятор авто-детектит число/дату + # (теряет xs:string). Когда авто-вывод дал бы ДРУГОЙ тип — фиксируем явный valueType. + $valueTypeAttr = 'xs:string' + } + } + } elseif ($rightNodes.Count -gt 1) { + $arr = @() + $rawTypes = @() + foreach ($rn in $rightNodes) { + $arr += (Get-FilterValue $rn) + $rawTypes += $rn.GetAttribute("type", $NS_XSI) + } + $value = $arr + $valueIsArrayFlag = $true + $uniqTypes = @($rawTypes | Sort-Object -Unique) + if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) { + $autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and ` + ($arr.Count -gt 0) -and ` + (@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0) + if (-not $autoDetectsDTV) { + $valueTypeAttr = $uniqTypes[0] + } + } + } + + $use = Get-Text $itemNode "dcsset:use" + $userId = Get-Text $itemNode "dcsset:userSettingID" + $vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) + $viewMode = if ($vmNode) { $vmNode.InnerText } else { $null } + $userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) + $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) + $fiPres = $null + if ($fiPresNode) { + # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) + $fiPres = Get-PresByType $fiPresNode + } + + $flags = @() + if ($use -eq 'false') { $flags += '@off' } + if ($userId) { $flags += '@user' } + if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' } + elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' } + elseif ($viewMode -eq 'Normal') { $flags += '@normal' } + + $noValueOps = @('filled','notFilled') + + # Пустой xs:string right (<dcsset:right xsi:type="xs:string"/>) ≠ отсутствие <right>: + # Get-FilterValueWithType маппит наличие пустого/nil right в value='_' (отсутствие → $null). + # Shorthand `_` неоднозначен (схлопывает оба, компилятор → нет right). Пробельные значения рвут + # shorthand-парсинг (split по пробелам). → форсим объектную форму (value='_' → self-closing; + # пробелы — как есть в <right>). + $valNeedsObj = $false + if ($rightNodes.Count -eq 1 -and -not $valueIsArrayFlag -and $op -notin $noValueOps) { + if ("$value" -eq '_') { + # Truly-empty <right xsi:type="xs:string"/> ИЛИ whitespace-only <right> </right> + # (PreserveWhitespace=false стрипнул пробелы в '' → Get-FilterValueWithType вернул '_'). + # Восстанавливаем реальные пробелы из WS-дока (как у whitespace-заголовков). + $ws = Resolve-WS $rightNodes[0] + if ($ws -and $ws.Length -gt 0 -and $ws.Trim() -eq '') { $value = $ws } + $valNeedsObj = $true + } + elseif ($null -ne $value -and "$value" -match '\s') { $valNeedsObj = $true } + } + + if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres -or $valNeedsObj) { + $obj = [ordered]@{ field = $field; op = $op } + if ($op -notin $noValueOps -and $null -ne $value) { + if ($valueIsArrayFlag) { + $arrAsList = New-Object System.Collections.ArrayList + foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) } + $obj['value'] = $arrAsList + } else { + $obj['value'] = $value + } + } + if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr } + if ($use -eq 'false') { $obj['use'] = $false } + if ($userId) { $obj['userSettingID'] = 'auto' } + if ($fiPres) { $obj['presentation'] = $fiPres } + if ($viewMode) { $obj['viewMode'] = $viewMode } + if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode } + return $obj + } + + $s = $field + if ($op -in $noValueOps) { + $s += " $op" + } else { + $vDisplay = '_' + if ($null -ne $value) { + if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } } + elseif ("$value" -ne '') { $vDisplay = "$value" } + } + $s += " $op $vDisplay" + } + if ($flags) { $s += ' ' + ($flags -join ' ') } + return $s +} + +# Рекурсивный хелпер одного элемента selection (для conditionalAppearance). +function Build-SelectionItem { + param($item, [string]$loc) + $xt = Get-LocalXsiType $item + if (-not $xt) { + $fName = Get-Text $item "dcsset:field" + if ($fName) { return $fName } + $fieldEl = $item.SelectSingleNode("dcsset:field", $ns) + if ($fieldEl) { return 'Auto' } + } + switch ($xt) { + 'SelectedItemAuto' { + $useV = Get-Text $item "dcsset:use" + if ($useV -eq 'false') { + return [ordered]@{ auto = $true; use = $false } + } + return 'Auto' + } + 'SelectedItemField' { + $fName = Get-Text $item "dcsset:field" + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $title = Get-MLText $titleNode + $vmN = $item.SelectSingleNode("dcsset:viewMode", $ns) + $useV = Get-Text $item "dcsset:use" + $useFalse = ($useV -eq 'false') + if ($title -or $vmN -or $useFalse) { + $obj = [ordered]@{ field = $fName } + if ($useFalse) { $obj['use'] = $false } + if ($title) { $obj['title'] = $title } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + return $obj + } + return $fName + } + 'SelectedItemFolder' { + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $folderTitle = Get-MLText $titleNode + $inner = @() + foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-SelectionItem -item $sub -loc "$loc/folder") + if ($null -ne $bi) { $inner += $bi } + } + $entry = [ordered]@{ folder = $folderTitle; items = $inner } + $folderField = Get-Text $item "dcsset:field" + if ($folderField) { $entry['field'] = $folderField } + $plN = $item.SelectSingleNode("dcsset:placement", $ns) + if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') { + $entry['placement'] = $plN.InnerText + } + return $entry + } + default { + [Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)") + return $null + } + } +} + +# Build selection items array (для conditionalAppearance). +function Build-Selection { + param($selNode, [string]$loc) + if (-not $selNode) { return @() } + $out = @() + foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-SelectionItem -item $it -loc $loc) + if ($null -ne $bi) { $out += $bi } + } + return ,$out +} + +# Build order items array. +function Build-Order { + param($ordNode, [string]$loc) + if (-not $ordNode) { return @() } + $out = @() + foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) { + $xt = Get-LocalXsiType $it + switch ($xt) { + 'OrderItemAuto' { $out += 'Auto' } + 'OrderItemField' { + $fn = Get-Text $it "dcsset:field" + $ot = Get-Text $it "dcsset:orderType" + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + $useV = Get-Text $it "dcsset:use" + $useFalse = ($useV -eq 'false') + if ($vmN -or $useFalse) { + $obj = [ordered]@{ field = $fn } + if ($useFalse) { $obj['use'] = $false } + if ($ot -eq 'Desc') { $obj['direction'] = 'desc' } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + $out += $obj + } else { + if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn } + } + } + default { + [Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)") + } + } + } + return ,$out +} + +# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items). +function Get-SettingsAppearance { + param($appNode) + if (-not $appNode) { return $null } + $dict = [ordered]@{} + foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) { + $pName = Get-Text $it "dcscor:parameter" + $val = $it.SelectSingleNode("dcscor:value", $ns) + if (-not $pName -or -not $val) { continue } + $rawVal = Read-AppearanceValueNode $val + $useV = Get-Text $it "dcscor:use" + $nestedItems = [ordered]@{} + foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) { + $subName = Get-Text $sub "dcscor:parameter" + $subVal = $sub.SelectSingleNode("dcscor:value", $ns) + if (-not $subName) { continue } + $subRaw = Read-AppearanceValueNode $subVal + $subUse = Get-Text $sub "dcscor:use" + $subEntry = [ordered]@{ value = $subRaw } + if ($subUse -eq 'false') { $subEntry['use'] = $false } + $nestedItems[$subName] = $subEntry + } + $valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line') + if ($valIsLine) { + if ($useV -eq 'false') { $rawVal['use'] = $false } + if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems } + $dict[$pName] = $rawVal + } elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) { + $wrap = [ordered]@{ value = $rawVal } + if ($useV -eq 'false') { $wrap['use'] = $false } + if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems } + $dict[$pName] = $wrap + } else { + $dict[$pName] = $rawVal + } + } + return $dict +} + +# Build conditionalAppearance array. +function Build-ConditionalAppearance { + param($caNode, [string]$loc) + if (-not $caNode) { return @() } + $out = @() + $i = 0 + foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) { + $entry = [ordered]@{} + $scopeNode = $it.SelectSingleNode("dcsset:scope", $ns) + if ($scopeNode -and $scopeNode.HasChildNodes) { + [Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)") + } + $selNode = $it.SelectSingleNode("dcsset:selection", $ns) + if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { + $entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection" + } + $filterNode = $it.SelectSingleNode("dcsset:filter", $ns) + if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { + $f = @() + foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter") + if ($null -ne $bi) { $f += $bi } + } + $entry['filter'] = $f + } + $appNode = $it.SelectSingleNode("dcsset:appearance", $ns) + $ap = Get-SettingsAppearance $appNode + if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap } + $presNode = $it.SelectSingleNode("dcsset:presentation", $ns) + if ($presNode) { + # Сохраняем форму по xsi:type: LocalStringType (даже ru-only) → объект → компилятор эмитит LocalStringType. + $pres = Get-PresByType $presNode + if ($null -ne $pres -and $pres -ne '') { $entry['presentation'] = $pres } + } + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + if ($vmN) { $entry['viewMode'] = $vmN.InnerText } + $usid = Get-Text $it "dcsset:userSettingID" + if ($usid) { $entry['userSettingID'] = 'auto' } + $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($uspN) { + $usp = Get-PresText $uspN + if ($usp) { $entry['userSettingPresentation'] = $usp } + } + $useV = Get-Text $it "dcsset:use" + if ($useV -eq 'false') { $entry['use'] = $false } + $useInDontUse = @() + foreach ($ch in $it.ChildNodes) { + if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue } + if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') { + $shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1) + $useInDontUse += $shortName + } + } + if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse } + $out += $entry + $i++ + } + return ,$out +} + +# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора). +# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>), +# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows. +function Add-Layout { + param($obj, $node) + # Общие свойства элемента (любой тип): default/drag/skip + if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true } + $soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') } + # EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + $esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') } + $edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') } + $fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm } + # AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault). + # Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики). + $amwNode = Get-Child $node 'AutoMaxWidth' + if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false } + elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true } + elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true } + $mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw } + if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false } + $mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh } + $w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w } + $h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h } + # Stretch: захват фактического значения (true И false — платформа эмитит явное) + $hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') } + $vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') } + $gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha } + $gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva } + $ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha } + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') } + } + $fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha } + $hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha } + # ColumnGroup: динамический заголовок из данных + формат заголовка (ML-текст) + $hdp = Get-Child $node 'HeaderDataPath'; if ($hdp) { $obj['headerDataPath'] = $hdp } + $hfNode = $node.SelectSingleNode("lf:HeaderFormat", $ns); if ($hfNode) { $hf = Get-LangText $hfNode; if ($null -ne $hf) { $obj['headerFormat'] = $hf } } +} + +# TitleLocation у check/radio (зеркало Emit-TitleLocation): +# тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем. +function Add-TitleLocation { + param($obj, $node, [string]$smartDefault) + $tl = Get-Child $node 'TitleLocation' + if ($null -eq $tl) { $obj['titleLocation'] = '' } + elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() } +} + +# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } +# в порядке документа. Имена обработчиков всегда явные (как у событий формы) — +# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим. +function Get-Events { + param($node, [string]$elName) + $ev = $node.SelectSingleNode("lf:Events", $ns) + if (-not $ev) { return $null } + $events = [ordered]@{} + foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) { + $events[$e.GetAttribute("name")] = $e.InnerText + } + if ($events.Count -eq 0) { return $null } + return $events +} + +# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). +# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }. +# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует. +function Decompile-XrFlag { + param($node, [string]$tag) + $el = $node.SelectSingleNode("*[local-name()='$tag']") + if (-not $el) { return $null } + $commonNode = $el.SelectSingleNode("*[local-name()='Common']") + $common = ($commonNode -and $commonNode.InnerText -eq 'true') + $valNodes = @($el.SelectNodes("*[local-name()='Value']")) + if ($valNodes.Count -eq 0) { return $common } + $roles = [ordered]@{} + foreach ($v in $valNodes) { + $rn = $v.GetAttribute("name") + if ($rn -match '^Role\.') { $rn = $rn.Substring(5) } + $roles[$rn] = ($v.InnerText -eq 'true') + } + $o = [ordered]@{} + $o['common'] = $common + $o['roles'] = $roles + return $o +} + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel, +# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки). +# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index + +# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible). +# Голый элемент (только команда, Type=Auto) → строковый shorthand. +function Decompile-CommandInterface { + $ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns) + if (-not $ciNode) { return $null } + $ci = [ordered]@{} + foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) { + $pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns) + if (-not $pn) { continue } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) { + $o = [ordered]@{} + $cmd = Get-Child $it 'Command' + $o['command'] = "$cmd" + $ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty } + $at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at } + $cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg } + $idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx } + $dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') } + $vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis } + # Голый элемент (только command) → строка-shorthand; иначе объект + if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) } + } + if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) } + } + if ($ci.Count -gt 0) { return $ci } + return $null +} + +# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть). +function Decompile-FunctionalOptions { + param($node) + $foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns) + if (-not $foNode) { return $null } + $opts = New-Object System.Collections.ArrayList + foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) { + $t = $it.InnerText.Trim() -replace '^FunctionalOption\.', '' + [void]$opts.Add($t) + } + if ($opts.Count -gt 0) { return ,@($opts) } + return $null +} + +# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions. +function Decompile-AttrColumn { + param($c) + $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") + $cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty } + $ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangTextWS $ctNode; if ($null -ne $t) { $co['title'] = $t } } + $cfc = Get-Child $c 'FillCheck'; if ($cfc) { $co['fillCheck'] = $cfc } # проверка заполнения колонки (как у реквизита) + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles}) + $cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv } + $ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce } + return $co +} + +# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash +# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). +# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false. +# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:"). +# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект +# {src, loadTransparent?, transparentPixel?}. +function Get-PictureRef { + param($node, [string]$picTag) + $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns) + if (-not $ref -and -not $abs) { return $null } + $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } + $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) + $ltTrue = ($lt -and $lt.InnerText -eq 'true') + $tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns) + if (-not $ltTrue -and -not $tpx) { return $src } + $o = [ordered]@{ src = $src } + if ($ltTrue) { $o['loadTransparent'] = $true } + if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } + return $o +} + +# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values): +# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии +# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture +# + отдельный loadTransparent:false. +function Set-CommandPicture { + param($obj, $node) + $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) + if (-not $ref -and -not $abs) { return } + $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } + $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns) + $ltFalse = ($lt -and $lt.InnerText -eq 'false') + $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) + if ($tpx) { + $o = [ordered]@{ src = $src } + if ($ltFalse) { $o['loadTransparent'] = $false } + $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } + $obj['picture'] = $o + } else { + $obj['picture'] = $src + if ($ltFalse) { $obj['loadTransparent'] = $false } + } +} + +# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты. +function Build-FontValue { + param($f) + $present = @() + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + if ($f.HasAttribute($a)) { $present += $a } + } + # Чистый style-ref (ref + kind=StyleItem) → строка-шорткат + if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') { + return $f.GetAttribute('ref') + } + $o = [ordered]@{} + foreach ($k in $present) { + $v = $f.GetAttribute($k) + if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v } + elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') } + else { $o[$k] = $v } + } + return $o +} + +# Граница <Border> → строка-ref (из стиля) или объект {width, style}. +function Build-BorderValue { + param($b) + if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') } + $o = [ordered]@{} + if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } + $st = $b.SelectSingleNode("v8ui:style", $ns) + if ($st) { $o['style'] = $st.InnerText } + return $o +} + +# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка. +function Add-Appearance { + param($obj, $node) + $colorMap = @{ + 'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor' + 'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor' + 'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor' + } + foreach ($tag in $colorMap.Keys) { + $c = $node.SelectSingleNode("lf:$tag", $ns) + if ($c) { $obj[$colorMap[$tag]] = $c.InnerText } + } + foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) { + $f = $node.SelectSingleNode("lf:$($pair[0])", $ns) + if ($f) { $obj[$pair[1]] = (Build-FontValue $f) } + } + $b = $node.SelectSingleNode("lf:Border", $ns) + if ($b) { $obj['border'] = (Build-BorderValue $b) } +} + +function Add-CommonProps { + param($obj, $node, [string]$elName) + Add-Appearance $obj $node + if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true } + if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true } + if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true } + $uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv } + $titleNode = $node.SelectSingleNode("lf:Title", $ns) + if ($titleNode) { + $t = Get-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок) + if ($null -ne $t) { $obj['title'] = $t } + # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) + } + $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) + if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } } + $ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr } + # Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon) + $hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp } + $fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp } + $ev = Get-Events $node $elName + if ($ev) { $obj['events'] = $ev } + # CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture): + # список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает. + $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) + if ($csNode) { + $exc = New-Object System.Collections.ArrayList + foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } + if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } + } +} + +# --- 3. Type decompile (inverse of Emit-Type) --- +function Decompile-Type { + param($typeNode) + if (-not $typeNode) { return $null } + $parts = New-Object System.Collections.ArrayList + foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { + $raw = $vt.InnerText.Trim() + $short = $raw + # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). + switch -regex ($raw) { + '^xs:string$' { + $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) + $al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns) + $fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно + if ($len -and [int]$len.InnerText -gt 0) { + $short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" } + } else { $short = "string" } # Length=0 → всегда Variable (корпус) + break + } + '^xs:decimal$' { + $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) + $f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) + $sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns) + $dd = if ($d) { $d.InnerText } else { '0' } + $ff = if ($f) { $f.InnerText } else { '0' } + if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } + break + } + '^xs:boolean$' { $short = "boolean"; break } + '^xs:dateTime$' { + $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) + $dfv = if ($df) { $df.InnerText } else { 'DateTime' } + switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } + break + } + '^cfg:(.+)$' { $short = $matches[1]; break } + '^(v8|v8ui):' { + # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом + # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. + $rev = @{ + 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' + 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' + 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' + 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' + } + if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } + break + } + default { $short = $raw } + } + [void]$parts.Add($short) + } + # TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида». + # Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. + foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) { + $raw = $ts.InnerText.Trim() + $short = $raw -replace '^(v8ui|v8|cfg):', '' + [void]$parts.Add($short) + } + # TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально + # стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID). + foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) { + [void]$parts.Add("typeid:" + $ti.InnerText.Trim()) + } + if ($parts.Count -eq 0) { return $null } + if ($parts.Count -eq 1) { return $parts[0] } + return ($parts -join ' | ') +} + +# Ограничения использования (useRestriction/attributeUseRestriction) → объект {field?,condition?,group?,order?}. +function Build-RestrictObj { + param($node) + $r = [ordered]@{} + foreach ($k in 'field','condition','group','order') { if ((Get-Child $node $k) -eq 'true') { $r[$k] = $true } } + return $r +} + +# Вычисляемое поле DataSet динамического списка (<CalculatedField>) → объектная модель. +# Зеркало эмиссии form-compile (Emit-CalcFields). Грамматика — как skd calculatedFields, +# + форм-extras presentationExpression/orderExpression (в namespace dcscommon). +function Build-CalcField { + param($cfNode) + $o = [ordered]@{} + $o['dataPath'] = Get-Child $cfNode 'dataPath' + $o['expression'] = Get-Child $cfNode 'expression' + $tn = $cfNode.SelectSingleNode("dcssch:title", $ns) + if ($tn) { $t = Get-LangText $tn; if ($null -ne $t) { $o['title'] = $t } } + $vt = $cfNode.SelectSingleNode("dcssch:valueType", $ns) + if ($vt) { $v = Decompile-Type $vt; if ($v) { $o['valueType'] = $v } } + $ur = $cfNode.SelectSingleNode("dcssch:useRestriction", $ns) + if ($ur) { + $r = [ordered]@{} + foreach ($k in 'field','condition','group','order') { if ((Get-Child $ur $k) -eq 'true') { $r[$k] = $true } } + if ($r.Count -gt 0) { $o['useRestriction'] = $r } + } + $pe = Get-Child $cfNode 'presentationExpression' + if ($null -ne $pe -and $pe -ne '') { $o['presentationExpression'] = $pe } + $oeNodes = @($cfNode.SelectNodes("dcssch:orderExpression", $ns)) + if ($oeNodes.Count -gt 0) { + $oes = New-Object System.Collections.ArrayList + foreach ($oen in $oeNodes) { + $eo = [ordered]@{} + $exprN = $oen.SelectSingleNode("*[local-name()='expression']") + $otN = $oen.SelectSingleNode("*[local-name()='orderType']") + $aoN = $oen.SelectSingleNode("*[local-name()='autoOrder']") + $eo['expression'] = if ($exprN) { $exprN.InnerText } else { '' } + if ($otN -and $otN.InnerText -ne 'Asc') { $eo['orderType'] = $otN.InnerText } + if ($aoN -and $aoN.InnerText -eq 'true') { $eo['autoOrder'] = $true } + [void]$oes.Add($eo) + } + $o['orderExpression'] = @($oes) + } + return $o +} + +# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии +# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и +# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter. +function Build-DLInputParameters { + param($ipNode) + $items = New-Object System.Collections.ArrayList + foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) { + $io = [ordered]@{} + $io['parameter'] = Get-Child $it 'parameter' + $useN = $it.SelectSingleNode("dcscor:use", $ns) + if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false } + $valN = $it.SelectSingleNode("dcscor:value", $ns) + if ($valN) { + $vt = $valN.GetAttribute("type", $NS_XSI) + if ($vt -match 'ChoiceParameters$') { + $cps = New-Object System.Collections.ArrayList + foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { + $cpo = [ordered]@{} + $cpo['name'] = Get-Child $cpi 'choiceParameter' + $vals = New-Object System.Collections.ArrayList + foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) { + [void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI)))) + } + $cpo['values'] = @($vals) + [void]$cps.Add($cpo) + } + $io['choiceParameters'] = @($cps) + } elseif ($vt -match 'ChoiceParameterLinks$') { + $cpls = New-Object System.Collections.ArrayList + foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { + $cpo = [ordered]@{} + $cpo['name'] = Get-Child $cpi 'choiceParameter' + $cpo['value'] = Get-Child $cpi 'value' + $md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md } + [void]$cpls.Add($cpo) + } + $io['choiceParameterLinks'] = @($cpls) + } elseif ($vt -match 'TypeLink$') { + # Связь по типу (dcscor:TypeLink): field + linkItem — структурное значение, + # НЕ склеивать InnerText в строку ("СчетДт"+"1"="СчетДт1"). + $tlo = [ordered]@{} + $tlf = Get-Child $valN 'field'; if ($null -ne $tlf) { $tlo['field'] = $tlf } + $tli = Get-Child $valN 'linkItem'; if ($null -ne $tli) { $tlo['linkItem'] = if ($tli -match '^-?\d+$') { [int]$tli } else { $tli } } + $io['typeLink'] = $tlo + } else { + if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt } + } + } + [void]$items.Add($io) + } + return @($items) +} + +# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного +# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, +# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). +function Build-FormDataParameters { + param($dpNode) + $entries = @() + foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { + $pn = Get-Text $it "dcscor:parameter" + $use = Get-Text $it "dcscor:use" + $valNodes = @($it.SelectNodes("dcscor:value", $ns)) + $valNode = if ($valNodes.Count -ge 1) { $valNodes[0] } else { $null } + $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($valNode -or $usidN -or $vmN -or $uspN) { + $obj = [ordered]@{ parameter = $pn } + if ($valNodes.Count -gt 1) { + # Список значений параметра (valueListAllowed) — захватываем ВСЕ <dcscor:value> массивом + $obj['value'] = @($valNodes | ForEach-Object { $_.InnerText }) + $vt0 = $valNodes[0].GetAttribute("type", $NS_XSI); if ($vt0) { $obj['valueType'] = $vt0 } + } elseif ($valNode) { + if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } + else { + $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText + if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } + elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } + else { $obj['value'] = $vVal } + if ($vType) { $obj['valueType'] = $vType } + } + } + if ($use -eq 'false') { $obj['use'] = $false } + if ($usidN) { $obj['userSettingID'] = 'auto' } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } + $entries += $obj + } else { + $s = $pn; if ($use -eq 'false') { $s += ' @off' } + $entries += $s + } + } + return ,$entries +} + +function Build-DLParameter { + param($pNode) + $name = Get-Child $pNode 'name' + $o = [ordered]@{} + $o['name'] = $name + # title — опускаем, если совпадает с авто-выводом из имени (ru-only) + $titleNode = $pNode.SelectSingleNode("dcssch:title", $ns) + if ($titleNode) { + $t = Get-LangText $titleNode + if ($null -ne $t) { + $auto = Title-FromName -name $name + if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t } + } + } + # valueType + $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) + $typeVal = $null + if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } + # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет <value xsi:nil/> + # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → + # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) + $vNodes = @($pNode.SelectNodes("dcssch:value", $ns)) + if ($vNodes.Count -gt 1) { + # valueListAllowed: список значений — захватываем ВСЕ <dcssch:value> массивом + $o['value'] = @($vNodes | ForEach-Object { Convert-TypedValue -raw $_.InnerText -xsiType ($_.GetAttribute("type", $NS_XSI)) }) + } elseif ($vNodes.Count -eq 1) { + $vNode = $vNodes[0] + if ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true') { + $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) + } elseif ((Get-Child $pNode 'valueListAllowed') -eq 'true') { + $o['value'] = $null + } + } + # useRestriction — опускаем true (дефолт), фиксируем false + if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } + # expression + $expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr } + # availableValues + $avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns)) + if ($avNodes.Count -gt 0) { + $avs = New-Object System.Collections.ArrayList + foreach ($avn in $avNodes) { + $avo = [ordered]@{} + $avv = $avn.SelectSingleNode("dcssch:value", $ns) + if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) } + else { $avo['value'] = $null } + $avp = $avn.SelectSingleNode("dcssch:presentation", $ns) + if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } } + [void]$avs.Add($avo) + } + $o['availableValues'] = @($avs) + } + # valueListAllowed / availableAsField + if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true } + if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false } + # inputParameters + $ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns) + if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } } + # denyIncompleteValues / use + if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true } + $use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use } + + # Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект. + $keys = @($o.Keys) + if ($keys.Count -eq 1) { return $name } + if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) } + return $o +} + +# --- 4. Element dispatch --- +$ELEMENT_KEY = @{ + 'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check'; + 'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField'; + 'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar'; + 'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup'; + 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl'; + 'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc'; + 'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar'; + 'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner'; + 'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart' +} + +# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value. +$GENERIC_SCALARS = @( + @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } + @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } + @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } + @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } + @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } + @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } + @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } + @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } + @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } + @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } + @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } + @{ Tag='Mask'; Key='mask'; Kind='value' } + @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } + @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } + @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } + # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора) + @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } + @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } + @{ Tag='Output'; Key='output'; Kind='value' } + @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } + @{ Tag='PointerType'; Key='pointerType'; Kind='value' } + @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } + @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } + @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } + @{ Tag='Protection'; Key='protection'; Kind='bool' } + @{ Tag='Edit'; Key='edit'; Kind='bool' } + @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } + @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } + @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } + @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } + @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } + @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } + @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } + @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } + @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } + @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } + @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } + @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } + @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } + @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } + # Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление / + # выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка + @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } + @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } + @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } + @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } + @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } + @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } + @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } + @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } + # Сочетание клавиш — общее свойство элемента (команда — отдельный путь) + @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } + # Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно) + @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } + @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } + @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } + @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } + @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } + @{ Tag='Shape'; Key='shape'; Kind='value' } + @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } + @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } + # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр + @{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' } + # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) + @{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' } + @{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' } + @{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' } + @{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' } + # Режим авто-показа кнопок открытия/очистки (input, enum) + @{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' } + @{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' } + # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент) + @{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' } + @{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' } + @{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' } + @{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' } + # Хвост листовых скаляров (по 1): автокоррекция / уникальность команды / пустое множ.значение / гориз.сжатие + @{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' } + @{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' } + @{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' } + @{ Tag='AllowInputEmptyMultipleValues'; Key='allowInputEmptyMultipleValues'; Kind='bool' } + @{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' } +) + +# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает. +function Add-GenericScalars { + param($obj, $node) + foreach ($s in $GENERIC_SCALARS) { + if ($obj.Contains($s.Key)) { continue } + $v = Get-Child $node $s.Tag + if ($null -eq $v) { continue } + if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v } + } +} + +function Decompile-Children { + param($parentNode, [string]$childContainer = 'ChildItems') + $container = $parentNode.SelectSingleNode("lf:$childContainer", $ns) + if (-not $container) { return $null } + $list = New-Object System.Collections.ArrayList + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + if ($COMPANION_TAGS -contains $child.LocalName) { continue } + $el = Decompile-Element $child + if ($el) { [void]$list.Add($el) } + } + if ($list.Count -eq 0) { return $null } + return ,@($list) +} + +# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом +# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing). +# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ: +# autofill=false (нет детей) → совпадает с дефолтом → молчим; +# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта). +function Decompile-CompanionPanel { + param($node, [string]$tag, [bool]$isDynListTable = $false) + $p = $node.SelectSingleNode("lf:$tag", $ns) + if (-not $p) { return $null } + $autofillRaw = Get-Child $p 'Autofill' + $halign = Get-Child $p 'HorizontalAlign' + $kids = Decompile-Children $p + $hasKids = $kids -and @($kids).Count -gt 0 + if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) { + if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим + return [ordered]@{ autofill = $true } # голая панель → отклонение + } + if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null } + $o = [ordered]@{} + if ($halign) { $o['horizontalAlign'] = $halign } + if ($autofillRaw -eq 'false') { $o['autofill'] = $false } + elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true } + if ($hasKids) { $o['children'] = $kids } + return $o +} + +# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null. +# У RadioButtonField и InputField. +function Decompile-ChoiceList { + param($node) + $cl = $node.SelectSingleNode("lf:ChoiceList", $ns) + if (-not $cl) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) { + $valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns) + $presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns) + $ci = [ordered]@{} + if ($valNode) { + if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { + # nil-значение элемента choiceList — компилятор эмитит <Value xsi:nil="true"/> + # (иначе Convert-TypedValue вернул бы "" → typed-empty xs:string). + $ci['valueType'] = 'nil' + } else { + $xsiType = $valNode.GetAttribute("type", $NS_XSI) + $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType + # Системное перечисление (ent:*) / иной не-примитивный тип → сохраняем valueType + # (Normalize-ChoiceValue вывела бы xs:string). DesignTimeRef обычно авто-детектится + # компилятором по named-ref (Enum.X.Y), НО raw-ссылка по GUID (GUID.GUID) — нет → сохраняем. + if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and ` + ($xsiType -ne 'xr:DesignTimeRef' -or "$($valNode.InnerText)" -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-')) { + $ci['valueType'] = $xsiType + } + } + } + # Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер, + # подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения). + if ($presNode) { + $p = Get-LangTextWS $presNode # восстановление значимого пробела (whitespace-presentation) + if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' } + } + [void]$items.Add($ci) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо +# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value). +function Convert-ChoiceParamValue { + param($valNode) + $vt = $valNode.GetAttribute("type", $NS_XSI) + if ($vt -match 'FixedArray$') { + $arr = New-Object System.Collections.ArrayList + foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) { + $inner = $it.SelectSingleNode("lf:Value", $ns) + if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) } + } + return ,@($arr) + } + return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt +} + +# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}]. +function Decompile-ChoiceParameters { + param($node) + $cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns) + if (-not $cpn) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($cpn.SelectNodes("app:item", $ns))) { + $o = [ordered]@{} + $o['name'] = $it.GetAttribute("name") + $valNode = $it.SelectSingleNode("app:value/lf:Value", $ns) + if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> → +# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит). +function Decompile-ChoiceParameterLinks { + param($node) + $cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns) + if (-not $cln) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) { + $o = [ordered]@{} + $o['name'] = Get-Text $lk "xr:Name" + $o['dataPath'] = Get-Text $lk "xr:DataPath" + $vc = Get-Text $lk "xr:ValueChange" + if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}. +function Decompile-TypeLink { + param($node) + $tn = $node.SelectSingleNode("lf:TypeLink", $ns) + if (-not $tn) { return $null } + $o = [ordered]@{} + $o['dataPath'] = Get-Text $tn "xr:DataPath" + $li = Get-Text $tn "xr:LinkItem" + if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li } + return $o +} + +# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}). +function Add-FormatProps { + param($obj, $node) + $fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } } + $efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } } +} + +# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation. +# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно). +function Add-AdditionCore { + param($obj, $node, [string]$elName) + $src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText } + Add-CommonProps $obj $node $elName + $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } +} + +# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту +# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем. +function Decompile-TableAdditions { + param($tableNode, [string]$tableName) + $tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' } + $map = [ordered]@{} + foreach ($child in $tableNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + if (-not $tagToKey.ContainsKey($child.LocalName)) { continue } + $key = $tagToKey[$child.LocalName] + $nm = $child.GetAttribute("name") + $o = [ordered]@{}; $o[$key] = $nm + Add-AdditionCore $o $child $nm + Add-Layout $o $child + $o.Remove($key) # имя авто + if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт + if ($o.Count -gt 0) { $map[$key] = $o } + } + if ($map.Count -gt 0) { return $map } + return $null +} + +# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode). +# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост. +function Decompile-SimpleField { + param($obj, $node, [string]$name, [string]$key) + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } +} + +# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField). +function Add-GaugeScalars { + param($obj, $node, $tags) + foreach ($p in $tags) { + $v = Get-Child $node $p + if ($null -eq $v) { continue } + $key = $p.Substring(0,1).ToLower() + $p.Substring(1) + if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v } + } +} + +function Decompile-Element { + param($node) + $tag = $node.LocalName + if (-not $ELEMENT_KEY.ContainsKey($tag)) { + Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag" + } + $key = $ELEMENT_KEY[$tag] + $name = $node.GetAttribute("name") + $obj = [ordered]@{} + + switch ($tag) { + 'UsualGroup' { + # group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем). + # group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление + # не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical' + # — иначе компилятор додумает <Group>Vertical</Group> там, где его нет. + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } + if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } + $behavior = Get-Child $node 'Behavior' + if ($behavior) { + $bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' } + if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior } + } + $obj['name'] = $name + Add-CommonProps $obj $node $name + $rep = Get-Child $node 'Representation' + if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } } + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } # использование текущей строки группы + $crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } } + if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false } + if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true } + # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы) + Add-FormatProps $obj $node + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'ColumnGroup' { + # columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление + # не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет. + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' } + if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } + $obj['name'] = $name + Add-CommonProps $obj $node $name + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + $sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'InputField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + # MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true») + $mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') } + # PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true») + $pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') } + $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangTextWS $ih; if ($t) { $obj['inputHint'] = $t } } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } + } + # InputField-специфичные bool-скаляры (захват «как есть») + foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } + } + # InputField-специфичные value-скаляры (захват «как есть») + foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v } + } + # MinValue/MaxValue — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип: + # xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string). + foreach ($p in @('MinValue','MaxValue')) { + $mn = $node.SelectSingleNode("lf:$p", $ns) + if ($mn) { + $xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText + $key = $p.Substring(0,1).ToLower() + $p.Substring(1) + if ($xt -match 'decimal|int') { + if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt } + } else { $obj[$key] = $txt } + } + } + # Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор. + # availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type. + $tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) } + $atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } } + $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } + $cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp } + if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } + Add-FormatProps $obj $node + # Параметры выбора / Связи параметров выбора / Связь по типу + $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } + $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } + $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } + } + 'CheckBoxField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + # CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение + $cbt = Get-Child $node 'CheckBoxType' + if ($null -eq $cbt) { $obj['checkBoxType'] = '' } + elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) } + Add-TitleLocation $obj $node 'Right' + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) + $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + Add-FormatProps $obj $node + } + 'RadioButtonField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + Add-TitleLocation $obj $node 'None' + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } + $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } + } + 'LabelDecoration' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + # title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip) + $tiNode = $node.SelectSingleNode("lf:Title", $ns) + if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } + } + 'LabelField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + # LabelField: тег <Hiperlink> (опечатка платформы), не <Hyperlink> + if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true } + # PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения + $pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input + $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + Add-FormatProps $obj $node + } + 'PictureDecoration' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + # title декорации — единая ML-text форма с formatted (атрибут <Title formatted> у PictureDecoration) + $tiNode = $node.SelectSingleNode("lf:Title", $ns) + if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } + $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } + $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) + if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs: + $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } + # Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности + $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) + if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + } + 'PictureField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + $vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp } + $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) + $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + } + 'CalendarField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm } + $scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') } + $wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim } + $him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him } + $smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') } + } + 'Table' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') } + $cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') } + if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true } + # enableDrag — теперь общий (Add-Layout, фактическое значение) + if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null } + if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } + if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } + # Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout) + $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr } + # Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table) + $hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh } + $fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh } + # Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through + $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } + # Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through + $rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr } + # CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем + # (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват). + $cbl = Get-Child $node 'CommandBarLocation' + if (Has-Child $node 'UpdateOnDataChange') { + if ($null -eq $cbl) { $obj['commandBarLocation'] = '' } + elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl } + } elseif ($cbl) { $obj['commandBarLocation'] = $cbl } + $ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl } + $vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl } + $scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl } + # --- Общие свойства таблицы (любой тип таблицы, не только динсписок) --- + if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true } + $selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm } + $rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm } + if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false } + if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false } + if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true } + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill). + $taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') } + if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true } + $soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin } + $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } + $itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv } + # RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel) + $rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp } + $rpdp = Get-Child $node 'RowPictureDataPath' + # --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) --- + if (Has-Child $node 'UpdateOnDataChange') { + if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true } + $arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp } + $cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi } + if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true } + if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false } + if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true } + $uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc } + if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false } + # RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture + if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' } + elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp } + $usg = Get-Child $node 'UserSettingsGroup' + if ($usg) { if ($usg -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: UserSettingsGroup '$usg' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['userSettingsGroup'] = $usg } } + } elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp } + $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) + if ($csNode) { + $exc = New-Object System.Collections.ArrayList + foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } + if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } + } + $cols = Decompile-Children $node + if ($cols) { $obj['columns'] = $cols } + # Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions + $addMap = Decompile-TableAdditions $node $name + if ($addMap) { $obj['additions'] = $addMap } + } + 'Pages' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr } + $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Page' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } + if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } + # Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false) + $pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp } + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы) + Add-FormatProps $obj $node + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Button' { + $obj[$key] = $name + $cmd = Get-Child $node 'CommandName' + if ($cmd) { + if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] } + elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] } + elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" } + else { $obj['commandName'] = $cmd } + } + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + # Parameter команды: xr:MDObjectRef (объект метаданных, строка) или v8:TypeDescription (тип → {type}) + $btnParam = $node.SelectSingleNode("lf:Parameter", $ns) + if ($btnParam) { + $pxt = $btnParam.GetAttribute("type", $NS_XSI) + if ($pxt -match 'TypeDescription$') { $pt = Decompile-Type $btnParam; if ($pt) { $obj['parameter'] = [ordered]@{ type = $pt } } } + elseif ($btnParam.InnerText) { $obj['parameter'] = $btnParam.InnerText } + } + Add-CommonProps $obj $node $name + $type = Get-Child $node 'Type' + if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } } + if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true } + if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true } + Set-CommandPicture $obj $node + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic } + } + 'ButtonGroup' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $cs = Get-Child $node 'CommandSource' + if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'CommandBar' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $cs = Get-Child $node 'CommandSource' + if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } + $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } + if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Popup' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + Set-CommandPicture $obj $node + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + # Источник команд попапа (Form / FormCommandPanelGlobalCommands / Item.X) — как у ButtonGroup/CommandBar + $cs = Get-Child $node 'CommandSource' + if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'TextDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') } + 'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') } + 'ChartField' { Decompile-SimpleField $obj $node $name $key } + 'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key } + 'PlannerField' { Decompile-SimpleField $obj $node $name $key } + 'PeriodField' { Decompile-SimpleField $obj $node $name $key } + 'DendrogramField' { Decompile-SimpleField $obj $node $name $key } + 'GanttChartField' { + Decompile-SimpleField $obj $node $name $key + # Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element. + # Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере). + $tblNode = $node.SelectSingleNode("lf:Table", $ns) + if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode } + } + } + # DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть». + $di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di } + # title: "" — подавление авто-вывода: для типов, где компилятор вывел бы + # заголовок из имени, а в оригинале <Title> отсутствует. + if (-not $obj.Contains('title')) { + $autoTitle = $false + if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true } + elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) } + elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') } + if ($autoTitle) { $obj['title'] = '' } + } + Add-Layout $obj $node + Add-GenericScalars $obj $node + # extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) → + # строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }. + $etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns) + if ($etNode) { + $etTitle = $etNode.SelectSingleNode("lf:Title", $ns) + $textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null } + $etObj = [ordered]@{} + Add-Layout $etObj $etNode + Add-GenericScalars $etObj $etNode + Add-Appearance $etObj $etNode + # ToolTip самого компаньона (подсказка расширенной подсказки) — реальный текст (ML), не пустой Title + $etTT = $etNode.SelectSingleNode("lf:ToolTip", $ns) + if ($etTT) { $ttVal = Get-LangText $etTT; if ($null -ne $ttVal) { $etObj['tooltip'] = $ttVal } } + if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true } + if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true } + if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true } + # События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента + $etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv } + if ($etObj.Count -gt 0) { + if ($null -ne $textVal) { + if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } } + else { $etObj['text'] = $textVal } + # formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте + if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true } + } + $obj['extendedTooltip'] = $etObj + } elseif ($null -ne $textVal) { + $obj['extendedTooltip'] = $textVal + } + } + # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент) + $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange') + $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable + if ($null -ne $cb) { $obj['commandBar'] = $cb } + $cm = Decompile-CompanionPanel $node 'ContextMenu' + if ($null -ne $cm) { $obj['contextMenu'] = $cm } + return $obj +} + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. +# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings. +function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') } +function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v } +# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null. +function Get-PlannerValue { + param($node) + if (-not $node) { return $null } + if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null } + if ($node.InnerText) { return $node.InnerText } else { return $null } +} +function Build-PlannerFont { + param($node) + if (-not $node) { return $null } + $o = [ordered]@{} + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av } + } + if ($o.Count -eq 0) { return $null } + return $o +} +function Build-PlannerBorder { + param($node) + if (-not $node) { return $null } + $o = [ordered]@{} + $w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } + $st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText } + return $o +} +function Build-PlannerItem { + param($itn) + $o = [ordered]@{} + $valNode = $itn.SelectSingleNode("*[local-name()='value']") + if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText } + $o['text'] = (Get-Child $itn 'text') + $tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt } + $o['begin'] = (Get-Child $itn 'begin') + $o['end'] = (Get-Child $itn 'end') + $o['borderColor'] = (Get-Child $itn 'borderColor') + $o['backColor'] = (Get-Child $itn 'backColor') + $o['textColor'] = (Get-Child $itn 'textColor') + $fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $o['replacementDate'] = (Get-Child $itn 'replacementDate') + $o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted')) + $o['id'] = (Get-Child $itn 'id') + $o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted')) + $brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd } + $o['editMode'] = (Get-Child $itn 'editMode') + return $o +} +function Build-PlannerDimElement { + param($eln) + $o = [ordered]@{} + $v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } + $o['text'] = (Get-Child $eln 'text') + $o['borderColor'] = (Get-Child $eln 'borderColor') + $o['backColor'] = (Get-Child $eln 'backColor') + $o['textColor'] = (Get-Child $eln 'textColor') + $fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $subs = New-Object System.Collections.ArrayList + foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) } + if ($subs.Count -gt 0) { $o['elements'] = @($subs) } + $sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') } + $o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted')) + return $o +} +function Build-PlannerDimension { + param($dn) + $o = [ordered]@{} + $v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } + $o['text'] = (Get-Child $dn 'text') + $o['borderColor'] = (Get-Child $dn 'borderColor') + $o['backColor'] = (Get-Child $dn 'backColor') + $o['textColor'] = (Get-Child $dn 'textColor') + $fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $els = New-Object System.Collections.ArrayList + foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) } + if ($els.Count -gt 0) { $o['elements'] = @($els) } + $o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted')) + return $o +} +function Build-PlannerLevel { + param($lvn) + $o = [ordered]@{} + $o['measure'] = (Get-Child $lvn 'measure') + $o['interval'] = (PLD-Int (Get-Child $lvn 'interval')) + $o['show'] = (PLD-Bool (Get-Child $lvn 'show')) + $lineNode = $lvn.SelectSingleNode("*[local-name()='line']") + if ($lineNode) { + $ln = [ordered]@{} + $w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w } + $g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') } + $st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText } + $o['line'] = $ln + } + $o['scaleColor'] = (Get-Child $lvn 'scaleColor') + $o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule') + $fmtNode = $lvn.SelectSingleNode("*[local-name()='format']") + if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } } + $labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']") + if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } } + $o['backColor'] = (Get-Child $lvn 'backColor') + $o['textColor'] = (Get-Child $lvn 'textColor') + $o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels')) + return $o +} +function Build-PlannerTimeScale { + param($tsn) + $o = [ordered]@{} + $o['placement'] = (Get-Child $tsn 'placement') + $levels = New-Object System.Collections.ArrayList + foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) } + $o['levels'] = @($levels) + $o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent')) + $o['backColor'] = (Get-Child $tsn 'backColor') + $o['textColor'] = (Get-Child $tsn 'textColor') + $o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel')) + return $o +} +function Build-PlannerSettings { + param($setNode) + $pl = [ordered]@{} + $itemNodes = @($setNode.SelectNodes("*[local-name()='item']")) + if ($itemNodes.Count -gt 0) { + $items = New-Object System.Collections.ArrayList + foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) } + $pl['items'] = @($items) + } + $dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']")) + if ($dimNodes.Count -gt 0) { + $dims = New-Object System.Collections.ArrayList + foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) } + $pl['dimensions'] = @($dims) + } + $pl['borderColor'] = (Get-Child $setNode 'borderColor') + $pl['backColor'] = (Get-Child $setNode 'backColor') + $pl['textColor'] = (Get-Child $setNode 'textColor') + $pl['lineColor'] = (Get-Child $setNode 'lineColor') + $fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt } + $pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod') + $pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod') + $pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale')) + $pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders')) + $pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders')) + $wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']") + if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } } + $pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit') + $pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition')) + $pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent')) + $pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent')) + $tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']") + if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) } + $perNode = $setNode.SelectSingleNode("*[local-name()='period']") + if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } } + $pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate')) + $pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation') + $pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient') + $pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth')) + $pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight')) + $pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth')) + $pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight')) + $pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader') + $pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader') + $brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd } + $pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType') + return $pl +} + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок: +# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии) +# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML), +# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип). +$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } +$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 } +$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } +function Conv-ChartScalar { + param([string]$v) + if ($v -eq 'true') { return $true } + if ($v -eq 'false') { return $false } + return $v +} +function Build-ChartNode { + param($n, [string]$name) + # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) + if ($CHART_ML_FIELDS.Contains($name)) { + $ml = Get-LangText $n + if ($null -eq $ml) { return '' } else { return $ml } + } + $kids = @($n.SelectNodes("*")) + if ($kids.Count -eq 0) { + # лист: attrs-only (шрифт/gaugeQualityBands) или текст + $attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' }) + if ($attrs.Count -gt 0) { + $o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o + } + return (Conv-ChartScalar $n.InnerText) + } + # line/border: дочерний v8ui:style (+ width[/gap]) + $styleChild = $n.SelectSingleNode("*[local-name()='style']") + if ($styleChild) { + $o = [ordered]@{} + $w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } + $g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') } + $o['style'] = $styleChild.InnerText + return $o + } + # вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени + $o = [ordered]@{} + foreach ($c in $kids) { + $ln = $c.LocalName + $val = Build-ChartNode $c $ln + if ($CHART_SERIES_FIELDS.Contains($ln)) { + if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList } + [void]$o[$ln].Add($val) + } elseif ($o.Contains($ln)) { + if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp } + [void]$o[$ln].Add($val) + } else { + $o[$ln] = $val + } + } + # нормализуем ArrayList → @() для сериализации + foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } } + return $o +} +function Build-ChartSettings { + param($setNode) + return (Build-ChartNode $setNode '') +} + +# --- 5. Form-level assembly --- +$dsl = [ordered]@{} + +$titleNode = $root.SelectSingleNode("lf:Title", $ns) +if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } + +# properties (прямые скаляры под <Form>, PascalCase → camelCase) +$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing','CustomSettingsFolder','SettingsStorage','Enabled','Scale') +$props = [ordered]@{} +foreach ($pn in $KNOWN_FORM_PROPS) { + $v = Get-Child $root $pn + if ($null -ne $v) { + $camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1) + if ($v -eq 'true') { $props[$camel] = $true } + elseif ($v -eq 'false') { $props[$camel] = $false } + elseif ($v -match '^\d+$') { $props[$camel] = [int]$v } + else { $props[$camel] = $v } + } +} +# Ссылка на члена формы по id ("N:uuid") в groupList/customSettingsFolder НЕ воспроизводима: наш +# компилятор переназначает id, и ссылка указала бы не туда (либо вовсе dangling — платформа сама их +# часто не разрешает). Резолв в имя ненадёжен (N не всегда соответствует named-элементу). Игнорируем +# с предупреждением (имя задаётся вручную через form-edit). Имя-форма захватывается как есть. +foreach ($refKey in 'groupList','customSettingsFolder') { + if ($props.Contains($refKey) -and "$($props[$refKey])" -match '^\d+:[0-9a-fA-F]{8}-') { + [Console]::Error.WriteLine("form-decompile: $refKey = '$($props[$refKey])' — ссылка на члена формы по id, не воспроизводима (id переназначаются), опущена. Задайте по имени через form-edit.") + $props.Remove($refKey) + } +} +# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим: +# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title); +# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить). +if ($dsl.Contains('title')) { + if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' } + elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } +} +if ($props.Count -gt 0) { $dsl['properties'] = $props } + +# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок +$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns) +if ($mdcb) { + $names = New-Object System.Collections.ArrayList + foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) { + $v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) } + } + if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) } +} + +# excludedCommands (form-level <CommandSet>) +$csForm = $root.SelectSingleNode("lf:CommandSet", $ns) +if ($csForm) { + $excForm = New-Object System.Collections.ArrayList + foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) } + if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) } +} + +# events (form-level) +$evForm = Get-Events $root $null +if ($evForm) { + # form-level: компилятор хранит как {Event: handler} напрямую + $evMap = [ordered]@{} + $evNode = $root.SelectSingleNode("lf:Events", $ns) + foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText } + if ($evMap.Count -gt 0) { $dsl['events'] = $evMap } +} + +# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо +# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить +# отклонение (голый корень при наличии cmdBar). +function Test-AnyCmdBar { + param($list) + if (-not $list) { return $false } + foreach ($e in $list) { + if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true } + if ($e -is [System.Collections.IDictionary]) { + if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true } + if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true } + } + } + return $false +} + +# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение) +$elemList = New-Object System.Collections.ArrayList +$elements = Decompile-Children $root +$formHasCmdBar = Test-AnyCmdBar $elements +$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns) +if ($acb) { + $haln = Get-Child $acb 'HorizontalAlign' + $acbAutofill = Get-Child $acb 'Autofill' + $acbDI = $acb.GetAttribute("DisplayImportance") # адаптивная важность форменной панели (атрибут тега) + $acbKids = Decompile-Children $acb + $acbObj = $null + if ($haln -or ($acbAutofill -eq 'false') -or $acbKids -or $acbDI) { + $acbObj = [ordered]@{} + $acbObj['autoCmdBar'] = $acb.GetAttribute("name") + if ($acbDI) { $acbObj['displayImportance'] = $acbDI } + if ($haln) { $acbObj['horizontalAlign'] = $haln } + if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false } + if ($acbKids) { $acbObj['children'] = $acbKids } + } elseif ($formHasCmdBar -and $null -eq $acbAutofill) { + # Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер + $acbObj = [ordered]@{} + $acbObj['autoCmdBar'] = $acb.GetAttribute("name") + $acbObj['autofill'] = $true + } + if ($acbObj) { [void]$elemList.Add($acbObj) } +} +if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } +if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } + +# attributes +# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. +function Test-IsObjectLikeTypeDec([string]$type) { + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } + return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') +} +$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) +if ($attrsNode) { + $attrs = New-Object System.Collections.ArrayList + # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно + # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) + # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. + $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) + $anyMainAttr = $false; $objLikeNodes = @() + foreach ($an in $allAttrNodes) { + if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } + $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) + if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } + } + $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } + foreach ($a in $allAttrNodes) { + $ao = [ordered]@{} + $ao['name'] = $a.GetAttribute("name") + $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } + # valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList + # (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно. + $setNode = $a.SelectSingleNode("lf:Settings", $ns) + if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') { + $vt = Decompile-Type $setNode + $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" + } + # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват). + elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') { + $ao['planner'] = Build-PlannerSettings $setNode + } + # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик). + elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') { + $ao['chart'] = Build-ChartSettings $setNode + } + if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } + elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } + $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } + $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } + # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок + # из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем + # ключ (компилятор воспроизведёт); иначе → явный заголовок. + $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title + $tNode = $a.SelectSingleNode("lf:Title", $ns) + if ($tNode) { + $t = Get-LangTextWS $tNode # восстановление значимого пробела (whitespace-заголовок реквизита) + if ($null -ne $t) { + if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t } + } + } elseif (-not $isMain) { + $ao['title'] = '' + } + # SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved: + # Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал + # тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute). + if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } + elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false } + # Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true; + # иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив. + $saveNode = $a.SelectSingleNode("lf:Save", $ns) + if ($saveNode) { + $nm = "$($ao['name'])" + $flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText }) + if ($flds.Count -eq 1 -and $flds[0] -eq $nm) { + $ao['save'] = $true + } elseif ($flds.Count -gt 0) { + # Снимаем префикс "имя." ТОЛЬКО когда остаток — простое подполе, которое компилятор + # реинъектит (зеркало его условия: ≠имя, без точки, не UUID-ссылка ^N/M). Многоуровневый + # путь "имя.Settings.Filter" и UUID-ссылка "имя.1/0:GUID" компилятор эмитит как есть + # (префикс не вернёт) → храним ПОЛНЫЙ путь, иначе префикс теряется. + $stripped = @($flds | ForEach-Object { + if ($_ -match "^$([regex]::Escape($nm))\.([^.]+)$" -and $matches[1] -notmatch '^\d+/\d+') { $matches[1] } else { $_ } + }) + if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped } + } + } + $fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc } + $afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo } + $colsNode = $a.SelectSingleNode("lf:Columns", $ns) + if ($colsNode) { + $cols = New-Object System.Collections.ArrayList + foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) } + if ($cols.Count -gt 0) { $ao['columns'] = @($cols) } + # AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть) + $addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns)) + if ($addNodes.Count -gt 0) { + $addList = New-Object System.Collections.ArrayList + foreach ($an in $addNodes) { + $acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table") + $acCols = New-Object System.Collections.ArrayList + foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) } + $acObj['columns'] = @($acCols) + [void]$addList.Add($acObj) + } + $ao['additionalColumns'] = @($addList) + } + } + # UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем. + # ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута. + # Дин-список/прочие (нет columns): массив useAlways на атрибуте. + $uaNode = $a.SelectSingleNode("lf:UseAlways", $ns) + if ($uaNode) { + $prefix = "$($ao['name'])." + $shorts = New-Object System.Collections.ArrayList + foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) { + $t = $fn.InnerText.Trim() + # Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем, + # префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно). + if ($t.StartsWith('~')) { + $rest = $t.Substring(1) + if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) } + $t = "~$rest" + } elseif ($t.StartsWith($prefix)) { + $t = $t.Substring($prefix.Length) + } + [void]$shorts.Add($t) + } + if ($ao.Contains('columns')) { + $rest = New-Object System.Collections.ArrayList + foreach ($s in $shorts) { + $col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1 + if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) } + } + if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) } + } elseif ($shorts.Count -gt 0) { + $ao['useAlways'] = @($shorts) + } + } + # Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше) + $setNode = $a.SelectSingleNode("lf:Settings", $ns) + if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') { + $so = [ordered]@{} + # AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть». + $afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') } + $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt } + # GetInvisibleFieldPresentations — дефолт true, платформа эмитит только отклонение (false, корпус 20/20). Факт. значение. + $gifp = Get-Child $setNode 'GetInvisibleFieldPresentations'; if ($null -ne $gifp) { $so['getInvisibleFieldPresentations'] = ($gifp -eq 'true') } + # Ключ набора (query-based список): KeyType (RowNumber/FieldValue/RowKey) + KeyField* (0+). + $kt = Get-Child $setNode 'KeyType'; if ($kt) { $so['keyType'] = $kt } + $kfNodes = @($setNode.SelectNodes("lf:KeyField", $ns) | ForEach-Object { $_.InnerText }) + if ($kfNodes.Count -gt 0) { $so['keyFields'] = @($kfNodes) } + # AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false; + # дефолт true → платформа эмитит отклонение). Захват факт. значения. + $asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') } + $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) + $hasQ = [bool]($qtNode -and $qtNode.InnerText) + if ($hasQ) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } + # ManualQuery: компилятор выводит из наличия query (hasQuery → true). Платформа в редких + # случаях (корпус 16) хранит QueryText при ManualQuery=false → фиксируем отклонение от эвристики. + $mqV = Get-Child $setNode 'ManualQuery' + if ($null -ne $mqV) { $mqActual = ($mqV -eq 'true'); if ($mqActual -ne $hasQ) { $so['manualQuery'] = $mqActual } } + # DynamicDataRead: дефолт true → эмитим только false + if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false } + # Явные поля набора (редко, ~4.5%) — захват только при наличии Field + $fieldNodes = @($setNode.SelectNodes("lf:Field", $ns)) + if ($fieldNodes.Count -gt 0) { + $fields = New-Object System.Collections.ArrayList + foreach ($fn in $fieldNodes) { + $fo = [ordered]@{} + $fld = Get-Child $fn 'field' + $dp = Get-Child $fn 'dataPath' + if ($fld) { $fo['field'] = $fld } + if ((Has-Child $fn 'dataPath') -and $dp -ne $fld) { $fo['dataPath'] = $dp } + # Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs + # DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested. + $fTypeAttr = $fn.GetAttribute("type", $NS_XSI) + if ($fTypeAttr -match 'NestedDataSet$') { $fo['nested'] = $true } + elseif ($fTypeAttr -match 'Folder$') { $fo['folder'] = $true } + $ftn = $fn.SelectSingleNode("dcssch:title", $ns) + if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } + # valueType поля набора (тип значения; вычисляемые/кастомные поля). Грамматика типа. + $fvt = $fn.SelectSingleNode("dcssch:valueType", $ns) + if ($fvt) { $fvtVal = Decompile-Type $fvt; if ($fvtVal) { $fo['valueType'] = $fvtVal } } + # presentationExpression (выражение представления поля) + appearance (формат/оформление поля) + $fpe = Get-Child $fn 'presentationExpression' + if ($null -ne $fpe -and $fpe -ne '') { $fo['presentationExpression'] = $fpe } + $fappNode = $fn.SelectSingleNode("dcssch:appearance", $ns) + if ($fappNode) { $fap = Get-SettingsAppearance $fappNode; if ($fap -and $fap.Count -gt 0) { $fo['appearance'] = $fap } } + $furNode = $fn.SelectSingleNode("dcssch:useRestriction", $ns) + if ($furNode) { $fur = Build-RestrictObj $furNode; if ($fur.Count -gt 0) { $fo['useRestriction'] = $fur } } + $faurNode = $fn.SelectSingleNode("dcssch:attributeUseRestriction", $ns) + if ($faurNode) { $faur = Build-RestrictObj $faurNode; if ($faur.Count -gt 0) { $fo['attributeUseRestriction'] = $faur } } + $fipNode = $fn.SelectSingleNode("dcssch:inputParameters", $ns) + if ($fipNode) { $fip = Build-DLInputParameters $fipNode; if (@($fip).Count -gt 0) { $fo['inputParameters'] = $fip } } + [void]$fields.Add($fo) + } + $so['fields'] = @($fields) + } + # Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*. + $calcNodes = @($setNode.SelectNodes("lf:CalculatedField", $ns)) + if ($calcNodes.Count -gt 0) { + $cfs = New-Object System.Collections.ArrayList + foreach ($cn in $calcNodes) { [void]$cfs.Add((Build-CalcField $cn)) } + $so['calculatedFields'] = @($cfs) + } + # Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings) + $paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns)) + if ($paramNodes.Count -gt 0) { + $dlPars = New-Object System.Collections.ArrayList + foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) } + $so['parameters'] = @($dlPars) + } + # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор + # регенерит каноничный скелет. Захватываем только контейнеры с реальными + # dcsset:item (filter/order/conditionalAppearance) в формат компилятора. + $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns) + if ($lsNode) { + $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns) + if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) { + $flt = @() + foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter") + if ($null -ne $bi) { $flt += $bi } + } + if ($flt.Count -gt 0) { $so['filter'] = @($flt) } + } + $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns) + if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) { + $ord = Build-Order -ordNode $oNode -loc "settings/order" + if (@($ord).Count -gt 0) { $so['order'] = @($ord) } + } + $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) + if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) { + $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" + if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } + } + # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в + # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. + $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) + if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { + $dp = Build-FormDataParameters $dpNode + if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } + } + # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). + # Канон → $null (компилятор регенерит полный скелет, как раньше). + # Группировка строк списка: прямой <dcsset:item> ListSettings (не в filter/order/CA). + $grpItemNode = $lsNode.SelectSingleNode("dcsset:item", $ns) + $grouping = $null + if ($grpItemNode) { $grouping = Build-ListGrouping $grpItemNode } + if ($null -ne $grouping) { $so['grouping'] = $grouping } + $lsShape = Get-ListSettingsShape $lsNode ($null -ne $grouping) + if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } + } + if ($so.Count -gt 0) { $ao['settings'] = $so } + } + [void]$attrs.Add($ao) + } + if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) } +} + +# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>; +# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance) +if ($attrsNode) { + $caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns) + if ($caNode) { + $ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance" + if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) } + } +} + +# parameters +$parsNode = $root.SelectSingleNode("lf:Parameters", $ns) +if ($parsNode) { + $pars = New-Object System.Collections.ArrayList + foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) { + $po = [ordered]@{}; $po['name'] = $p.GetAttribute("name") + $ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty } + if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true } + [void]$pars.Add($po) + } + if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) } +} + +# commands +$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns) +if ($cmdsNode) { + $cmds = New-Object System.Collections.ArrayList + foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) { + $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") + $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } + if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true } + # Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор + # додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале). + $tNode = $c.SelectSingleNode("lf:Title", $ns) + if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } + else { $co['title'] = '' } + $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } + $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } + $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } + # Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>) + $ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate } + $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } + Set-CommandPicture $co $c + $rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep } + [void]$cmds.Add($co) + } + if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) } +} + +# commandInterface (форменный <CommandInterface> — последний дочерний Form) +$ci = Decompile-CommandInterface +if ($null -ne $ci) { $dsl['commandInterface'] = $ci } + +# --- 6. Output --- +$json = ConvertTo-CompactJson -obj $dsl +if ($OutputPath) { + [System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false))) + Save-QueryFiles + Write-Host "form-decompile: $OutputPath" +} else { + Write-Output $json +} diff --git a/.claude/skills/form-edit/SKILL.md b/.claude/skills/form-edit/SKILL.md index bccf3a7d..1b528cc8 100644 --- a/.claude/skills/form-edit/SKILL.md +++ b/.claude/skills/form-edit/SKILL.md @@ -1,142 +1,142 @@ ---- -name: form-edit -description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С. Используй когда нужно точечно модифицировать готовую форму -argument-hint: <FormPath> <JsonPath> -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /form-edit — Редактирование формы - -Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий. - -## Использование - -``` -/form-edit <FormPath> <JsonPath> -``` - -## Параметры - -| Параметр | Обязательный | Описание | -|-----------|:------------:|----------------------------------| -| FormPath | да | Путь к существующему Form.xml | -| JsonPath | да | Путь к JSON с описанием добавлений | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>" -``` - -## JSON формат - -```json -{ - "into": "ГруппаШапка", - "after": "Контрагент", - "elements": [ - { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } - ], - "attributes": [ - { "name": "СуммаИтого", "type": "decimal(15,2)" } - ], - "commands": [ - { "name": "Рассчитать", "action": "РассчитатьОбработка" } - ] -} -``` - -### Расширения (extension-формы) - -Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции: - -```json -{ - "formEvents": [ - { "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" }, - { "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" } - ], - "elementEvents": [ - { "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" } - ], - "commands": [ - { "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" }, - { "name": "Запрос", "actions": [ - { "callType": "Before", "handler": "Расш1_ЗапросПеред" }, - { "callType": "After", "handler": "Расш1_ЗапросПосле" } - ]} - ], - "elements": [ - { "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] } - ] -} -``` - -### Позиционирование элементов - -| Ключ | По умолчанию | Описание | -|------|-------------|----------| -| `into` | корневой ChildItems | Имя группы/таблицы/страницы, куда вставлять | -| `after` | в конец | Имя элемента, после которого вставлять | - -### Типы элементов - -Те же DSL-ключи, что в `/form-compile`: - -| Ключ | XML тег | Companions | -|------|---------|------------| -| `input` | InputField | ContextMenu, ExtendedTooltip | -| `check` | CheckBoxField | ContextMenu, ExtendedTooltip | -| `label` | LabelDecoration | ContextMenu, ExtendedTooltip | -| `labelField` | LabelField | ContextMenu, ExtendedTooltip | -| `group` | UsualGroup | ExtendedTooltip | -| `table` | Table | ContextMenu, AutoCommandBar, Search*, ViewStatus* | -| `pages` | Pages | ExtendedTooltip | -| `page` | Page | ExtendedTooltip | -| `button` | Button | ExtendedTooltip | - -Группы и таблицы поддерживают `children`/`columns` для вложенных элементов. - -### Кнопки: command и stdCommand - -- `"command": "ИмяКоманды"` → `Form.Command.ИмяКоманды` -- `"stdCommand": "Close"` → `Form.StandardCommand.Close` -- `"stdCommand": "Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` (стандартная команда элемента) - -### Допустимые события (`on`) - -Компилятор предупреждает об ошибках в именах событий. Основные: - -- **input**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Clearing`, `AutoComplete`, `TextEditEnd` -- **check**: `OnChange` -- **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `BeforeAddRow`, `BeforeDeleteRow`, `OnActivateRow` -- **label/picture**: `Click`, `URLProcessing` -- **pages**: `OnCurrentPageChange` -- **button**: `Click` - -### Система типов (для attributes) - -`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). - -### Секции расширений - -| Секция | Назначение | -|--------|-----------| -| `formEvents` | События уровня формы с `callType` (Before/After/Override) | -| `elementEvents` | События на существующих элементах заимствованной формы | -| `callType` на `commands` | callType на Action команды | -| `callType` на `on` | callType на событиях новых элементов (объектный формат) | - -Все extension-секции опциональны — без них навык работает как с обычными формами. - -## Workflow - -1. `/form-info` — посмотреть текущую структуру формы -2. Создать JSON с описанием добавлений -3. `/form-edit` — добавить в форму -4. `/form-validate` — проверить корректность -5. `/form-info` — убедиться что добавилось правильно +--- +name: form-edit +description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С. Используй когда нужно точечно модифицировать готовую форму +argument-hint: <FormPath> <JsonPath> +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /form-edit — Редактирование формы + +Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий. + +## Использование + +``` +/form-edit <FormPath> <JsonPath> +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|-----------|:------------:|----------------------------------| +| FormPath | да | Путь к существующему Form.xml | +| JsonPath | да | Путь к JSON с описанием добавлений | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>" +``` + +## JSON формат + +```json +{ + "into": "ГруппаШапка", + "after": "Контрагент", + "elements": [ + { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } + ], + "attributes": [ + { "name": "СуммаИтого", "type": "decimal(15,2)" } + ], + "commands": [ + { "name": "Рассчитать", "action": "РассчитатьОбработка" } + ] +} +``` + +### Расширения (extension-формы) + +Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции: + +```json +{ + "formEvents": [ + { "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" }, + { "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" } + ], + "elementEvents": [ + { "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" } + ], + "commands": [ + { "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" }, + { "name": "Запрос", "actions": [ + { "callType": "Before", "handler": "Расш1_ЗапросПеред" }, + { "callType": "After", "handler": "Расш1_ЗапросПосле" } + ]} + ], + "elements": [ + { "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] } + ] +} +``` + +### Позиционирование элементов + +| Ключ | По умолчанию | Описание | +|------|-------------|----------| +| `into` | корневой ChildItems | Имя группы/таблицы/страницы, куда вставлять | +| `after` | в конец | Имя элемента, после которого вставлять | + +### Типы элементов + +Те же DSL-ключи, что в `/form-compile`: + +| Ключ | XML тег | Companions | +|------|---------|------------| +| `input` | InputField | ContextMenu, ExtendedTooltip | +| `check` | CheckBoxField | ContextMenu, ExtendedTooltip | +| `label` | LabelDecoration | ContextMenu, ExtendedTooltip | +| `labelField` | LabelField | ContextMenu, ExtendedTooltip | +| `group` | UsualGroup | ExtendedTooltip | +| `table` | Table | ContextMenu, AutoCommandBar, Search*, ViewStatus* | +| `pages` | Pages | ExtendedTooltip | +| `page` | Page | ExtendedTooltip | +| `button` | Button | ExtendedTooltip | + +Группы и таблицы поддерживают `children`/`columns` для вложенных элементов. + +### Кнопки: command и stdCommand + +- `"command": "ИмяКоманды"` → `Form.Command.ИмяКоманды` +- `"stdCommand": "Close"` → `Form.StandardCommand.Close` +- `"stdCommand": "Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` (стандартная команда элемента) + +### Допустимые события (`on`) + +Компилятор предупреждает об ошибках в именах событий. Основные: + +- **input**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Clearing`, `AutoComplete`, `TextEditEnd` +- **check**: `OnChange` +- **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `BeforeAddRow`, `BeforeDeleteRow`, `OnActivateRow` +- **label/picture**: `Click`, `URLProcessing` +- **pages**: `OnCurrentPageChange` +- **button**: `Click` + +### Система типов (для attributes) + +`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). + +### Секции расширений + +| Секция | Назначение | +|--------|-----------| +| `formEvents` | События уровня формы с `callType` (Before/After/Override) | +| `elementEvents` | События на существующих элементах заимствованной формы | +| `callType` на `commands` | callType на Action команды | +| `callType` на `on` | callType на событиях новых элементов (объектный формат) | + +Все extension-секции опциональны — без них навык работает как с обычными формами. + +## Workflow + +1. `/form-info` — посмотреть текущую структуру формы +2. Создать JSON с описанием добавлений +3. `/form-edit` — добавить в форму +4. `/form-validate` — проверить корректность +5. `/form-info` — убедиться что добавилось правильно diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 1de172c3..9f171d2f 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1,1430 +1,1430 @@ -# form-edit v1.3 — Edit 1C managed form elements -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$FormPath, - - [Parameter(Mandatory)] - [string]$JsonPath -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -# === 1. Load Form.xml === - -if (-not (Test-Path $FormPath)) { - Write-Error "File not found: $FormPath" - exit 1 -} -if (-not (Test-Path $JsonPath)) { - Write-Error "File not found: $JsonPath" - exit 1 -} - -$resolvedFormPath = (Resolve-Path $FormPath).Path -Assert-EditAllowed $resolvedFormPath 'editable' -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $true -try { - $xmlDoc.Load($resolvedFormPath) -} catch { - Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" - exit 1 -} - -$formNs = "http://v8.1c.ru/8.3/xcf/logform" -$v8Ns = "http://v8.1c.ru/8.1/data/core" -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("f", $formNs) -$nsMgr.AddNamespace("v8", $v8Ns) - -$root = $xmlDoc.DocumentElement - -# === 2. Load JSON === - -$def = Get-Content -Raw -Encoding UTF8 $JsonPath | ConvertFrom-Json - -# === 3. Form name + header === - -$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) -$parentDir = [System.IO.Path]::GetDirectoryName($resolvedFormPath) -if ($parentDir) { - $extDir = [System.IO.Path]::GetFileName($parentDir) - if ($extDir -eq "Ext") { - $formDir = [System.IO.Path]::GetDirectoryName($parentDir) - if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } - } -} - -Write-Host "=== form-edit: $formName ===" -Write-Host "" - -# === 4. Scan max IDs per pool === - -$script:nextElemId = 0 -$script:nextAttrId = 0 -$script:nextCmdId = 0 - -# Scan ALL element IDs via XPath (includes companions like ExtendedTooltip, ContextMenu) -$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) -if ($rootCI) { - foreach ($elem in $rootCI.SelectNodes(".//*[@id]")) { - $id = $elem.GetAttribute("id") - if ($id -and $id -ne "-1") { - try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} - } - } -} -$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) -if ($acb) { - $id = $acb.GetAttribute("id") - if ($id -and $id -ne "-1") { - try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} - } -} - -# Scan attribute IDs (including column IDs — same pool) -foreach ($attr in $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr)) { - $id = $attr.GetAttribute("id") - if ($id) { - try { $intId = [int]$id; if ($intId -gt $script:nextAttrId) { $script:nextAttrId = $intId } } catch {} - } - # Column IDs are in the same pool as attribute IDs - foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { - $colId = $col.GetAttribute("id") - if ($colId) { - try { $intColId = [int]$colId; if ($intColId -gt $script:nextAttrId) { $script:nextAttrId = $intColId } } catch {} - } - } -} - -# Scan command IDs -foreach ($cmd in $root.SelectNodes("f:Commands/f:Command", $nsMgr)) { - $id = $cmd.GetAttribute("id") - if ($id) { - try { $intId = [int]$id; if ($intId -gt $script:nextCmdId) { $script:nextCmdId = $intId } } catch {} - } -} - -$script:nextElemId++ -$script:nextAttrId++ -$script:nextCmdId++ - -# --- 4b. Auto-detect extension mode (BaseForm present) --- -$script:isExtension = $false -$baseForm = $root.SelectSingleNode("f:BaseForm", $nsMgr) -if ($baseForm) { - $script:isExtension = $true - if ($script:nextAttrId -lt 1000000) { $script:nextAttrId = 1000000 } - if ($script:nextCmdId -lt 1000000) { $script:nextCmdId = 1000000 } - if ($script:nextElemId -lt 1000000) { $script:nextElemId = 1000000 } -} - -function New-ElemId { $id = $script:nextElemId; $script:nextElemId++; return $id } -function New-AttrId { $id = $script:nextAttrId; $script:nextAttrId++; return $id } -function New-CmdId { $id = $script:nextCmdId; $script:nextCmdId++; return $id } - -# For element emitters, New-Id = New-ElemId -function New-Id { return New-ElemId } - -# === 5. Fragment helpers (StringBuilder + Emit-* from form-compile) === - -$script:xml = New-Object System.Text.StringBuilder 4096 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') -} - -function Emit-MLText { - param([string]$tag, [string]$text, [string]$indent) - X "$indent<$tag>" - X "$indent`t<v8:item>" - X "$indent`t`t<v8:lang>ru</v8:lang>" - X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>" - X "$indent`t</v8:item>" - X "$indent</$tag>" -} - -# --- Type emitter --- - -$script:formTypeSynonyms = New-Object System.Collections.Hashtable -$script:formTypeSynonyms["строка"] = "string" -$script:formTypeSynonyms["число"] = "decimal" -$script:formTypeSynonyms["булево"] = "boolean" -$script:formTypeSynonyms["дата"] = "date" -$script:formTypeSynonyms["датавремя"]= "dateTime" -$script:formTypeSynonyms["number"] = "decimal" -$script:formTypeSynonyms["bool"] = "boolean" -$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" -$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" -$script:formTypeSynonyms["документссылка"] = "DocumentRef" -$script:formTypeSynonyms["документобъект"] = "DocumentObject" -$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" -$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:formTypeSynonyms["задачассылка"] = "TaskRef" -$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $base = $Matches[1].Trim(); $params = $Matches[2] - $r = $script:formTypeSynonyms[$base.ToLower()] - if ($r) { return "$r($params)" } - return $typeStr - } - if ($typeStr.Contains('.')) { - $i = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) - $r = $script:formTypeSynonyms[$prefix.ToLower()] - if ($r) { return "$r$suffix" } - return $typeStr - } - $r = $script:formTypeSynonyms[$typeStr.ToLower()] - if ($r) { return $r } - return $typeStr -} - -function Emit-Type { - param($typeStr, [string]$indent) - if (-not $typeStr) { X "$indent<Type/>"; return } - $typeString = "$typeStr" - $parts = $typeString -split '\s*[|+]\s*' - X "$indent<Type>" - foreach ($part in $parts) { - Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" - } - X "$indent</Type>" -} - -function Emit-SingleType { - param([string]$typeStr, [string]$indent) - - $typeStr = Resolve-TypeStr $typeStr - - if ($typeStr -eq "boolean") { - X "$indent<v8:Type>xs:boolean</v8:Type>"; return - } - if ($typeStr -match '^string(\((\d+)\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - X "$indent<v8:Type>xs:string</v8:Type>" - X "$indent<v8:StringQualifiers>" - X "$indent`t<v8:Length>$len</v8:Length>" - X "$indent`t<v8:AllowedLength>Variable</v8:AllowedLength>" - X "$indent</v8:StringQualifiers>"; return - } - if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1]; $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - X "$indent<v8:Type>xs:decimal</v8:Type>" - X "$indent<v8:NumberQualifiers>" - X "$indent`t<v8:Digits>$digits</v8:Digits>" - X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" - X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" - X "$indent</v8:NumberQualifiers>"; return - } - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } "time" { "Time" } } - X "$indent<v8:Type>xs:dateTime</v8:Type>" - X "$indent<v8:DateQualifiers>" - X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" - X "$indent</v8:DateQualifiers>"; return - } - $v8Types = @{ - "ValueTable" = "v8:ValueTable"; "ValueTree" = "v8:ValueTree"; "ValueList" = "v8:ValueListType" - "TypeDescription" = "v8:TypeDescription"; "Universal" = "v8:Universal" - "FixedArray" = "v8:FixedArray"; "FixedStructure" = "v8:FixedStructure" - } - if ($v8Types.ContainsKey($typeStr)) { X "$indent<v8:Type>$($v8Types[$typeStr])</v8:Type>"; return } - $uiTypes = @{ "FormattedString" = "v8ui:FormattedString"; "Picture" = "v8ui:Picture"; "Color" = "v8ui:Color"; "Font" = "v8ui:Font" } - if ($uiTypes.ContainsKey($typeStr)) { X "$indent<v8:Type>$($uiTypes[$typeStr])</v8:Type>"; return } - if ($typeStr -eq "DynamicList") { X "$indent<v8:Type>cfg:DynamicList</v8:Type>"; return } - if ($typeStr -match '^DataComposition') { - $dcsMap = @{ "DataCompositionSettings" = "dcsset:DataCompositionSettings"; "DataCompositionSchema" = "dcssch:DataCompositionSchema"; "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" } - if ($dcsMap.ContainsKey($typeStr)) { X "$indent<v8:Type>$($dcsMap[$typeStr])</v8:Type>"; return } - } - if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.') { - X "$indent<v8:Type>cfg:$typeStr</v8:Type>"; return - } - if ($typeStr.Contains('.')) { X "$indent<v8:Type>cfg:$typeStr</v8:Type>" } - else { X "$indent<v8:Type>$typeStr</v8:Type>" } -} - -# --- Event handler name generator --- - -$script:eventSuffixMap = @{ - "OnChange" = "ПриИзменении"; "StartChoice" = "НачалоВыбора"; "ChoiceProcessing" = "ОбработкаВыбора" - "AutoComplete" = "АвтоПодбор"; "Clearing" = "Очистка"; "Opening" = "Открытие"; "Click" = "Нажатие" - "OnActivateRow" = "ПриАктивизацииСтроки"; "BeforeAddRow" = "ПередНачаломДобавления" - "BeforeDeleteRow" = "ПередУдалением"; "BeforeRowChange" = "ПередНачаломИзменения" - "OnStartEdit" = "ПриНачалеРедактирования"; "OnEndEdit" = "ПриОкончанииРедактирования" - "Selection" = "ВыборСтроки"; "OnCurrentPageChange" = "ПриСменеСтраницы" - "TextEditEnd" = "ОкончаниеВводаТекста"; "URLProcessing" = "ОбработкаНавигационнойСсылки" - "DragStart" = "НачалоПеретаскивания"; "Drag" = "Перетаскивание" - "DragCheck" = "ПроверкаПеретаскивания"; "Drop" = "Помещение"; "AfterDeleteRow" = "ПослеУдаления" -} - -function Get-HandlerName { - param([string]$elementName, [string]$eventName) - $suffix = $script:eventSuffixMap[$eventName] - if ($suffix) { return "$elementName$suffix" } - return "$elementName$eventName" -} - -# --- Element helpers --- - -function Get-ElementName { - param($el, [string]$typeKey) - if ($el.name) { return "$($el.name)" } - return "$($el.$typeKey)" -} - -# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс). -function Assert-EditUnique { - param([string]$name, [hashtable]$seen, [string]$ctx) - if ($seen.ContainsKey($name)) { - Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form" - exit 1 - } - $seen[$name] = $true -} - -$script:knownEvents = @{ - "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") - "check" = @("OnChange") - "label" = @("Click","URLProcessing") - "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") - "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") - "pages" = @("OnCurrentPageChange") - "page" = @("OnCurrentPageChange") - "button" = @("Click") - "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") - "calendar" = @("OnChange","OnActivate") - "picture" = @("Click") - "cmdBar" = @() - "popup" = @() - "group" = @() -} - -function Emit-Events { - param($el, [string]$elementName, [string]$indent, [string]$typeKey) - if (-not $el.on) { return } - - # Validate event names - if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { - $allowed = $script:knownEvents[$typeKey] - foreach ($evt in $el.on) { - $evtStr = if ($evt -is [string]) { "$evt" } else { "$($evt.event)" } - if ($allowed.Count -gt 0 -and $allowed -notcontains $evtStr) { - Write-Host "[WARN] Unknown event '$evtStr' for $typeKey '$elementName'. Known: $($allowed -join ', ')" - } - } - } - - X "$indent<Events>" - foreach ($evt in $el.on) { - # Support both string ("OnChange") and object ({ "event": "OnChange", "callType": "After" }) - if ($evt -is [string] -or -not $evt.event) { - $evtName = "$evt" - $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } - else { Get-HandlerName -elementName $elementName -eventName $evtName } - X "$indent`t<Event name=`"$evtName`">$handler</Event>" - } else { - $evtName = "$($evt.event)" - $handler = if ($evt.handler) { "$($evt.handler)" } - elseif ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } - else { Get-HandlerName -elementName $elementName -eventName $evtName } - $callTypeAttr = if ($evt.callType) { " callType=`"$($evt.callType)`"" } else { "" } - X "$indent`t<Event name=`"$evtName`"$callTypeAttr>$handler</Event>" - } - } - X "$indent</Events>" -} - -function Emit-Companion { - param([string]$tag, [string]$name, [string]$indent) - $id = New-Id - X "$indent<$tag name=`"$name`" id=`"$id`"/>" -} - -function Emit-CommonFlags { - param($el, [string]$indent) - if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" } - if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" } - if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" } -} - -function Emit-Title { - param($el, [string]$name, [string]$indent) - if ($el.title) { Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent } -} - -# --- Element emitters --- - -function Emit-Group { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent<UsualGroup name=`"$name`" id=`"$id`">" - $inner = "$indent`t" - Emit-Title -el $el -name $name -indent $inner - $groupVal = "$($el.group)" - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } "vertical" { "Vertical" } - "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } - default { $null } - } - if ($orientation) { X "$inner<Group>$orientation</Group>" } - if ($groupVal -eq "collapsible") { X "$inner<Group>Vertical</Group>"; X "$inner<Behavior>Collapsible</Behavior>" } - if ($el.representation) { - $repr = switch ("$($el.representation)") { "none" { "None" } "normal" { "NormalSeparation" } "weak" { "WeakSeparation" } "strong" { "StrongSeparation" } default { "$($el.representation)" } } - X "$inner<Representation>$repr</Representation>" - } - if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" } - if ($el.united -eq $false) { X "$inner<United>false</United>" } - Emit-CommonFlags -el $el -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - if ($el.children -and $el.children.Count -gt 0) { - X "$inner<ChildItems>" - foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } - X "$inner</ChildItems>" - } - X "$indent</UsualGroup>" -} - -function Emit-Input { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent<InputField name=`"$name`" id=`"$id`">" - $inner = "$indent`t" - if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { "none" { "None" } "left" { "Left" } "right" { "Right" } "top" { "Top" } "bottom" { "Bottom" } default { "$($el.titleLocation)" } } - X "$inner<TitleLocation>$loc</TitleLocation>" - } - if ($el.multiLine -eq $true) { X "$inner<MultiLine>true</MultiLine>" } - if ($el.passwordMode -eq $true) { X "$inner<PasswordMode>true</PasswordMode>" } - if ($el.choiceButton -eq $false) { X "$inner<ChoiceButton>false</ChoiceButton>" } - if ($el.clearButton -eq $true) { X "$inner<ClearButton>true</ClearButton>" } - if ($el.spinButton -eq $true) { X "$inner<SpinButton>true</SpinButton>" } - if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" } - if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" } - if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" } - if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" } - if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" } - if ($el.width) { X "$inner<Width>$($el.width)</Width>" } - if ($el.height) { X "$inner<Height>$($el.height)</Height>" } - if ($el.horizontalStretch -eq $true) { X "$inner<HorizontalStretch>true</HorizontalStretch>" } - if ($el.verticalStretch -eq $true) { X "$inner<VerticalStretch>true</VerticalStretch>" } - if ($el.inputHint) { Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" - X "$indent</InputField>" -} - -function Emit-Check { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent<CheckBoxField name=`"$name`" id=`"$id`">" - $inner = "$indent`t" - if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { X "$inner<TitleLocation>$($el.titleLocation)</TitleLocation>" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" - X "$indent</CheckBoxField>" -} - -function Emit-Label { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent<LabelDecoration name=`"$name`" id=`"$id`">" - $inner = "$indent`t" - if ($el.title) { - $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } - X "$inner<Title formatted=`"$formatted`">" - X "$inner`t<v8:item>" - X "$inner`t`t<v8:lang>ru</v8:lang>" - X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>" - X "$inner`t</v8:item>" - X "$inner" - } - Emit-CommonFlags -el $el -indent $inner - if ($el.hyperlink -eq $true) { X "$innertrue" } - if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } - if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" - X "$indent" -} - -function Emit-LabelField { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" - X "$indent" -} - -function Emit-Table { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent
" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.representation) { X "$inner$($el.representation)" } - if ($el.changeRowSet -eq $true) { X "$innertrue" } - if ($el.changeRowOrder -eq $true) { X "$innertrue" } - if ($el.height) { X "$inner$($el.height)" } - if ($el.header -eq $false) { X "$inner
false
" } - if ($el.footer -eq $true) { X "$inner
true
" } - if ($el.commandBarLocation) { X "$inner$($el.commandBarLocation)" } - if ($el.searchStringLocation) { X "$inner$($el.searchStringLocation)" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner - Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner - Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner - Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner - if ($el.columns -and $el.columns.Count -gt 0) { - X "$inner" - foreach ($col in $el.columns) { Emit-Element -el $col -indent "$inner`t" } - X "$inner" - } - Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" - X "$indent
" -} - -function Emit-Pages { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.pagesRepresentation) { X "$inner$($el.pagesRepresentation)" } - Emit-CommonFlags -el $el -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } - X "$inner" - } - X "$indent" -} - -function Emit-Page { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.group) { - $orientation = switch ("$($el.group)") { "horizontal" { "Horizontal" } "vertical" { "Vertical" } "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } default { $null } } - if ($orientation) { X "$inner$orientation" } - } - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } - X "$inner" - } - X "$indent" -} - -function Emit-Button { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" -} - -function Emit-PictureDecoration { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.picture -or $el.src) { - $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } - X "$inner"; X "$inner`t$ref"; X "$inner`ttrue"; X "$inner" - } - if ($el.hyperlink -eq $true) { X "$innertrue" } - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" - X "$indent" -} - -function Emit-PictureField { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" - X "$indent" -} - -function Emit-Calendar { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" - X "$indent" -} - -function Emit-CommandBarEl { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.autofill -eq $true) { X "$innertrue" } - Emit-CommonFlags -el $el -indent $inner - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } - X "$inner" - } - X "$indent" -} - -function Emit-Popup { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - if ($el.picture) { - X "$inner"; X "$inner`t$($el.picture)"; X "$inner`ttrue"; X "$inner" - } - if ($el.representation) { X "$inner$($el.representation)" } - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } - X "$inner" - } - X "$indent" -} - -# --- Element dispatcher --- - -function Emit-Element { - param($el, [string]$indent) - - $typeKey = $null - foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { - if ($el.$key -ne $null) { $typeKey = $key; break } - } - if (-not $typeKey) { Write-Warning "Unknown element type, skipping"; return } - - # Validate known keys — warn about typos - $knownKeys = @{ - "group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 - "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 - "name"=1;"path"=1;"title"=1 - "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1 - "on"=1;"handlers"=1 - "titleLocation"=1;"representation"=1;"width"=1;"height"=1 - "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 - "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 - "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 - "hyperlink"=1;"showTitle"=1;"united"=1;"children"=1;"columns"=1 - "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 - "commandBarLocation"=1;"searchStringLocation"=1;"pagesRepresentation"=1 - "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 - "src"=1;"autofill"=1 - } - foreach ($p in $el.PSObject.Properties) { - if (-not $knownKeys.ContainsKey($p.Name)) { - Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored." - } - } - - $name = Get-ElementName -el $el -typeKey $typeKey - $id = New-Id - - switch ($typeKey) { - "group" { Emit-Group -el $el -name $name -id $id -indent $indent } - "input" { Emit-Input -el $el -name $name -id $id -indent $indent } - "check" { Emit-Check -el $el -name $name -id $id -indent $indent } - "label" { Emit-Label -el $el -name $name -id $id -indent $indent } - "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } - "table" { Emit-Table -el $el -name $name -id $id -indent $indent } - "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } - "page" { Emit-Page -el $el -name $name -id $id -indent $indent } - "button" { Emit-Button -el $el -name $name -id $id -indent $indent } - "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } - "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } - "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } - "cmdBar" { Emit-CommandBarEl -el $el -name $name -id $id -indent $indent } - "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } - } -} - -# === 6. Find element by name recursively === - -function Find-Element($startNode, [string]$targetName) { - foreach ($child in $startNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childName = $child.GetAttribute("name") - if ($childName -eq $targetName) { return $child } - $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) - if ($ci) { - $found = Find-Element $ci $targetName - if ($found) { return $found } - } - } - return $null -} - -# === 7. Detect indent level of a container's children === - -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - $text = $child.Value - if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($text -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - # Fallback: count depth from root - $depth = 0 - $current = $container - while ($current -and $current -ne $xmlDoc.DocumentElement) { - $depth++ - $current = $current.ParentNode - } - return "`t" * ($depth + 1) -} - -# === 8. Insert node into container === - -function Insert-IntoContainer($container, $newNode, $afterName, $childIndent) { - $refNode = $null - - if ($afterName) { - # Find the after-element, then insert after it - $afterElem = $null - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.GetAttribute("name") -eq $afterName) { - $afterElem = $child - break - } - } - if ($afterElem) { - $refNode = $afterElem.NextSibling - } else { - Write-Host "[WARN] after='$afterName' not found in target container, appending at end" - } - } - - if (-not $refNode) { - # Append at end: insert before trailing whitespace - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $refNode = $trailing - } - } - - $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $refNode) | Out-Null - } else { - # Container is empty (self-closing) — add framing whitespace - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } -} - -# === 9. Generate fragment, parse, import nodes === - -$allNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema"' - -function Parse-Fragment([string]$xmlText) { - $fragDoc = New-Object System.Xml.XmlDocument - $fragDoc.PreserveWhitespace = $true - $fragDoc.LoadXml($xmlText) - return $fragDoc -} - -function Import-ElementNodes($fragDoc) { - $nodes = @() - foreach ($child in $fragDoc.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $xmlDoc.ImportNode($child, $true) - } - } - return $nodes -} - -# === 10. Add elements === - -$addedElems = @() -$companionCount = 0 - -if ($def.elements -and $def.elements.Count -gt 0) { - # Resolve target container - $targetCI = $null - $intoName = if ($def.into) { "$($def.into)" } else { $null } - $afterName = if ($def.after) { "$($def.after)" } else { $null } - - if ($intoName) { - $targetGroup = Find-Element $rootCI $intoName - if (-not $targetGroup) { - Write-Host "[ERROR] Target group '$intoName' not found" - exit 1 - } - $targetCI = $targetGroup.SelectSingleNode("f:ChildItems", $nsMgr) - if (-not $targetCI) { - # Create ChildItems for the group - $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) - $targetGroup.AppendChild($targetCI) | Out-Null - } - } elseif ($afterName) { - # Find the after element globally and use its parent as target - $afterElem = Find-Element $rootCI $afterName - if (-not $afterElem) { - Write-Host "[ERROR] Element '$afterName' not found" - exit 1 - } - $targetCI = $afterElem.ParentNode - } else { - $targetCI = $rootCI - } - - if (-not $targetCI) { - # Create ChildItems section in form — insert after Events or AutoCommandBar - $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) - $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) - if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) } - if ($insertAfter) { - $refNode = $insertAfter.NextSibling - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($targetCI, $refNode) | Out-Null - } else { - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null - $root.AppendChild($targetCI) | Out-Null - } - # Also update $rootCI reference - $rootCI = $targetCI - } - - # Detect indent level - $childIndent = Get-ChildIndent $targetCI - - # Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения - # (рекурсивно по children/columns). - $elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup") - function Walk-ElemNames($el, [hashtable]$seen) { - $tk = $null - foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } } - if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' } - if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } } - if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } } - } - $dslElemNames = @{} - foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames } - - # Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется) - foreach ($el in $def.elements) { - $typeKey = $null - foreach ($key in $elemTypeKeys) { - if ($el.$key -ne $null) { $typeKey = $key; break } - } - if ($typeKey) { - $elName = Get-ElementName -el $el -typeKey $typeKey - $existing = Find-Element $rootCI $elName - if ($existing) { - Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique" - exit 1 - } - } - } - - # Remember starting element ID for companion counting - $startElemId = $script:nextElemId - - # Generate fragment - $script:xml = New-Object System.Text.StringBuilder 4096 - X "<_F $allNsDecl>" - foreach ($el in $def.elements) { - Emit-Element -el $el -indent $childIndent - } - X "" - - $fragDoc = Parse-Fragment $script:xml.ToString() - $importedNodes = Import-ElementNodes $fragDoc - - # Count actual elements (non-companion) for reporting - foreach ($el in $def.elements) { - $typeKey = $null - foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { - if ($el.$key -ne $null) { $typeKey = $key; break } - } - $name = Get-ElementName -el $el -typeKey $typeKey - $tagMap = @{ - "group"="Group"; "input"="Input"; "check"="Check"; "label"="Label"; "labelField"="LabelField" - "table"="Table"; "pages"="Pages"; "page"="Page"; "button"="Button" - "picture"="Picture"; "picField"="PicField"; "calendar"="Calendar"; "cmdBar"="CmdBar"; "popup"="Popup" - } - $pathStr = if ($el.path) { " -> $($el.path)" } else { "" } - $evtStr = if ($el.on) { " {$($el.on -join ', ')}" } else { "" } - $addedElems += " + [$($tagMap[$typeKey])] $name$pathStr$evtStr" - } - - # Insert each imported node - foreach ($node in $importedNodes) { - Insert-IntoContainer -container $targetCI -newNode $node -afterName $afterName -childIndent $childIndent - # Only use afterName for the first insertion; subsequent ones go after the previous - $afterName = $node.GetAttribute("name") - } - - $totalNewElemIds = $script:nextElemId - $startElemId - $companionCount = $totalNewElemIds - $def.elements.Count -} - -# === 11. Add attributes === - -$addedAttrs = @() - -if ($def.attributes -and $def.attributes.Count -gt 0) { - $attrsSection = $root.SelectSingleNode("f:Attributes", $nsMgr) - if (-not $attrsSection) { - # Create Attributes section — insert after ChildItems or after Events - $attrsSection = $xmlDoc.CreateElement("Attributes", $formNs) - # Find insertion point: after ChildItems or after the last pre-Attributes element - $insertAfter = $rootCI - if (-not $insertAfter) { - $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) - } - if (-not $insertAfter) { - $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) - } - if ($insertAfter) { - $refNode = $insertAfter.NextSibling - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($attrsSection, $refNode) | Out-Null - } else { - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null - $root.AppendChild($attrsSection) | Out-Null - } - } - - # Detect indent for attribute children - $attrChildIndent = Get-ChildIndent $attrsSection - if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" } - - # Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и - # против уже существующих реквизитов формы. - $dslAttrNames = @{} - foreach ($attr in $def.attributes) { - Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name' - if ($attr.columns) { - $dslColNames = @{} - foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" } - } - $existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr) - if ($existingAttr) { - Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique" - exit 1 - } - } - - # Generate attribute fragments - $script:xml = New-Object System.Text.StringBuilder 2048 - X "<_F $allNsDecl>" - foreach ($attr in $def.attributes) { - $attrId = New-AttrId - $attrName = "$($attr.name)" - X "$attrChildIndent" - $inner = "$attrChildIndent`t" - - if ($attr.title) { Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner } - if ($attr.type) { Emit-Type -typeStr "$($attr.type)" -indent $inner } else { X "$inner" } - if ($attr.main -eq $true) { X "$innertrue" } - if ($attr.savedData -eq $true) { X "$innertrue" } - if ($attr.fillChecking) { X "$inner$($attr.fillChecking)" } - - if ($attr.columns -and $attr.columns.Count -gt 0) { - X "$inner" - $colId = 1 - foreach ($col in $attr.columns) { - X "$inner`t" - if ($col.title) { Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" } - Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" - X "$inner`t" - $colId++ - } - X "$inner" - } - - X "$attrChildIndent" - $typeStr = if ($attr.type) { "$($attr.type)" } else { "(no type)" } - $addedAttrs += " + ${attrName}: $typeStr (id=$attrId)" - } - X "" - - $fragDoc = Parse-Fragment $script:xml.ToString() - $importedAttrs = Import-ElementNodes $fragDoc - - foreach ($node in $importedAttrs) { - Insert-IntoContainer -container $attrsSection -newNode $node -afterName $null -childIndent $attrChildIndent - } -} - -# === 12. Add commands === - -$addedCmds = @() - -if ($def.commands -and $def.commands.Count -gt 0) { - $cmdsSection = $root.SelectSingleNode("f:Commands", $nsMgr) - if (-not $cmdsSection) { - # Create Commands section — insert after Parameters or Attributes - $cmdsSection = $xmlDoc.CreateElement("Commands", $formNs) - $insertAfter = $root.SelectSingleNode("f:Parameters", $nsMgr) - if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:Attributes", $nsMgr) } - if (-not $insertAfter) { $insertAfter = $rootCI } - if ($insertAfter) { - $refNode = $insertAfter.NextSibling - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($cmdsSection, $refNode) | Out-Null - } else { - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null - $root.AppendChild($cmdsSection) | Out-Null - } - } - - $cmdChildIndent = Get-ChildIndent $cmdsSection - if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" } - - # Уникальность имён команд: внутри JSON-определения и против существующих команд формы. - $dslCmdNames = @{} - foreach ($cmd in $def.commands) { - Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name' - $existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr) - if ($existingCmd) { - Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique" - exit 1 - } - } - - # Generate command fragments - $script:xml = New-Object System.Text.StringBuilder 1024 - X "<_F $allNsDecl>" - foreach ($cmd in $def.commands) { - $cmdId = New-CmdId - $cmdName = "$($cmd.name)" - X "$cmdChildIndent" - $inner = "$cmdChildIndent`t" - - if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner } - - # Support single action with optional callType, or multiple actions - if ($cmd.actions) { - # Multiple actions: [{ "callType": "Before", "handler": "..." }, ...] - foreach ($act in $cmd.actions) { - $actHandler = "$($act.handler)" - $callTypeAttr = if ($act.callType) { " callType=`"$($act.callType)`"" } else { "" } - X "$inner$actHandler" - } - } elseif ($cmd.action) { - $callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" } - X "$inner$($cmd.action)" - } - - if ($cmd.shortcut) { X "$inner$($cmd.shortcut)" } - if ($cmd.picture) { - X "$inner" - X "$inner`t$($cmd.picture)" - X "$inner`ttrue" - X "$inner" - } - if ($cmd.representation) { X "$inner$($cmd.representation)" } - - X "$cmdChildIndent" - $actionStr = if ($cmd.action) { " -> $($cmd.action)" } elseif ($cmd.actions) { " -> $($cmd.actions.Count) action(s)" } else { "" } - $addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)" - } - X "" - - $fragDoc = Parse-Fragment $script:xml.ToString() - $importedCmds = Import-ElementNodes $fragDoc - - foreach ($node in $importedCmds) { - Insert-IntoContainer -container $cmdsSection -newNode $node -afterName $null -childIndent $cmdChildIndent - } -} - -# === 12b. Add form-level events === - -$addedFormEvents = @() - -if ($def.formEvents -and $def.formEvents.Count -gt 0) { - $eventsSection = $root.SelectSingleNode("f:Events", $nsMgr) - if (-not $eventsSection) { - # Create Events section — insert after AutoCommandBar or at the beginning - $eventsSection = $xmlDoc.CreateElement("Events", $formNs) - $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) - if ($insertAfter) { - # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) - $ws1 = $xmlDoc.CreateWhitespace("`r`n`t") - $ws2 = $xmlDoc.CreateWhitespace("`r`n`t") - if ($insertAfter.NextSibling) { - $root.InsertBefore($ws1, $insertAfter.NextSibling) | Out-Null - $root.InsertBefore($eventsSection, $ws1) | Out-Null - $root.InsertBefore($ws2, $eventsSection) | Out-Null - } else { - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null - $root.AppendChild($eventsSection) | Out-Null - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n")) | Out-Null - } - } else { - $firstChild = $root.FirstChild - if ($firstChild) { - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - $root.InsertBefore($eventsSection, $firstChild) | Out-Null - $root.InsertBefore($ws, $eventsSection) | Out-Null - } else { - $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null - $root.AppendChild($eventsSection) | Out-Null - } - } - } - - $evtChildIndent = Get-ChildIndent $eventsSection - if (-not $evtChildIndent -or $evtChildIndent -eq "") { $evtChildIndent = "`t`t" } - - # Generate event fragments - $script:xml = New-Object System.Text.StringBuilder 512 - X "<_F $allNsDecl>" - foreach ($fe in $def.formEvents) { - $feName = "$($fe.name)" - $feHandler = "$($fe.handler)" - $callTypeAttr = if ($fe.callType) { " callType=`"$($fe.callType)`"" } else { "" } - X "$evtChildIndent$feHandler" - $ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" } - $addedFormEvents += " + $feName${ctStr} -> $feHandler" - } - X "" - - $fragDoc = Parse-Fragment $script:xml.ToString() - $importedEvents = Import-ElementNodes $fragDoc - - foreach ($node in $importedEvents) { - Insert-IntoContainer -container $eventsSection -newNode $node -afterName $null -childIndent $evtChildIndent - } -} - -# === 12c. Add element-level events === - -$addedElemEvents = @() - -if ($def.elementEvents -and $def.elementEvents.Count -gt 0) { - if (-not $rootCI) { - $rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) - } - - foreach ($ee in $def.elementEvents) { - $targetName = "$($ee.element)" - $targetEl = Find-Element $rootCI $targetName - if (-not $targetEl) { - Write-Host "[WARN] Element '$targetName' not found — skipping elementEvent" - continue - } - - # Find or create Events element within the target - $targetEvents = $targetEl.SelectSingleNode("f:Events", $nsMgr) - if (-not $targetEvents) { - $targetEvents = $xmlDoc.CreateElement("Events", $formNs) - # Insert Events before closing tag (after last property, before ChildItems if any) - $ciNode = $targetEl.SelectSingleNode("f:ChildItems", $nsMgr) - if ($ciNode) { - $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) - $targetEl.InsertBefore($ws, $ciNode) | Out-Null - $targetEl.InsertBefore($targetEvents, $ciNode) | Out-Null - } else { - $trailing = $targetEl.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) - $targetEl.InsertBefore($ws, $trailing) | Out-Null - $targetEl.InsertBefore($targetEvents, $trailing) | Out-Null - } else { - $targetEl.AppendChild($xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))) | Out-Null - $targetEl.AppendChild($targetEvents) | Out-Null - } - } - } - - $eeChildIndent = Get-ChildIndent $targetEvents - if (-not $eeChildIndent -or $eeChildIndent -eq "") { - $parentIndent = Get-ChildIndent $targetEl - $eeChildIndent = "$parentIndent`t" - } - - # Create Event element - $eeName = "$($ee.name)" - $eeHandler = "$($ee.handler)" - $callTypeAttr = if ($ee.callType) { " callType=`"$($ee.callType)`"" } else { "" } - - $script:xml = New-Object System.Text.StringBuilder 256 - X "<_F $allNsDecl>" - X "$eeChildIndent$eeHandler" - X "" - - $fragDoc = Parse-Fragment $script:xml.ToString() - $importedEE = Import-ElementNodes $fragDoc - - foreach ($node in $importedEE) { - Insert-IntoContainer -container $targetEvents -newNode $node -afterName $null -childIndent $eeChildIndent - } - - $ctStr = if ($ee.callType) { "[$($ee.callType)]" } else { "" } - $addedElemEvents += " + $targetName.$eeName${ctStr} -> $eeHandler" - } -} - -# === 13. Save === - -$content = $xmlDoc.OuterXml -# Ensure encoding declaration is uppercase UTF-8 -$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '' - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) - -# === 14. Summary === - -if ($script:isExtension) { - Write-Host "[EXTENSION] BaseForm detected — IDs start at 1000000+" - Write-Host "" -} - -if ($addedFormEvents.Count -gt 0) { - Write-Host "Added form events:" - foreach ($line in $addedFormEvents) { Write-Host $line } - Write-Host "" -} - -if ($addedElemEvents.Count -gt 0) { - Write-Host "Added element events:" - foreach ($line in $addedElemEvents) { Write-Host $line } - Write-Host "" -} - -if ($addedElems.Count -gt 0) { - $posStr = "" - if ($def.into) { $posStr += "into $($def.into)" } - if ($def.after) { if ($posStr) { $posStr += ", " }; $posStr += "after $($def.after)" } - if ($posStr) { $posStr = " ($posStr)" } - Write-Host "Added elements${posStr}:" - foreach ($line in $addedElems) { Write-Host $line } - Write-Host "" -} - -if ($addedAttrs.Count -gt 0) { - Write-Host "Added attributes:" - foreach ($line in $addedAttrs) { Write-Host $line } - Write-Host "" -} - -if ($addedCmds.Count -gt 0) { - Write-Host "Added commands:" - foreach ($line in $addedCmds) { Write-Host $line } - Write-Host "" -} - -Write-Host "---" -$totalParts = @() -if ($addedFormEvents.Count -gt 0) { $totalParts += "$($addedFormEvents.Count) form event(s)" } -if ($addedElemEvents.Count -gt 0) { $totalParts += "$($addedElemEvents.Count) element event(s)" } -if ($addedElems.Count -gt 0) { - $compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" } - $totalParts += "$($addedElems.Count) element(s)$compStr" -} -if ($addedAttrs.Count -gt 0) { $totalParts += "$($addedAttrs.Count) attribute(s)" } -if ($addedCmds.Count -gt 0) { $totalParts += "$($addedCmds.Count) command(s)" } -Write-Host "Total: $($totalParts -join ', ')" -Write-Host "Run /form-validate to verify." +# form-edit v1.3 — Edit 1C managed form elements +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$FormPath, + + [Parameter(Mandatory)] + [string]$JsonPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +# === 1. Load Form.xml === + +if (-not (Test-Path $FormPath)) { + Write-Error "File not found: $FormPath" + exit 1 +} +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$resolvedFormPath = (Resolve-Path $FormPath).Path +Assert-EditAllowed $resolvedFormPath 'editable' +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +try { + $xmlDoc.Load($resolvedFormPath) +} catch { + Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" + exit 1 +} + +$formNs = "http://v8.1c.ru/8.3/xcf/logform" +$v8Ns = "http://v8.1c.ru/8.1/data/core" +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("f", $formNs) +$nsMgr.AddNamespace("v8", $v8Ns) + +$root = $xmlDoc.DocumentElement + +# === 2. Load JSON === + +$def = Get-Content -Raw -Encoding UTF8 $JsonPath | ConvertFrom-Json + +# === 3. Form name + header === + +$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) +$parentDir = [System.IO.Path]::GetDirectoryName($resolvedFormPath) +if ($parentDir) { + $extDir = [System.IO.Path]::GetFileName($parentDir) + if ($extDir -eq "Ext") { + $formDir = [System.IO.Path]::GetDirectoryName($parentDir) + if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } + } +} + +Write-Host "=== form-edit: $formName ===" +Write-Host "" + +# === 4. Scan max IDs per pool === + +$script:nextElemId = 0 +$script:nextAttrId = 0 +$script:nextCmdId = 0 + +# Scan ALL element IDs via XPath (includes companions like ExtendedTooltip, ContextMenu) +$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($rootCI) { + foreach ($elem in $rootCI.SelectNodes(".//*[@id]")) { + $id = $elem.GetAttribute("id") + if ($id -and $id -ne "-1") { + try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} + } + } +} +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $id = $acb.GetAttribute("id") + if ($id -and $id -ne "-1") { + try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} + } +} + +# Scan attribute IDs (including column IDs — same pool) +foreach ($attr in $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr)) { + $id = $attr.GetAttribute("id") + if ($id) { + try { $intId = [int]$id; if ($intId -gt $script:nextAttrId) { $script:nextAttrId = $intId } } catch {} + } + # Column IDs are in the same pool as attribute IDs + foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { + $colId = $col.GetAttribute("id") + if ($colId) { + try { $intColId = [int]$colId; if ($intColId -gt $script:nextAttrId) { $script:nextAttrId = $intColId } } catch {} + } + } +} + +# Scan command IDs +foreach ($cmd in $root.SelectNodes("f:Commands/f:Command", $nsMgr)) { + $id = $cmd.GetAttribute("id") + if ($id) { + try { $intId = [int]$id; if ($intId -gt $script:nextCmdId) { $script:nextCmdId = $intId } } catch {} + } +} + +$script:nextElemId++ +$script:nextAttrId++ +$script:nextCmdId++ + +# --- 4b. Auto-detect extension mode (BaseForm present) --- +$script:isExtension = $false +$baseForm = $root.SelectSingleNode("f:BaseForm", $nsMgr) +if ($baseForm) { + $script:isExtension = $true + if ($script:nextAttrId -lt 1000000) { $script:nextAttrId = 1000000 } + if ($script:nextCmdId -lt 1000000) { $script:nextCmdId = 1000000 } + if ($script:nextElemId -lt 1000000) { $script:nextElemId = 1000000 } +} + +function New-ElemId { $id = $script:nextElemId; $script:nextElemId++; return $id } +function New-AttrId { $id = $script:nextAttrId; $script:nextAttrId++; return $id } +function New-CmdId { $id = $script:nextCmdId; $script:nextCmdId++; return $id } + +# For element emitters, New-Id = New-ElemId +function New-Id { return New-ElemId } + +# === 5. Fragment helpers (StringBuilder + Emit-* from form-compile) === + +$script:xml = New-Object System.Text.StringBuilder 4096 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function Emit-MLText { + param([string]$tag, [string]$text, [string]$indent) + X "$indent<$tag>" + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml $text)" + X "$indent`t" + X "$indent" +} + +# --- Type emitter --- + +$script:formTypeSynonyms = New-Object System.Collections.Hashtable +$script:formTypeSynonyms["строка"] = "string" +$script:formTypeSynonyms["число"] = "decimal" +$script:formTypeSynonyms["булево"] = "boolean" +$script:formTypeSynonyms["дата"] = "date" +$script:formTypeSynonyms["датавремя"]= "dateTime" +$script:formTypeSynonyms["number"] = "decimal" +$script:formTypeSynonyms["bool"] = "boolean" +$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" +$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" +$script:formTypeSynonyms["документссылка"] = "DocumentRef" +$script:formTypeSynonyms["документобъект"] = "DocumentObject" +$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" +$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:formTypeSynonyms["задачассылка"] = "TaskRef" +$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $base = $Matches[1].Trim(); $params = $Matches[2] + $r = $script:formTypeSynonyms[$base.ToLower()] + if ($r) { return "$r($params)" } + return $typeStr + } + if ($typeStr.Contains('.')) { + $i = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) + $r = $script:formTypeSynonyms[$prefix.ToLower()] + if ($r) { return "$r$suffix" } + return $typeStr + } + $r = $script:formTypeSynonyms[$typeStr.ToLower()] + if ($r) { return $r } + return $typeStr +} + +function Emit-Type { + param($typeStr, [string]$indent) + if (-not $typeStr) { X "$indent"; return } + $typeString = "$typeStr" + $parts = $typeString -split '\s*[|+]\s*' + X "$indent" + foreach ($part in $parts) { + Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" + } + X "$indent" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + $typeStr = Resolve-TypeStr $typeStr + + if ($typeStr -eq "boolean") { + X "$indentxs:boolean"; return + } + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent"; return + } + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1]; $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent"; return + } + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } "time" { "Time" } } + X "$indentxs:dateTime" + X "$indent" + X "$indent`t$fractions" + X "$indent"; return + } + $v8Types = @{ + "ValueTable" = "v8:ValueTable"; "ValueTree" = "v8:ValueTree"; "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription"; "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray"; "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { X "$indent$($v8Types[$typeStr])"; return } + $uiTypes = @{ "FormattedString" = "v8ui:FormattedString"; "Picture" = "v8ui:Picture"; "Color" = "v8ui:Color"; "Font" = "v8ui:Font" } + if ($uiTypes.ContainsKey($typeStr)) { X "$indent$($uiTypes[$typeStr])"; return } + if ($typeStr -eq "DynamicList") { X "$indentcfg:DynamicList"; return } + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ "DataCompositionSettings" = "dcsset:DataCompositionSettings"; "DataCompositionSchema" = "dcssch:DataCompositionSchema"; "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" } + if ($dcsMap.ContainsKey($typeStr)) { X "$indent$($dcsMap[$typeStr])"; return } + } + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.') { + X "$indentcfg:$typeStr"; return + } + if ($typeStr.Contains('.')) { X "$indentcfg:$typeStr" } + else { X "$indent$typeStr" } +} + +# --- Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении"; "StartChoice" = "НачалоВыбора"; "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор"; "Clearing" = "Очистка"; "Opening" = "Открытие"; "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки"; "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением"; "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования"; "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки"; "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста"; "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания"; "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания"; "Drop" = "Помещение"; "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { return "$elementName$suffix" } + return "$elementName$eventName" +} + +# --- Element helpers --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс). +function Assert-EditUnique { + param([string]$name, [hashtable]$seen, [string]$ctx) + if ($seen.ContainsKey($name)) { + Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form" + exit 1 + } + $seen[$name] = $true +} + +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "label" = @("Click","URLProcessing") + "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") + "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") + "pages" = @("OnCurrentPageChange") + "page" = @("OnCurrentPageChange") + "button" = @("Click") + "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") + "calendar" = @("OnChange","OnActivate") + "picture" = @("Click") + "cmdBar" = @() + "popup" = @() + "group" = @() +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent, [string]$typeKey) + if (-not $el.on) { return } + + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($evt in $el.on) { + $evtStr = if ($evt -is [string]) { "$evt" } else { "$($evt.event)" } + if ($allowed.Count -gt 0 -and $allowed -notcontains $evtStr) { + Write-Host "[WARN] Unknown event '$evtStr' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + + X "$indent" + foreach ($evt in $el.on) { + # Support both string ("OnChange") and object ({ "event": "OnChange", "callType": "After" }) + if ($evt -is [string] -or -not $evt.event) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + X "$indent`t$handler" + } else { + $evtName = "$($evt.event)" + $handler = if ($evt.handler) { "$($evt.handler)" } + elseif ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + $callTypeAttr = if ($evt.callType) { " callType=`"$($evt.callType)`"" } else { "" } + X "$indent`t$handler" + } + } + X "$indent" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent) + $id = New-Id + X "$indent<$tag name=`"$name`" id=`"$id`"/>" +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +function Emit-Title { + param($el, [string]$name, [string]$indent) + if ($el.title) { Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent } +} + +# --- Element emitters --- + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + $groupVal = "$($el.group)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + if ($groupVal -eq "collapsible") { X "$innerVertical"; X "$innerCollapsible" } + if ($el.representation) { + $repr = switch ("$($el.representation)") { "none" { "None" } "normal" { "NormalSeparation" } "weak" { "WeakSeparation" } "strong" { "StrongSeparation" } default { "$($el.representation)" } } + X "$inner$repr" + } + if ($el.showTitle -eq $false) { X "$innerfalse" } + if ($el.united -eq $false) { X "$innerfalse" } + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { "none" { "None" } "left" { "Left" } "right" { "Right" } "top" { "Top" } "bottom" { "Bottom" } default { "$($el.titleLocation)" } } + X "$inner$loc" + } + if ($el.multiLine -eq $true) { X "$innertrue" } + if ($el.passwordMode -eq $true) { X "$innertrue" } + if ($el.choiceButton -eq $false) { X "$innerfalse" } + if ($el.clearButton -eq $true) { X "$innertrue" } + if ($el.spinButton -eq $true) { X "$innertrue" } + if ($el.dropListButton -eq $true) { X "$innertrue" } + if ($el.markIncomplete -eq $true) { X "$innertrue" } + if ($el.skipOnInput -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.horizontalStretch -eq $true) { X "$innertrue" } + if ($el.verticalStretch -eq $true) { X "$innertrue" } + if ($el.inputHint) { Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$($el.titleLocation)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" + X "$indent" +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.title) { + $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } + X "$inner" + X "$inner`t<v8:item>" + X "$inner`t`t<v8:lang>ru</v8:lang>" + X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>" + X "$inner`t</v8:item>" + X "$inner" + } + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" + X "$indent" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.representation) { X "$inner$($el.representation)" } + if ($el.changeRowSet -eq $true) { X "$innertrue" } + if ($el.changeRowOrder -eq $true) { X "$innertrue" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + if ($el.commandBarLocation) { X "$inner$($el.commandBarLocation)" } + if ($el.searchStringLocation) { X "$inner$($el.searchStringLocation)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner + Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner + Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { Emit-Element -el $col -indent "$inner`t" } + X "$inner" + } + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.pagesRepresentation) { X "$inner$($el.pagesRepresentation)" } + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.group) { + $orientation = switch ("$($el.group)") { "horizontal" { "Horizontal" } "vertical" { "Vertical" } "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } default { $null } } + if ($orientation) { X "$inner$orientation" } + } + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.picture -or $el.src) { + $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } + X "$inner"; X "$inner`t$ref"; X "$inner`ttrue"; X "$inner" + } + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" + X "$indent" +} + +function Emit-CommandBarEl { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.autofill -eq $true) { X "$innertrue" } + Emit-CommonFlags -el $el -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.picture) { + X "$inner"; X "$inner`t$($el.picture)"; X "$inner`ttrue"; X "$inner" + } + if ($el.representation) { X "$inner$($el.representation)" } + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +# --- Element dispatcher --- + +function Emit-Element { + param($el, [string]$indent) + + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + if (-not $typeKey) { Write-Warning "Unknown element type, skipping"; return } + + # Validate known keys — warn about typos + $knownKeys = @{ + "group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + "name"=1;"path"=1;"title"=1 + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1 + "on"=1;"handlers"=1 + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + "hyperlink"=1;"showTitle"=1;"united"=1;"children"=1;"columns"=1 + "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1;"pagesRepresentation"=1 + "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 + "src"=1;"autofill"=1 + } + foreach ($p in $el.PSObject.Properties) { + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored." + } + } + + $name = Get-ElementName -el $el -typeKey $typeKey + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBarEl -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + } +} + +# === 6. Find element by name recursively === + +function Find-Element($startNode, [string]$targetName) { + foreach ($child in $startNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childName = $child.GetAttribute("name") + if ($childName -eq $targetName) { return $child } + $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ci) { + $found = Find-Element $ci $targetName + if ($found) { return $found } + } + } + return $null +} + +# === 7. Detect indent level of a container's children === + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + $text = $child.Value + if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($text -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + # Fallback: count depth from root + $depth = 0 + $current = $container + while ($current -and $current -ne $xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +# === 8. Insert node into container === + +function Insert-IntoContainer($container, $newNode, $afterName, $childIndent) { + $refNode = $null + + if ($afterName) { + # Find the after-element, then insert after it + $afterElem = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.GetAttribute("name") -eq $afterName) { + $afterElem = $child + break + } + } + if ($afterElem) { + $refNode = $afterElem.NextSibling + } else { + Write-Host "[WARN] after='$afterName' not found in target container, appending at end" + } + } + + if (-not $refNode) { + # Append at end: insert before trailing whitespace + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $refNode = $trailing + } + } + + $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $refNode) | Out-Null + } else { + # Container is empty (self-closing) — add framing whitespace + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# === 9. Generate fragment, parse, import nodes === + +$allNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema"' + +function Parse-Fragment([string]$xmlText) { + $fragDoc = New-Object System.Xml.XmlDocument + $fragDoc.PreserveWhitespace = $true + $fragDoc.LoadXml($xmlText) + return $fragDoc +} + +function Import-ElementNodes($fragDoc) { + $nodes = @() + foreach ($child in $fragDoc.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $xmlDoc.ImportNode($child, $true) + } + } + return $nodes +} + +# === 10. Add elements === + +$addedElems = @() +$companionCount = 0 + +if ($def.elements -and $def.elements.Count -gt 0) { + # Resolve target container + $targetCI = $null + $intoName = if ($def.into) { "$($def.into)" } else { $null } + $afterName = if ($def.after) { "$($def.after)" } else { $null } + + if ($intoName) { + $targetGroup = Find-Element $rootCI $intoName + if (-not $targetGroup) { + Write-Host "[ERROR] Target group '$intoName' not found" + exit 1 + } + $targetCI = $targetGroup.SelectSingleNode("f:ChildItems", $nsMgr) + if (-not $targetCI) { + # Create ChildItems for the group + $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) + $targetGroup.AppendChild($targetCI) | Out-Null + } + } elseif ($afterName) { + # Find the after element globally and use its parent as target + $afterElem = Find-Element $rootCI $afterName + if (-not $afterElem) { + Write-Host "[ERROR] Element '$afterName' not found" + exit 1 + } + $targetCI = $afterElem.ParentNode + } else { + $targetCI = $rootCI + } + + if (-not $targetCI) { + # Create ChildItems section in form — insert after Events or AutoCommandBar + $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) + $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) + if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($targetCI, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($targetCI) | Out-Null + } + # Also update $rootCI reference + $rootCI = $targetCI + } + + # Detect indent level + $childIndent = Get-ChildIndent $targetCI + + # Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения + # (рекурсивно по children/columns). + $elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup") + function Walk-ElemNames($el, [hashtable]$seen) { + $tk = $null + foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } } + if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' } + if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } } + if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } } + } + $dslElemNames = @{} + foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames } + + # Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется) + foreach ($el in $def.elements) { + $typeKey = $null + foreach ($key in $elemTypeKeys) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + if ($typeKey) { + $elName = Get-ElementName -el $el -typeKey $typeKey + $existing = Find-Element $rootCI $elName + if ($existing) { + Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique" + exit 1 + } + } + } + + # Remember starting element ID for companion counting + $startElemId = $script:nextElemId + + # Generate fragment + $script:xml = New-Object System.Text.StringBuilder 4096 + X "<_F $allNsDecl>" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent $childIndent + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedNodes = Import-ElementNodes $fragDoc + + # Count actual elements (non-companion) for reporting + foreach ($el in $def.elements) { + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + $name = Get-ElementName -el $el -typeKey $typeKey + $tagMap = @{ + "group"="Group"; "input"="Input"; "check"="Check"; "label"="Label"; "labelField"="LabelField" + "table"="Table"; "pages"="Pages"; "page"="Page"; "button"="Button" + "picture"="Picture"; "picField"="PicField"; "calendar"="Calendar"; "cmdBar"="CmdBar"; "popup"="Popup" + } + $pathStr = if ($el.path) { " -> $($el.path)" } else { "" } + $evtStr = if ($el.on) { " {$($el.on -join ', ')}" } else { "" } + $addedElems += " + [$($tagMap[$typeKey])] $name$pathStr$evtStr" + } + + # Insert each imported node + foreach ($node in $importedNodes) { + Insert-IntoContainer -container $targetCI -newNode $node -afterName $afterName -childIndent $childIndent + # Only use afterName for the first insertion; subsequent ones go after the previous + $afterName = $node.GetAttribute("name") + } + + $totalNewElemIds = $script:nextElemId - $startElemId + $companionCount = $totalNewElemIds - $def.elements.Count +} + +# === 11. Add attributes === + +$addedAttrs = @() + +if ($def.attributes -and $def.attributes.Count -gt 0) { + $attrsSection = $root.SelectSingleNode("f:Attributes", $nsMgr) + if (-not $attrsSection) { + # Create Attributes section — insert after ChildItems or after Events + $attrsSection = $xmlDoc.CreateElement("Attributes", $formNs) + # Find insertion point: after ChildItems or after the last pre-Attributes element + $insertAfter = $rootCI + if (-not $insertAfter) { + $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) + } + if (-not $insertAfter) { + $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($attrsSection, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($attrsSection) | Out-Null + } + } + + # Detect indent for attribute children + $attrChildIndent = Get-ChildIndent $attrsSection + if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" } + + # Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и + # против уже существующих реквизитов формы. + $dslAttrNames = @{} + foreach ($attr in $def.attributes) { + Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name' + if ($attr.columns) { + $dslColNames = @{} + foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" } + } + $existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr) + if ($existingAttr) { + Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique" + exit 1 + } + } + + # Generate attribute fragments + $script:xml = New-Object System.Text.StringBuilder 2048 + X "<_F $allNsDecl>" + foreach ($attr in $def.attributes) { + $attrId = New-AttrId + $attrName = "$($attr.name)" + X "$attrChildIndent" + $inner = "$attrChildIndent`t" + + if ($attr.title) { Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner } + if ($attr.type) { Emit-Type -typeStr "$($attr.type)" -indent $inner } else { X "$inner" } + if ($attr.main -eq $true) { X "$innertrue" } + if ($attr.savedData -eq $true) { X "$innertrue" } + if ($attr.fillChecking) { X "$inner$($attr.fillChecking)" } + + if ($attr.columns -and $attr.columns.Count -gt 0) { + X "$inner" + $colId = 1 + foreach ($col in $attr.columns) { + X "$inner`t" + if ($col.title) { Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" } + Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" + X "$inner`t" + $colId++ + } + X "$inner" + } + + X "$attrChildIndent" + $typeStr = if ($attr.type) { "$($attr.type)" } else { "(no type)" } + $addedAttrs += " + ${attrName}: $typeStr (id=$attrId)" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedAttrs = Import-ElementNodes $fragDoc + + foreach ($node in $importedAttrs) { + Insert-IntoContainer -container $attrsSection -newNode $node -afterName $null -childIndent $attrChildIndent + } +} + +# === 12. Add commands === + +$addedCmds = @() + +if ($def.commands -and $def.commands.Count -gt 0) { + $cmdsSection = $root.SelectSingleNode("f:Commands", $nsMgr) + if (-not $cmdsSection) { + # Create Commands section — insert after Parameters or Attributes + $cmdsSection = $xmlDoc.CreateElement("Commands", $formNs) + $insertAfter = $root.SelectSingleNode("f:Parameters", $nsMgr) + if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:Attributes", $nsMgr) } + if (-not $insertAfter) { $insertAfter = $rootCI } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($cmdsSection, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($cmdsSection) | Out-Null + } + } + + $cmdChildIndent = Get-ChildIndent $cmdsSection + if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" } + + # Уникальность имён команд: внутри JSON-определения и против существующих команд формы. + $dslCmdNames = @{} + foreach ($cmd in $def.commands) { + Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name' + $existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr) + if ($existingCmd) { + Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique" + exit 1 + } + } + + # Generate command fragments + $script:xml = New-Object System.Text.StringBuilder 1024 + X "<_F $allNsDecl>" + foreach ($cmd in $def.commands) { + $cmdId = New-CmdId + $cmdName = "$($cmd.name)" + X "$cmdChildIndent" + $inner = "$cmdChildIndent`t" + + if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner } + + # Support single action with optional callType, or multiple actions + if ($cmd.actions) { + # Multiple actions: [{ "callType": "Before", "handler": "..." }, ...] + foreach ($act in $cmd.actions) { + $actHandler = "$($act.handler)" + $callTypeAttr = if ($act.callType) { " callType=`"$($act.callType)`"" } else { "" } + X "$inner$actHandler" + } + } elseif ($cmd.action) { + $callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" } + X "$inner$($cmd.action)" + } + + if ($cmd.shortcut) { X "$inner$($cmd.shortcut)" } + if ($cmd.picture) { + X "$inner" + X "$inner`t$($cmd.picture)" + X "$inner`ttrue" + X "$inner" + } + if ($cmd.representation) { X "$inner$($cmd.representation)" } + + X "$cmdChildIndent" + $actionStr = if ($cmd.action) { " -> $($cmd.action)" } elseif ($cmd.actions) { " -> $($cmd.actions.Count) action(s)" } else { "" } + $addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedCmds = Import-ElementNodes $fragDoc + + foreach ($node in $importedCmds) { + Insert-IntoContainer -container $cmdsSection -newNode $node -afterName $null -childIndent $cmdChildIndent + } +} + +# === 12b. Add form-level events === + +$addedFormEvents = @() + +if ($def.formEvents -and $def.formEvents.Count -gt 0) { + $eventsSection = $root.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsSection) { + # Create Events section — insert after AutoCommandBar or at the beginning + $eventsSection = $xmlDoc.CreateElement("Events", $formNs) + $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + if ($insertAfter) { + # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) + $ws1 = $xmlDoc.CreateWhitespace("`r`n`t") + $ws2 = $xmlDoc.CreateWhitespace("`r`n`t") + if ($insertAfter.NextSibling) { + $root.InsertBefore($ws1, $insertAfter.NextSibling) | Out-Null + $root.InsertBefore($eventsSection, $ws1) | Out-Null + $root.InsertBefore($ws2, $eventsSection) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($eventsSection) | Out-Null + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n")) | Out-Null + } + } else { + $firstChild = $root.FirstChild + if ($firstChild) { + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($eventsSection, $firstChild) | Out-Null + $root.InsertBefore($ws, $eventsSection) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($eventsSection) | Out-Null + } + } + } + + $evtChildIndent = Get-ChildIndent $eventsSection + if (-not $evtChildIndent -or $evtChildIndent -eq "") { $evtChildIndent = "`t`t" } + + # Generate event fragments + $script:xml = New-Object System.Text.StringBuilder 512 + X "<_F $allNsDecl>" + foreach ($fe in $def.formEvents) { + $feName = "$($fe.name)" + $feHandler = "$($fe.handler)" + $callTypeAttr = if ($fe.callType) { " callType=`"$($fe.callType)`"" } else { "" } + X "$evtChildIndent$feHandler" + $ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" } + $addedFormEvents += " + $feName${ctStr} -> $feHandler" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedEvents = Import-ElementNodes $fragDoc + + foreach ($node in $importedEvents) { + Insert-IntoContainer -container $eventsSection -newNode $node -afterName $null -childIndent $evtChildIndent + } +} + +# === 12c. Add element-level events === + +$addedElemEvents = @() + +if ($def.elementEvents -and $def.elementEvents.Count -gt 0) { + if (-not $rootCI) { + $rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) + } + + foreach ($ee in $def.elementEvents) { + $targetName = "$($ee.element)" + $targetEl = Find-Element $rootCI $targetName + if (-not $targetEl) { + Write-Host "[WARN] Element '$targetName' not found — skipping elementEvent" + continue + } + + # Find or create Events element within the target + $targetEvents = $targetEl.SelectSingleNode("f:Events", $nsMgr) + if (-not $targetEvents) { + $targetEvents = $xmlDoc.CreateElement("Events", $formNs) + # Insert Events before closing tag (after last property, before ChildItems if any) + $ciNode = $targetEl.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ciNode) { + $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) + $targetEl.InsertBefore($ws, $ciNode) | Out-Null + $targetEl.InsertBefore($targetEvents, $ciNode) | Out-Null + } else { + $trailing = $targetEl.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl)) + $targetEl.InsertBefore($ws, $trailing) | Out-Null + $targetEl.InsertBefore($targetEvents, $trailing) | Out-Null + } else { + $targetEl.AppendChild($xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))) | Out-Null + $targetEl.AppendChild($targetEvents) | Out-Null + } + } + } + + $eeChildIndent = Get-ChildIndent $targetEvents + if (-not $eeChildIndent -or $eeChildIndent -eq "") { + $parentIndent = Get-ChildIndent $targetEl + $eeChildIndent = "$parentIndent`t" + } + + # Create Event element + $eeName = "$($ee.name)" + $eeHandler = "$($ee.handler)" + $callTypeAttr = if ($ee.callType) { " callType=`"$($ee.callType)`"" } else { "" } + + $script:xml = New-Object System.Text.StringBuilder 256 + X "<_F $allNsDecl>" + X "$eeChildIndent$eeHandler" + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedEE = Import-ElementNodes $fragDoc + + foreach ($node in $importedEE) { + Insert-IntoContainer -container $targetEvents -newNode $node -afterName $null -childIndent $eeChildIndent + } + + $ctStr = if ($ee.callType) { "[$($ee.callType)]" } else { "" } + $addedElemEvents += " + $targetName.$eeName${ctStr} -> $eeHandler" + } +} + +# === 13. Save === + +$content = $xmlDoc.OuterXml +# Ensure encoding declaration is uppercase UTF-8 +$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '' + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) + +# === 14. Summary === + +if ($script:isExtension) { + Write-Host "[EXTENSION] BaseForm detected — IDs start at 1000000+" + Write-Host "" +} + +if ($addedFormEvents.Count -gt 0) { + Write-Host "Added form events:" + foreach ($line in $addedFormEvents) { Write-Host $line } + Write-Host "" +} + +if ($addedElemEvents.Count -gt 0) { + Write-Host "Added element events:" + foreach ($line in $addedElemEvents) { Write-Host $line } + Write-Host "" +} + +if ($addedElems.Count -gt 0) { + $posStr = "" + if ($def.into) { $posStr += "into $($def.into)" } + if ($def.after) { if ($posStr) { $posStr += ", " }; $posStr += "after $($def.after)" } + if ($posStr) { $posStr = " ($posStr)" } + Write-Host "Added elements${posStr}:" + foreach ($line in $addedElems) { Write-Host $line } + Write-Host "" +} + +if ($addedAttrs.Count -gt 0) { + Write-Host "Added attributes:" + foreach ($line in $addedAttrs) { Write-Host $line } + Write-Host "" +} + +if ($addedCmds.Count -gt 0) { + Write-Host "Added commands:" + foreach ($line in $addedCmds) { Write-Host $line } + Write-Host "" +} + +Write-Host "---" +$totalParts = @() +if ($addedFormEvents.Count -gt 0) { $totalParts += "$($addedFormEvents.Count) form event(s)" } +if ($addedElemEvents.Count -gt 0) { $totalParts += "$($addedElemEvents.Count) element event(s)" } +if ($addedElems.Count -gt 0) { + $compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" } + $totalParts += "$($addedElems.Count) element(s)$compStr" +} +if ($addedAttrs.Count -gt 0) { $totalParts += "$($addedAttrs.Count) attribute(s)" } +if ($addedCmds.Count -gt 0) { $totalParts += "$($addedCmds.Count) command(s)" } +Write-Host "Total: $($totalParts -join ', ')" +Write-Host "Run /form-validate to verify." diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md index 1c106c34..3771074a 100644 --- a/.claude/skills/form-info/SKILL.md +++ b/.claude/skills/form-info/SKILL.md @@ -1,30 +1,30 @@ ---- -name: form-info -description: Анализ структуры управляемой формы 1С (Form.xml) — элементы, реквизиты, команды, события. Используй для понимания формы — при написании модуля формы, анализе обработчиков и элементов -argument-hint: -allowed-tools: - - Bash - - Read - - Glob ---- - -# /form-info — Компактная сводка формы - -Читает Form.xml и выводит дерево элементов, реквизиты с типами, команды, события. Заменяет чтение тысяч строк XML. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>" -``` - -## Параметры - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| FormPath | да | Путь к файлу Form.xml | -| Expand | нет | Раскрыть свёрнутую секцию по имени или title, `*` — все | -| Limit | нет | Макс. строк (по умолчанию 150) | -| Offset | нет | Пропустить N строк (пагинация) | - -Вывод самодокументирован. `[Group:AH]`/`[Group:AV]` = AlwaysHorizontal/AlwaysVertical. +--- +name: form-info +description: Анализ структуры управляемой формы 1С (Form.xml) — элементы, реквизиты, команды, события. Используй для понимания формы — при написании модуля формы, анализе обработчиков и элементов +argument-hint: +allowed-tools: + - Bash + - Read + - Glob +--- + +# /form-info — Компактная сводка формы + +Читает Form.xml и выводит дерево элементов, реквизиты с типами, команды, события. Заменяет чтение тысяч строк XML. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>" +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| FormPath | да | Путь к файлу Form.xml | +| Expand | нет | Раскрыть свёрнутую секцию по имени или title, `*` — все | +| Limit | нет | Макс. строк (по умолчанию 150) | +| Offset | нет | Пропустить N строк (пагинация) | + +Вывод самодокументирован. `[Group:AH]`/`[Group:AV]` = AlwaysHorizontal/AlwaysVertical. diff --git a/.claude/skills/form-patterns/SKILL.md b/.claude/skills/form-patterns/SKILL.md index 81d2a483..0d04ce38 100644 --- a/.claude/skills/form-patterns/SKILL.md +++ b/.claude/skills/form-patterns/SKILL.md @@ -1,253 +1,253 @@ ---- -name: form-patterns -description: Справочник паттернов компоновки управляемых форм 1С. Используй как справочник при проектировании форм — архетипы, конвенции, продвинутые приёмы -argument-hint: (no arguments) -allowed-tools: [] ---- - -# /form-patterns — паттерны компоновки форм - -Справочник типовых паттернов дизайна управляемых форм 1С. Вызывай **перед** проектированием формы через `/form-compile`, когда требования пользователя не детализируют расположение элементов. - -**Как использовать:** выбери подходящий архетип, применяй конвенции именования, при необходимости используй продвинутые паттерны. - ---- - -## Архетипы форм - -### Форма документа - -``` -Шапка (horizontal, 2 колонки) -├─ Левая (vertical): НомерДата (H: Номер + Дата "от"), Контрагент, Договор -├─ Правая (vertical): Организация, Подразделение, ЦеныИВалюта (надпись-ссылка) -Страницы (pages) -├─ Товары: таблица Объект.Товары -├─ Услуги: таблица Объект.Услуги (опционально) -└─ Дополнительно: прочие реквизиты -Подвал (vertical) -├─ Итоги (horizontal): Всего, НДС, Скидка -└─ КомментарийОтветственный (horizontal): Комментарий + Ответственный -``` - -**События:** OnCreateAtServer, OnReadAtServer, OnOpen, BeforeWriteAtServer, AfterWriteAtServer, AfterWrite, NotificationProcessing -**Свойства:** autoTitle=false - -### Форма обработки (DataProcessor) - -``` -Параметры (vertical) -├─ Группа полей ввода (Организация, Период, режимы работы) -├─ Информационные надписи (label, hyperlink) -Рабочая область -├─ Таблица данных или Pages с вкладками -Главная АКП формы (autoCmdBar) -├─ Выполнить / Применить (defaultButton: true) -└─ Закрыть (stdCommand: Close) -``` - -**События:** OnCreateAtServer, OnOpen, NotificationProcessing -**Свойства:** windowOpeningMode=LockOwnerWindow (если диалог), autoTitle=false - -### Форма списка - -``` -Отборы (group: alwaysHorizontal) -├─ ГруппаОтбор[Поле] (H): Флажок + Поле ввода (для каждого фильтра) -Список (table, DynamicList) -├─ Колонки: labelField (не input — данные только для чтения) -``` - -**События:** OnCreateAtServer, OnOpen, NotificationProcessing, OnLoadDataFromSettingsAtServer -**Свойства:** autoSaveDataInSettings=Use -**Фильтры:** пара реквизитов на каждый — `Отбор[Поле]` (значение) + `Отбор[Поле]Использование` (boolean) - -### Форма элемента справочника - -**Простая:** -``` -ГруппаРеквизитов (horizontal) -├─ Наименование -> Объект.Description -└─ Код -> Объект.Code (если нужен) -``` - -**Сложная:** -``` -Главное (vertical) -├─ Наименование -> Объект.Description -├─ Параметры (horizontal, 2 колонки) -│ ├─ Левая: основные реквизиты -│ └─ Правая: дополнительные реквизиты -└─ КонтактныеДанные / Дополнительно (vertical) -``` - -**События:** OnCreateAtServer, OnReadAtServer, BeforeWriteAtServer, NotificationProcessing - -### Мастер (Wizard) - -``` -Страницы (pages, OnCurrentPageChange) -├─ Шаг1: описание + параметры -├─ Шаг2: основная работа -└─ Шаг3: результат -Главная АКП формы (autoCmdBar) -├─ Назад, Далее (defaultButton: true), Выполнить -└─ Закрыть (stdCommand: Close) -``` - -**Свойства:** windowOpeningMode=LockOwnerWindow - ---- - -## Конвенции именования - -### Группы - -| Назначение | Имя | Тип | -|-----------|-----|-----| -| Шапка | `ГруппаШапка` | horizontal | -| Левая колонка | `ГруппаШапкаЛевая` | vertical | -| Правая колонка | `ГруппаШапкаПравая` | vertical | -| Номер+Дата | `ГруппаНомерДата` | horizontal | -| Подвал | `ГруппаПодвал` | vertical | -| Итоги | `ГруппаИтоги` | horizontal | -| Главная АКП формы | `ФормаКоманднаяПанель` | autoCmdBar | -| Страницы | `ГруппаСтраницы` / `Страницы` | pages | -| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false | -| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse | - -### Элементы - -| Назначение | Имя | -|-----------|-----| -| Поле в таблице | `[Таблица][Поле]` | -| Итог | `Итоги[Поле]` | -| Надпись-ссылка | `[Поле]Надпись` | -| Фильтр | `Отбор[Поле]` | -| Флажок фильтра | `Отбор[Поле]Использование` | -| Кнопка команды | `[Команда]Кнопка` | -| Баннер-картинка | `[Баннер]Картинка` | -| Баннер-надпись | `[Баннер]Надпись` | -| Подменю | `Подменю[Действие]` | - -### Обработчики событий - -Имя = имя элемента + суффикс на русском: - -| Событие | Суффикс | Пример | -|---------|---------|--------| -| OnChange | ПриИзменении | `ОрганизацияПриИзменении` | -| StartChoice | НачалоВыбора | `КонтрагентНачалоВыбора` | -| Click | Нажатие | `ЦеныИВалютаНажатие` | -| OnEditEnd | ПриОкончанииРедактирования | `ТоварыПриОкончанииРедактирования` | -| OnStartEdit | ПриНачалеРедактирования | `ТоварыПриНачалеРедактирования` | - -Обработчики формы: `ПриСозданииНаСервере`, `ПриОткрытии`, `ПередЗакрытием`, `ОбработкаОповещения`. - ---- - -## Принципы компоновки - -1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху. -2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение). -3. **Кнопки действий — на главной АКП формы** (`autoCmdBar`), не в отдельной группе на форме. Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя. -4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages. -5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly. -6. **Фильтры — отдельная зона.** Над списком, alwaysHorizontal, пара «флажок + поле» на каждый фильтр. -7. **Скрытые элементы для состояний.** Баннеры, предупреждения — `visible: false`, показываются программно. -8. **Надписи-ссылки для диалогов.** `labelField` с `hyperlink: true` и событием Click. - ---- - -## Продвинутые паттерны (ERP) - -### Сворачиваемые группы - -Для необязательных секций (подписи, дополнительно, прочее): - -```json -{ "group": "collapsible", "name": "ГруппаПодписи", "title": "Подписи", - "collapsed": true, "children": [...] } -``` - -### Баннер-предупреждение - -Группа «картинка + надпись», скрыта по умолчанию, показывается программно: - -```json -{ "group": "horizontal", "name": "ГруппаПредупреждение", "showTitle": false, - "visible": false, "children": [ - { "picture": "ПредупреждениеКартинка" }, - { "label": "ПредупреждениеНадпись", "title": "Текст", "maxWidth": 76, "autoMaxWidth": false } -]} -``` - -### Popup-меню в командной панели - -Группировка связанных команд (печать, отправка) в одну кнопку с иконкой: - -```json -{ "cmdBar": "КоманднаяПанель", "children": [ - { "popup": "ПодменюПечать", "title": "Печать", - "picture": "StdPicture.Print", "representation": "Picture", "children": [ - { "button": "ПечатьНакладная", "command": "Печать" }, - { "button": "ПечатьСчёт", "command": "ПечатьСчёт" } - ]} -]} -``` - -### Форма без стандартной командной панели - -Для модальных диалогов и мастеров: - -```json -{ "properties": { "commandBarLocation": "None", "windowOpeningMode": "LockWholeInterface" } } -``` - -### Надпись-гиперссылка - -Вместо кнопки для открытия подформ (ЦеныИВалюта, УчётнаяПолитика): - -```json -{ "labelField": "ЦеныИВалютаНадпись", "path": "ЦеныИВалюта", "hyperlink": true, "on": ["Click"] } -``` - ---- - -## Пример: форма обработки (полный DSL) - -```json -{ - "title": "Загрузка данных из CSV", - "properties": { "autoTitle": false, "windowOpeningMode": "LockOwnerWindow" }, - "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, - "elements": [ - { "group": "vertical", "name": "ГруппаПараметры", "children": [ - { "input": "ФайлЗагрузки", "path": "ФайлЗагрузки", "title": "Файл", "clearButton": true, "horizontalStretch": true, "on": ["StartChoice"] }, - { "input": "Кодировка", "path": "Кодировка" }, - { "input": "Разделитель", "path": "Разделитель", "title": "Разделитель колонок" } - ]}, - { "table": "Данные", "path": "Объект.Данные", "on": ["OnStartEdit"], "columns": [ - { "input": "ДанныеНомерСтроки", "path": "Объект.Данные.LineNumber", "readOnly": true, "title": "№" }, - { "input": "ДанныеНаименование", "path": "Объект.Данные.Наименование" }, - { "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] }, - { "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true } - ]}, - { "autoCmdBar": "ФормаКоманднаяПанель", "children": [ - { "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true }, - { "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" }, - { "button": "Закрыть", "stdCommand": "Close" } - ]} - ], - "attributes": [ - { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзCSV", "main": true }, - { "name": "ФайлЗагрузки", "type": "string" }, - { "name": "Кодировка", "type": "string(20)" }, - { "name": "Разделитель", "type": "string(5)" } - ], - "commands": [ - { "name": "Загрузить", "action": "ЗагрузитьОбработка" }, - { "name": "Очистить", "action": "ОчиститьОбработка" } - ] -} -``` +--- +name: form-patterns +description: Справочник паттернов компоновки управляемых форм 1С. Используй как справочник при проектировании форм — архетипы, конвенции, продвинутые приёмы +argument-hint: (no arguments) +allowed-tools: [] +--- + +# /form-patterns — паттерны компоновки форм + +Справочник типовых паттернов дизайна управляемых форм 1С. Вызывай **перед** проектированием формы через `/form-compile`, когда требования пользователя не детализируют расположение элементов. + +**Как использовать:** выбери подходящий архетип, применяй конвенции именования, при необходимости используй продвинутые паттерны. + +--- + +## Архетипы форм + +### Форма документа + +``` +Шапка (horizontal, 2 колонки) +├─ Левая (vertical): НомерДата (H: Номер + Дата "от"), Контрагент, Договор +├─ Правая (vertical): Организация, Подразделение, ЦеныИВалюта (надпись-ссылка) +Страницы (pages) +├─ Товары: таблица Объект.Товары +├─ Услуги: таблица Объект.Услуги (опционально) +└─ Дополнительно: прочие реквизиты +Подвал (vertical) +├─ Итоги (horizontal): Всего, НДС, Скидка +└─ КомментарийОтветственный (horizontal): Комментарий + Ответственный +``` + +**События:** OnCreateAtServer, OnReadAtServer, OnOpen, BeforeWriteAtServer, AfterWriteAtServer, AfterWrite, NotificationProcessing +**Свойства:** autoTitle=false + +### Форма обработки (DataProcessor) + +``` +Параметры (vertical) +├─ Группа полей ввода (Организация, Период, режимы работы) +├─ Информационные надписи (label, hyperlink) +Рабочая область +├─ Таблица данных или Pages с вкладками +Главная АКП формы (autoCmdBar) +├─ Выполнить / Применить (defaultButton: true) +└─ Закрыть (stdCommand: Close) +``` + +**События:** OnCreateAtServer, OnOpen, NotificationProcessing +**Свойства:** windowOpeningMode=LockOwnerWindow (если диалог), autoTitle=false + +### Форма списка + +``` +Отборы (group: alwaysHorizontal) +├─ ГруппаОтбор[Поле] (H): Флажок + Поле ввода (для каждого фильтра) +Список (table, DynamicList) +├─ Колонки: labelField (не input — данные только для чтения) +``` + +**События:** OnCreateAtServer, OnOpen, NotificationProcessing, OnLoadDataFromSettingsAtServer +**Свойства:** autoSaveDataInSettings=Use +**Фильтры:** пара реквизитов на каждый — `Отбор[Поле]` (значение) + `Отбор[Поле]Использование` (boolean) + +### Форма элемента справочника + +**Простая:** +``` +ГруппаРеквизитов (horizontal) +├─ Наименование -> Объект.Description +└─ Код -> Объект.Code (если нужен) +``` + +**Сложная:** +``` +Главное (vertical) +├─ Наименование -> Объект.Description +├─ Параметры (horizontal, 2 колонки) +│ ├─ Левая: основные реквизиты +│ └─ Правая: дополнительные реквизиты +└─ КонтактныеДанные / Дополнительно (vertical) +``` + +**События:** OnCreateAtServer, OnReadAtServer, BeforeWriteAtServer, NotificationProcessing + +### Мастер (Wizard) + +``` +Страницы (pages, OnCurrentPageChange) +├─ Шаг1: описание + параметры +├─ Шаг2: основная работа +└─ Шаг3: результат +Главная АКП формы (autoCmdBar) +├─ Назад, Далее (defaultButton: true), Выполнить +└─ Закрыть (stdCommand: Close) +``` + +**Свойства:** windowOpeningMode=LockOwnerWindow + +--- + +## Конвенции именования + +### Группы + +| Назначение | Имя | Тип | +|-----------|-----|-----| +| Шапка | `ГруппаШапка` | horizontal | +| Левая колонка | `ГруппаШапкаЛевая` | vertical | +| Правая колонка | `ГруппаШапкаПравая` | vertical | +| Номер+Дата | `ГруппаНомерДата` | horizontal | +| Подвал | `ГруппаПодвал` | vertical | +| Итоги | `ГруппаИтоги` | horizontal | +| Главная АКП формы | `ФормаКоманднаяПанель` | autoCmdBar | +| Страницы | `ГруппаСтраницы` / `Страницы` | pages | +| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false | +| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse | + +### Элементы + +| Назначение | Имя | +|-----------|-----| +| Поле в таблице | `[Таблица][Поле]` | +| Итог | `Итоги[Поле]` | +| Надпись-ссылка | `[Поле]Надпись` | +| Фильтр | `Отбор[Поле]` | +| Флажок фильтра | `Отбор[Поле]Использование` | +| Кнопка команды | `[Команда]Кнопка` | +| Баннер-картинка | `[Баннер]Картинка` | +| Баннер-надпись | `[Баннер]Надпись` | +| Подменю | `Подменю[Действие]` | + +### Обработчики событий + +Имя = имя элемента + суффикс на русском: + +| Событие | Суффикс | Пример | +|---------|---------|--------| +| OnChange | ПриИзменении | `ОрганизацияПриИзменении` | +| StartChoice | НачалоВыбора | `КонтрагентНачалоВыбора` | +| Click | Нажатие | `ЦеныИВалютаНажатие` | +| OnEditEnd | ПриОкончанииРедактирования | `ТоварыПриОкончанииРедактирования` | +| OnStartEdit | ПриНачалеРедактирования | `ТоварыПриНачалеРедактирования` | + +Обработчики формы: `ПриСозданииНаСервере`, `ПриОткрытии`, `ПередЗакрытием`, `ОбработкаОповещения`. + +--- + +## Принципы компоновки + +1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху. +2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение). +3. **Кнопки действий — на главной АКП формы** (`autoCmdBar`), не в отдельной группе на форме. Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя. +4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages. +5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly. +6. **Фильтры — отдельная зона.** Над списком, alwaysHorizontal, пара «флажок + поле» на каждый фильтр. +7. **Скрытые элементы для состояний.** Баннеры, предупреждения — `visible: false`, показываются программно. +8. **Надписи-ссылки для диалогов.** `labelField` с `hyperlink: true` и событием Click. + +--- + +## Продвинутые паттерны (ERP) + +### Сворачиваемые группы + +Для необязательных секций (подписи, дополнительно, прочее): + +```json +{ "group": "collapsible", "name": "ГруппаПодписи", "title": "Подписи", + "collapsed": true, "children": [...] } +``` + +### Баннер-предупреждение + +Группа «картинка + надпись», скрыта по умолчанию, показывается программно: + +```json +{ "group": "horizontal", "name": "ГруппаПредупреждение", "showTitle": false, + "visible": false, "children": [ + { "picture": "ПредупреждениеКартинка" }, + { "label": "ПредупреждениеНадпись", "title": "Текст", "maxWidth": 76, "autoMaxWidth": false } +]} +``` + +### Popup-меню в командной панели + +Группировка связанных команд (печать, отправка) в одну кнопку с иконкой: + +```json +{ "cmdBar": "КоманднаяПанель", "children": [ + { "popup": "ПодменюПечать", "title": "Печать", + "picture": "StdPicture.Print", "representation": "Picture", "children": [ + { "button": "ПечатьНакладная", "command": "Печать" }, + { "button": "ПечатьСчёт", "command": "ПечатьСчёт" } + ]} +]} +``` + +### Форма без стандартной командной панели + +Для модальных диалогов и мастеров: + +```json +{ "properties": { "commandBarLocation": "None", "windowOpeningMode": "LockWholeInterface" } } +``` + +### Надпись-гиперссылка + +Вместо кнопки для открытия подформ (ЦеныИВалюта, УчётнаяПолитика): + +```json +{ "labelField": "ЦеныИВалютаНадпись", "path": "ЦеныИВалюта", "hyperlink": true, "on": ["Click"] } +``` + +--- + +## Пример: форма обработки (полный DSL) + +```json +{ + "title": "Загрузка данных из CSV", + "properties": { "autoTitle": false, "windowOpeningMode": "LockOwnerWindow" }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "vertical", "name": "ГруппаПараметры", "children": [ + { "input": "ФайлЗагрузки", "path": "ФайлЗагрузки", "title": "Файл", "clearButton": true, "horizontalStretch": true, "on": ["StartChoice"] }, + { "input": "Кодировка", "path": "Кодировка" }, + { "input": "Разделитель", "path": "Разделитель", "title": "Разделитель колонок" } + ]}, + { "table": "Данные", "path": "Объект.Данные", "on": ["OnStartEdit"], "columns": [ + { "input": "ДанныеНомерСтроки", "path": "Объект.Данные.LineNumber", "readOnly": true, "title": "№" }, + { "input": "ДанныеНаименование", "path": "Объект.Данные.Наименование" }, + { "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] }, + { "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true } + ]}, + { "autoCmdBar": "ФормаКоманднаяПанель", "children": [ + { "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true }, + { "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзCSV", "main": true }, + { "name": "ФайлЗагрузки", "type": "string" }, + { "name": "Кодировка", "type": "string(20)" }, + { "name": "Разделитель", "type": "string(5)" } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка" }, + { "name": "Очистить", "action": "ОчиститьОбработка" } + ] +} +``` diff --git a/.claude/skills/form-remove/SKILL.md b/.claude/skills/form-remove/SKILL.md index 926089f9..f35c272c 100644 --- a/.claude/skills/form-remove/SKILL.md +++ b/.claude/skills/form-remove/SKILL.md @@ -1,47 +1,47 @@ ---- -name: form-remove -description: Удалить форму из объекта 1С (обработка, отчёт, справочник, документ и др.) -argument-hint: -disable-model-invocation: true -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /form-remove — Удаление формы - -Удаляет форму и убирает её регистрацию из корневого XML объекта. - -## Usage - -``` -/form-remove -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|------------|:------------:|--------------|-------------------------------------| -| ObjectName | да | — | Имя объекта | -| FormName | да | — | Имя формы для удаления | -| SrcDir | нет | `src` | Каталог исходников | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "" -FormName "" [-SrcDir ""] -``` - -## Что удаляется - -``` -//Forms/.xml # Метаданные формы -//Forms// # Каталог формы (рекурсивно) -``` - -## Что модифицируется - -- `/.xml` — убирается `` из `ChildObjects` -- Если удаляемая форма была DefaultForm — очищается значение DefaultForm +--- +name: form-remove +description: Удалить форму из объекта 1С (обработка, отчёт, справочник, документ и др.) +argument-hint: +disable-model-invocation: true +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /form-remove — Удаление формы + +Удаляет форму и убирает её регистрацию из корневого XML объекта. + +## Usage + +``` +/form-remove +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|-------------------------------------| +| ObjectName | да | — | Имя объекта | +| FormName | да | — | Имя формы для удаления | +| SrcDir | нет | `src` | Каталог исходников | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "" -FormName "" [-SrcDir ""] +``` + +## Что удаляется + +``` +//Forms/.xml # Метаданные формы +//Forms// # Каталог формы (рекурсивно) +``` + +## Что модифицируется + +- `/.xml` — убирается `` из `ChildObjects` +- Если удаляемая форма была DefaultForm — очищается значение DefaultForm diff --git a/.claude/skills/form-remove/scripts/remove-form.ps1 b/.claude/skills/form-remove/scripts/remove-form.ps1 index 8dd2c2a4..f3d66975 100644 --- a/.claude/skills/form-remove/scripts/remove-form.ps1 +++ b/.claude/skills/form-remove/scripts/remove-form.ps1 @@ -1,89 +1,89 @@ -# form-remove v1.2 — Remove form from 1C object -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias("ProcessorName")] - [string]$ObjectName, - - [Parameter(Mandatory)] - [string]$FormName, - - [string]$SrcDir = "src" -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 - -# --- Проверки --- - -$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml" -if (-not (Test-Path $rootXmlPath)) { - Write-Error "Корневой файл обработки не найден: $rootXmlPath" - exit 1 -} - -$processorDir = Join-Path $SrcDir $ObjectName -$formsDir = Join-Path $processorDir "Forms" -$formMetaPath = Join-Path $formsDir "$FormName.xml" -$formDir = Join-Path $formsDir $FormName - -if (-not (Test-Path $formMetaPath)) { - Write-Error "Метаданные формы не найдены: $formMetaPath" - exit 1 -} - -# --- Удаление файлов --- - -if (Test-Path $formDir) { - Remove-Item -Path $formDir -Recurse -Force - Write-Host "[OK] Удалён каталог: $formDir" -} - -Remove-Item -Path $formMetaPath -Force -Write-Host "[OK] Удалён файл: $formMetaPath" - -# --- Модификация корневого XML --- - -$rootXmlFull = Resolve-Path $rootXmlPath -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $true -$xmlDoc.Load($rootXmlFull.Path) - -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - -# Удалить FormName из ChildObjects -$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr) -foreach ($node in $formNodes) { - if ($node.InnerText -eq $FormName) { - $parent = $node.ParentNode - # Удалить предшествующий whitespace - $prev = $node.PreviousSibling - if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { - $parent.RemoveChild($prev) | Out-Null - } - $parent.RemoveChild($node) | Out-Null - break - } -} - -# Очистить DefaultForm если указывала на эту форму -$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr) -if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") { - $defaultForm.InnerText = "" -} - -# Сохранить с BOM -$encBom = New-Object System.Text.UTF8Encoding($true) -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = $encBom -$settings.Indent = $false - -$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create) -$writer = [System.Xml.XmlWriter]::Create($stream, $settings) -$xmlDoc.Save($writer) -$writer.Close() -$stream.Close() - -Write-Host "[OK] Форма $FormName удалена из $rootXmlPath" +# form-remove v1.2 — Remove form from 1C object +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias("ProcessorName")] + [string]$ObjectName, + + [Parameter(Mandatory)] + [string]$FormName, + + [string]$SrcDir = "src" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +# --- Проверки --- + +$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml" +if (-not (Test-Path $rootXmlPath)) { + Write-Error "Корневой файл обработки не найден: $rootXmlPath" + exit 1 +} + +$processorDir = Join-Path $SrcDir $ObjectName +$formsDir = Join-Path $processorDir "Forms" +$formMetaPath = Join-Path $formsDir "$FormName.xml" +$formDir = Join-Path $formsDir $FormName + +if (-not (Test-Path $formMetaPath)) { + Write-Error "Метаданные формы не найдены: $formMetaPath" + exit 1 +} + +# --- Удаление файлов --- + +if (Test-Path $formDir) { + Remove-Item -Path $formDir -Recurse -Force + Write-Host "[OK] Удалён каталог: $formDir" +} + +Remove-Item -Path $formMetaPath -Force +Write-Host "[OK] Удалён файл: $formMetaPath" + +# --- Модификация корневого XML --- + +$rootXmlFull = Resolve-Path $rootXmlPath +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($rootXmlFull.Path) + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + +# Удалить
FormName
из ChildObjects +$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr) +foreach ($node in $formNodes) { + if ($node.InnerText -eq $FormName) { + $parent = $node.ParentNode + # Удалить предшествующий whitespace + $prev = $node.PreviousSibling + if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $parent.RemoveChild($prev) | Out-Null + } + $parent.RemoveChild($node) | Out-Null + break + } +} + +# Очистить DefaultForm если указывала на эту форму +$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr) +if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") { + $defaultForm.InnerText = "" +} + +# Сохранить с BOM +$encBom = New-Object System.Text.UTF8Encoding($true) +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = $encBom +$settings.Indent = $false + +$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create) +$writer = [System.Xml.XmlWriter]::Create($stream, $settings) +$xmlDoc.Save($writer) +$writer.Close() +$stream.Close() + +Write-Host "[OK] Форма $FormName удалена из $rootXmlPath" diff --git a/.claude/skills/form-validate/SKILL.md b/.claude/skills/form-validate/SKILL.md index 32a9b716..b26d55cc 100644 --- a/.claude/skills/form-validate/SKILL.md +++ b/.claude/skills/form-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: form-validate -description: Валидация управляемой формы 1С. Используй после создания или модификации формы для проверки корректности. При наличии BaseForm автоматически проверяет callType и ID расширений -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /form-validate — валидация управляемой формы 1С - -Проверяет Form.xml на структурные ошибки: уникальность ID, наличие companion-элементов, корректность ссылок DataPath и команд. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|-----------|:-----:|---------|-----------------------------------------| -| FormPath | да | — | Путь к файлу Form.xml | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml" -``` - +--- +name: form-validate +description: Валидация управляемой формы 1С. Используй после создания или модификации формы для проверки корректности. При наличии BaseForm автоматически проверяет callType и ID расширений +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /form-validate — валидация управляемой формы 1С + +Проверяет Form.xml на структурные ошибки: уникальность ID, наличие companion-элементов, корректность ссылок DataPath и команд. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|-----------|:-----:|---------|-----------------------------------------| +| FormPath | да | — | Путь к файлу Form.xml | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml" +``` + diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index b6c9d9b6..bc147841 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -1,870 +1,870 @@ -# form-validate v1.8 — Validate 1C managed form -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$FormPath, - - [switch]$Detailed, - - [int]$MaxErrors = 30 -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- -# A: Directory → Ext/Form.xml -if (Test-Path $FormPath -PathType Container) { - $FormPath = Join-Path (Join-Path $FormPath "Ext") "Form.xml" -} -# B1: Missing Ext/ (e.g. Forms/Форма/Form.xml → Forms/Форма/Ext/Form.xml) -if (-not (Test-Path $FormPath)) { - $fn = [System.IO.Path]::GetFileName($FormPath) - if ($fn -eq "Form.xml") { - $c = Join-Path (Join-Path (Split-Path $FormPath) "Ext") $fn - if (Test-Path $c) { $FormPath = $c } - } -} -# B2: Descriptor (Forms/Форма.xml → Forms/Форма/Ext/Form.xml) -if (-not (Test-Path $FormPath) -and $FormPath.EndsWith(".xml")) { - $stem = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) - $dir = Split-Path $FormPath - $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Form.xml" - if (Test-Path $c) { $FormPath = $c } -} - -# --- Load XML --- - -if (-not (Test-Path $FormPath)) { - Write-Error "File not found: $FormPath" - exit 1 -} - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -try { - $xmlDoc.Load((Resolve-Path $FormPath).Path) -} catch { - Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" - Write-Host "" - Write-Host "---" - Write-Host "Errors: 1, Warnings: 0" - exit 1 -} - -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") -$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - -$root = $xmlDoc.DocumentElement - -# --- Detect context: config vs EPF/ERF --- -# Walk up from FormPath looking for Configuration.xml → config context -# No Configuration.xml → external data processor / report (EPF/ERF) -$script:isConfigContext = $false -$walkDir = Split-Path (Resolve-Path $FormPath) -Parent -for ($i = 0; $i -lt 15; $i++) { - if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break } - if (Test-Path (Join-Path $walkDir "Configuration.xml")) { - $script:isConfigContext = $true - break - } - $walkDir = Split-Path $walkDir -} - -# --- Counters --- - -$errors = 0 -$warnings = 0 -$stopped = $false -$script:okCount = 0 - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Write-Host "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Write-Host "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Write-Host "[WARN] $msg" -} - -# --- Form name from path --- - -$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) -$parentDir = [System.IO.Path]::GetDirectoryName($FormPath) -if ($parentDir) { - $extDir = [System.IO.Path]::GetFileName($parentDir) - if ($extDir -eq "Ext") { - $formDir = [System.IO.Path]::GetDirectoryName($parentDir) - if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } - } -} - -if ($Detailed) { - Write-Host "=== Validation: $formName ===" - Write-Host "" -} - -# Early BaseForm detection (used in Check 5 to skip base element DataPath validation) -$hasBaseForm = ($root.SelectSingleNode("f:BaseForm", $nsMgr) -ne $null) - -# --- Check 1: Root element and version --- - -if ($root.LocalName -ne "Form") { - Report-Error "Root element is '$($root.LocalName)', expected 'Form'" -} else { - $version = $root.GetAttribute("version") - if ($version -eq "2.17" -or $version -eq "2.20") { - Report-OK "Root element: Form version=$version" - } elseif ($version) { - Report-Warn "Form version='$version' (expected 2.17 or 2.20)" - } else { - Report-Warn "Form version attribute missing" - } -} - -# --- Check 2: AutoCommandBar --- - -if (-not $stopped) { - $acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) - if ($acb) { - $acbName = $acb.GetAttribute("name") - $acbId = $acb.GetAttribute("id") - if ($acbId -eq "-1") { - Report-OK "AutoCommandBar: name='$acbName', id=$acbId" - } else { - Report-Error "AutoCommandBar id='$acbId', expected '-1'" - } - } else { - Report-Error "AutoCommandBar element missing" - } -} - -# --- Collect all elements with IDs --- - -$elementIds = @{} # id -> name (element ID pool) -$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы) -$allElements = @() # @{Name; Tag; Id; ParentName; Node} - -function Collect-Elements { - param($node, [string]$parentName) - - foreach ($child in $node.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - - $name = $child.GetAttribute("name") - $id = $child.GetAttribute("id") - - if ($name -and $id) { - $tag = $child.LocalName - - $script:allElements += @{ - Name = $name - Tag = $tag - Id = $id - ParentName = $parentName - Node = $child - } - - # Track element IDs (skip AutoCommandBar which has -1) - if ($id -ne "-1") { - if ($elementIds.ContainsKey($id)) { - Report-Error "Duplicate element id=${id}: '$name' and '$($elementIds[$id])'" - } else { - $elementIds[$id] = $name - } - - # Имена элементов уникальны (требование 1С) - if ($elementNames.ContainsKey($name)) { - Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])" - } else { - $elementNames[$name] = $id - } - } - - # Recurse into ChildItems - $childItems = $child.SelectSingleNode("f:ChildItems", $nsMgr) - if ($childItems) { - Collect-Elements -node $childItems -parentName $name - } - } - } -} - -# Collect from ChildItems -$childItemsRoot = $root.SelectSingleNode("f:ChildItems", $nsMgr) -if ($childItemsRoot) { - Collect-Elements -node $childItemsRoot -parentName "(root)" -} - -# Also collect from AutoCommandBar's ChildItems -$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) -if ($acb) { - $acbChildren = $acb.SelectSingleNode("f:ChildItems", $nsMgr) - if ($acbChildren) { - Collect-Elements -node $acbChildren -parentName "ФормаКоманднаяПанель" - } -} - -# --- Check 3: Unique element IDs --- - -if (-not $stopped) { - $dupCount = ($allElements | Group-Object { $_.Id } | Where-Object { $_.Count -gt 1 -and $_.Name -ne "-1" }).Count - if ($dupCount -eq 0) { - Report-OK "Unique element IDs: $($elementIds.Count) elements" - } -} - -# --- Collect attributes (separate ID pool) --- - -$attrMap = @{} # name -> node -$attrIds = @{} # id -> name -$attrNodes = $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr) -foreach ($attr in $attrNodes) { - $attrName = $attr.GetAttribute("name") - $attrId = $attr.GetAttribute("id") - if ($attrName) { - # Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов) - if ($attrMap.ContainsKey($attrName)) { - Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))" - } - $attrMap[$attrName] = $attr - } - if ($attrId -and $attrId -ne "") { - if ($attrIds.ContainsKey($attrId)) { - Report-Error "Duplicate attribute id=${attrId}: '$attrName' and '$($attrIds[$attrId])'" - } else { - $attrIds[$attrId] = $attrName - } - } - - # Column IDs are a separate sub-pool per attribute — check uniqueness within parent - $colIds = @{} - $colNames = @{} # имена колонок уникальны в пределах своего реквизита - foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { - $colId = $col.GetAttribute("id") - $colName = $col.GetAttribute("name") - if ($colId -and $colId -ne "") { - if ($colIds.ContainsKey($colId)) { - Report-Error "Duplicate column id=${colId} in '$attrName': '$colName' and '$($colIds[$colId])'" - } else { - $colIds[$colId] = $colName - } - } - if ($colName) { - if ($colNames.ContainsKey($colName)) { - Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])" - } else { - $colNames[$colName] = $colId - } - } - } -} - -if (-not $stopped) { - $attrDupCount = ($attrIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count - if ($attrDupCount -eq 0 -and $attrIds.Count -gt 0) { - Report-OK "Unique attribute IDs: $($attrIds.Count) entries" - } -} - -# --- Collect commands (separate ID pool) --- - -$cmdMap = @{} # name -> node -$cmdIds = @{} # id -> name -$cmdNodes = $root.SelectNodes("f:Commands/f:Command", $nsMgr) -foreach ($cmd in $cmdNodes) { - $cmdName = $cmd.GetAttribute("name") - $cmdId = $cmd.GetAttribute("id") - if ($cmdName) { - # Имена команд уникальны среди команд (отдельный неймспейс) - if ($cmdMap.ContainsKey($cmdName)) { - Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))" - } - $cmdMap[$cmdName] = $cmd - } - if ($cmdId -and $cmdId -ne "") { - if ($cmdIds.ContainsKey($cmdId)) { - Report-Error "Duplicate command id=${cmdId}: '$cmdName' and '$($cmdIds[$cmdId])'" - } else { - $cmdIds[$cmdId] = $cmdName - } - } -} - -if (-not $stopped) { - if ($cmdIds.Count -gt 0) { - $cmdDupCount = ($cmdIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count - if ($cmdDupCount -eq 0) { - Report-OK "Unique command IDs: $($cmdIds.Count) entries" - } - } -} - -# --- Collect parameters (separate name pool, без id) --- - -$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров) -foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) { - $paramName = $param.GetAttribute("name") - if ($paramName) { - if ($paramNames.ContainsKey($paramName)) { - Report-Error "Duplicate parameter name '$paramName'" - } else { - $paramNames[$paramName] = $true - } - } -} - -# --- Check 4: Companion elements --- - -# Define required companions per element type -$companionRules = @{ - "InputField" = @("ContextMenu", "ExtendedTooltip") - "CheckBoxField" = @("ContextMenu", "ExtendedTooltip") - "LabelDecoration" = @("ContextMenu", "ExtendedTooltip") - "LabelField" = @("ContextMenu", "ExtendedTooltip") - "PictureDecoration" = @("ContextMenu", "ExtendedTooltip") - "PictureField" = @("ContextMenu", "ExtendedTooltip") - "CalendarField" = @("ContextMenu", "ExtendedTooltip") - "UsualGroup" = @("ExtendedTooltip") - "Pages" = @("ExtendedTooltip") - "Page" = @("ExtendedTooltip") - "Button" = @("ExtendedTooltip") - "Table" = @("ContextMenu", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition") -} - -if (-not $stopped) { - $companionErrors = 0 - $companionChecked = 0 - - foreach ($el in $allElements) { - if ($stopped) { break } - $tag = $el.Tag - $elName = $el.Name - $node = $el.Node - - if (-not $companionRules.ContainsKey($tag)) { continue } - - $required = $companionRules[$tag] - $companionChecked++ - - foreach ($compTag in $required) { - $compNode = $node.SelectSingleNode("f:$compTag", $nsMgr) - if (-not $compNode) { - Report-Error "[$tag] '$elName': missing companion <$compTag>" - $companionErrors++ - } - } - } - - if ($companionErrors -eq 0 -and $companionChecked -gt 0) { - Report-OK "Companion elements: $companionChecked elements checked" - } -} - -# --- Check 5: DataPath -> Attribute references --- - -if (-not $stopped) { - $pathErrors = 0 - $pathChecked = 0 - $pathBaseSkipped = 0 - - # All data-binding tags whose value is an attribute path (root must exist in ). - $bindingTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath', - 'MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath','MultipleValuePictureDataPath') - - foreach ($el in $allElements) { - if ($stopped) { break } - $tag = $el.Tag - $elName = $el.Name - $node = $el.Node - - # Skip companion elements - if ($tag -in @("ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition")) { - continue - } - - # In borrowed forms, skip DataPath check for base elements (id < 1000000) - if ($hasBaseForm -and $el.Id) { - try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {} - } - - foreach ($bTag in $bindingTags) { - if ($stopped) { break } - $dpNode = $node.SelectSingleNode("f:$bTag", $nsMgr) - if (-not $dpNode) { continue } - - $dataPath = $dpNode.InnerText.Trim() - if (-not $dataPath) { continue } - - # Opaque platform-internal shapes — not validatable from Form.xml alone: - # - bare numeric (e.g. "10", "1000003") — internal index - # - "N/M:" — metadata reference by UUID - if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') { - continue - } - - $pathChecked++ - - # Extract root segment of path, strip array indices like [0] - $cleanPath = $dataPath -replace '\[\d+\]', '' - # Strip leading '~' (current row of DynamicList: ~Список.Поле) - if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) } - $segments = $cleanPath -split '\.' - $rootAttr = $segments[0] - - # Resolve Items..CurrentData.... — table element, not attribute - if ($rootAttr -eq 'Items') { - if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') { - Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items..CurrentData.*" - continue - } - $tableName = $segments[1] - $tableEl = $null - foreach ($candidate in $allElements) { - if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) { - $tableEl = $candidate - break - } - } - if (-not $tableEl) { - Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found" - $pathErrors++ - continue - } - $tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr) - if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) { - # Table without DataPath — can't resolve further, accept silently - continue - } - $tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', '' - if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) } - $rootAttr = ($tableDp -split '\.')[0] - } - - if (-not $attrMap.ContainsKey($rootAttr)) { - Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found" - $pathErrors++ - } - } - } - - $pathMsg = "" - if ($pathChecked -gt 0) { $pathMsg = "$pathChecked paths checked" } - if ($pathBaseSkipped -gt 0) { - $skipNote = "$pathBaseSkipped base skipped" - $pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote } - } - if ($pathErrors -eq 0 -and $pathMsg) { - Report-OK "Data bindings: $pathMsg" - } elseif ($pathErrors -eq 0) { - Report-OK "Data bindings: none" - } -} - -# --- Check 6: Button command references --- - -if (-not $stopped) { - $cmdErrors = 0 - $cmdChecked = 0 - - foreach ($el in $allElements) { - if ($stopped) { break } - $tag = $el.Tag - $elName = $el.Name - $node = $el.Node - - if ($tag -ne "Button") { continue } - - $cmdNode = $node.SelectSingleNode("f:CommandName", $nsMgr) - if (-not $cmdNode) { continue } - - $cmdRef = $cmdNode.InnerText.Trim() - if (-not $cmdRef) { continue } - - # Form.Command.XXX -> check command XXX exists - if ($cmdRef -match '^Form\.Command\.(.+)$') { - $cmdName = $Matches[1] - $cmdChecked++ - if (-not $cmdMap.ContainsKey($cmdName)) { - Report-Error "[Button] '$elName': CommandName='$cmdRef' — command '$cmdName' not found in Commands" - $cmdErrors++ - } - } - # Form.StandardCommand.XXX — skip, standard commands always exist - } - - if ($cmdErrors -eq 0 -and $cmdChecked -gt 0) { - Report-OK "Command references: $cmdChecked buttons checked" - } elseif ($cmdChecked -eq 0) { - Report-OK "Command references: none" - } -} - -# --- Check 7: Events have handler names --- - -if (-not $stopped) { - $eventErrors = 0 - $eventChecked = 0 - - # Form-level events - $formEvents = $root.SelectSingleNode("f:Events", $nsMgr) - if ($formEvents) { - foreach ($evt in $formEvents.SelectNodes("f:Event", $nsMgr)) { - $evtName = $evt.GetAttribute("name") - $handler = $evt.InnerText.Trim() - $eventChecked++ - if (-not $handler) { - Report-Error "Form event '$evtName': empty handler name" - $eventErrors++ - } - } - } - - # Element-level events - foreach ($el in $allElements) { - if ($stopped) { break } - $tag = $el.Tag - $elName = $el.Name - $node = $el.Node - - $eventsNode = $node.SelectSingleNode("f:Events", $nsMgr) - if (-not $eventsNode) { continue } - - foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { - $evtName = $evt.GetAttribute("name") - $handler = $evt.InnerText.Trim() - $eventChecked++ - if (-not $handler) { - Report-Error "[$tag] '$elName' event '$evtName': empty handler name" - $eventErrors++ - } - } - } - - if ($eventErrors -eq 0 -and $eventChecked -gt 0) { - Report-OK "Event handlers: $eventChecked events checked" - } elseif ($eventChecked -eq 0) { - Report-OK "Event handlers: none" - } -} - -# --- Check 8: Command actions --- - -if (-not $stopped) { - $actionErrors = 0 - $actionChecked = 0 - - foreach ($cmd in $cmdNodes) { - if ($stopped) { break } - $cmdName = $cmd.GetAttribute("name") - $actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr) - $actionChecked++ - if (-not $actionNode -or -not $actionNode.InnerText.Trim()) { - Report-Error "Command '$cmdName': missing or empty Action" - $actionErrors++ - } - } - - if ($actionErrors -eq 0 -and $actionChecked -gt 0) { - Report-OK "Command actions: $actionChecked commands checked" - } elseif ($actionChecked -eq 0) { - Report-OK "Command actions: none" - } -} - -# --- Check 9: MainAttribute count --- - -if (-not $stopped) { - $mainCount = 0 - foreach ($attr in $attrNodes) { - $mainNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr) - if ($mainNode -and $mainNode.InnerText -eq "true") { - $mainCount++ - } - } - - if ($mainCount -le 1) { - $mainInfo = if ($mainCount -eq 1) { "1 main attribute" } else { "no main attribute" } - Report-OK "MainAttribute: $mainInfo" - } else { - Report-Error "Multiple MainAttribute=true ($mainCount found, expected 0 or 1)" - } -} - -# --- Check 10: Title must be multilingual XML (not plain text) --- - -if (-not $stopped) { - $titleNode = $root.SelectSingleNode("f:Title", $nsMgr) - if ($titleNode) { - $v8items = $titleNode.SelectNodes("v8:item", $nsMgr) - if ($v8items.Count -eq 0 -and $titleNode.InnerText.Trim() -ne "") { - Report-Error "Form Title is plain text ('$($titleNode.InnerText.Trim())') — must be multilingual XML (). Use top-level 'title' key in form-compile DSL." - } else { - Report-OK "Title: multilingual XML" - } - } -} - -# --- Check 11: Extension-specific validations --- - -$baseFormNode = $root.SelectSingleNode("f:BaseForm", $nsMgr) -$isExtension = ($baseFormNode -ne $null) - -if (-not $stopped -and $isExtension) { - # 11a. BaseForm version - $bfVersion = $baseFormNode.GetAttribute("version") - if ($bfVersion) { - Report-OK "BaseForm: version=$bfVersion" - } else { - Report-Warn "BaseForm: version attribute missing" - } - - # 11b. callType values validation (Before, After, Override) - $validCallTypes = @("Before", "After", "Override") - $ctErrors = 0 - $ctChecked = 0 - - # Check form-level events - $formEventsNode = $root.SelectSingleNode("f:Events", $nsMgr) - if ($formEventsNode) { - foreach ($evt in $formEventsNode.SelectNodes("f:Event", $nsMgr)) { - $ct = $evt.GetAttribute("callType") - if ($ct) { - $ctChecked++ - if ($validCallTypes -notcontains $ct) { - Report-Error "Form event '$($evt.GetAttribute('name'))': invalid callType='$ct' (expected: Before, After, Override)" - $ctErrors++ - } - } - } - } - - # Check element-level events - foreach ($el in $allElements) { - if ($stopped) { break } - $eventsNode = $el.Node.SelectSingleNode("f:Events", $nsMgr) - if (-not $eventsNode) { continue } - foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { - $ct = $evt.GetAttribute("callType") - if ($ct) { - $ctChecked++ - if ($validCallTypes -notcontains $ct) { - Report-Error "[$($el.Tag)] '$($el.Name)' event '$($evt.GetAttribute('name'))': invalid callType='$ct'" - $ctErrors++ - } - } - } - } - - # Check command actions - foreach ($cmd in $cmdNodes) { - if ($stopped) { break } - $cmdName = $cmd.GetAttribute("name") - foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { - $ct = $action.GetAttribute("callType") - if ($ct) { - $ctChecked++ - if ($validCallTypes -notcontains $ct) { - Report-Error "Command '$cmdName' Action: invalid callType='$ct'" - $ctErrors++ - } - } - } - } - - if (-not $stopped -and $ctErrors -eq 0 -and $ctChecked -gt 0) { - Report-OK "callType values: $ctChecked checked" - } - - # 11c. Extension ID ranges — warn if extension-added attrs/commands have id < 1000000 - # Collect BaseForm attribute names to distinguish added ones - $baseAttrNames = @{} - $baseCmdNames = @{} - $bfNs = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $bfNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") - foreach ($bAttr in $baseFormNode.SelectNodes("f:Attributes/f:Attribute", $bfNs)) { - $baName = $bAttr.GetAttribute("name") - if ($baName) { $baseAttrNames[$baName] = $true } - } - foreach ($bCmd in $baseFormNode.SelectNodes("f:Commands/f:Command", $bfNs)) { - $bcName = $bCmd.GetAttribute("name") - if ($bcName) { $baseCmdNames[$bcName] = $true } - } - - $idWarnCount = 0 - foreach ($attr in $attrNodes) { - $aName = $attr.GetAttribute("name") - $aId = $attr.GetAttribute("id") - if ($aName -and -not $baseAttrNames.ContainsKey($aName) -and $aId) { - try { - $intId = [int]$aId - if ($intId -lt 1000000) { - Report-Warn "Attribute '$aName' (id=$aId): extension-added attribute has id < 1000000" - $idWarnCount++ - } - } catch {} - } - } - - foreach ($cmd in $cmdNodes) { - $cName = $cmd.GetAttribute("name") - $cId = $cmd.GetAttribute("id") - if ($cName -and -not $baseCmdNames.ContainsKey($cName) -and $cId) { - try { - $intId = [int]$cId - if ($intId -lt 1000000) { - Report-Warn "Command '$cName' (id=$cId): extension-added command has id < 1000000" - $idWarnCount++ - } - } catch {} - } - } - - if (-not $stopped -and $idWarnCount -eq 0) { - $extAttrCount = ($attrNodes | Where-Object { -not $baseAttrNames.ContainsKey($_.GetAttribute("name")) }).Count - $extCmdCount = ($cmdNodes | Where-Object { -not $baseCmdNames.ContainsKey($_.GetAttribute("name")) }).Count - if (($extAttrCount + $extCmdCount) -gt 0) { - Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000" - } - } -} - -# Check callType without BaseForm (structural warning) -if (-not $stopped -and -not $isExtension) { - $callTypeWithoutBase = $false - $feNode = $root.SelectSingleNode("f:Events", $nsMgr) - if ($feNode) { - foreach ($evt in $feNode.SelectNodes("f:Event", $nsMgr)) { - if ($evt.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } - } - } - if (-not $callTypeWithoutBase) { - foreach ($cmd in $cmdNodes) { - foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { - if ($action.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } - } - if ($callTypeWithoutBase) { break } - } - } - if ($callTypeWithoutBase) { - Report-Warn "callType attributes found but no BaseForm — possible incorrect structure" - } -} - -# --- Check 12: Type values validation --- - -$knownInvalidTypes = @( - "FormDataStructure","FormDataCollection","FormDataTree","FormDataTreeItem","FormDataCollectionItem" - "FormGroup","FormField","FormButton","FormDecoration","FormTable" -) -$validClosedTypes = @( - "xs:boolean","xs:string","xs:decimal","xs:dateTime","xs:binary" - "v8:FillChecking","v8:Null","v8:StandardPeriod","v8:StandardBeginningDate","v8:Type" - "v8:TypeDescription","v8:UUID","v8:ValueListType","v8:ValueTable","v8:ValueTree" - "v8:Universal","v8:FixedArray","v8:FixedStructure" - "v8ui:Color","v8ui:Font","v8ui:FormattedString","v8ui:HorizontalAlign" - "v8ui:Picture","v8ui:SizeChangeMode","v8ui:VerticalAlign" - "dcsset:DataCompositionComparisonType","dcsset:DataCompositionFieldPlacement" - "dcsset:Filter","dcsset:SettingsComposer","dcsset:DataCompositionSettings" - "dcssch:DataCompositionSchema" - "dcscor:DataCompositionComparisonType","dcscor:DataCompositionGroupType" - "dcscor:DataCompositionPeriodAdditionType","dcscor:DataCompositionSortDirection","dcscor:Field" - "ent:AccountType","ent:AccumulationRecordType","ent:AccountingRecordType" -) -$validCfgPrefixes = @( - "AccountingRegisterRecordSet","AccumulationRegisterRecordSet" - "BusinessProcessObject","BusinessProcessRef" - "CatalogObject","CatalogRef" - "ChartOfAccountsObject","ChartOfAccountsRef" - "ChartOfCalculationTypesObject","ChartOfCalculationTypesRef" - "ChartOfCharacteristicTypesObject","ChartOfCharacteristicTypesRef" - "ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef" - "DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef" - "ExternalDataProcessorObject","ExternalReportObject" - "InformationRegisterRecordManager","InformationRegisterRecordSet" - "ReportObject","TaskObject","TaskRef" -) - -if (-not $stopped) { - $typeNodes = $root.SelectNodes("//v8:Type", $nsMgr) - $typeOk = $true - $typeChecked = 0 - $typeInvalid = 0 - foreach ($tn in $typeNodes) { - $tv = $tn.InnerText.Trim() - if (-not $tv) { continue } - $typeChecked++ - if ($tv -in $knownInvalidTypes) { - Report-Error "12. Type '$tv': invalid runtime/UI type (not valid in XDTO schema)" - $typeOk = $false; $typeInvalid++ - continue - } - if ($tv -in $validClosedTypes) { continue } - if ($tv -match '^cfg:(.+)$') { - $cfgVal = $Matches[1] - if ($cfgVal -eq "DynamicList") { continue } - if ($cfgVal -match '^([^.]+)\.') { - $pfx = $Matches[1] - if ($pfx -in $validCfgPrefixes) { - # ExternalDataProcessorObject/ExternalReportObject valid only for EPF/ERF, not config - if ($script:isConfigContext -and ($pfx -eq "ExternalDataProcessorObject" -or $pfx -eq "ExternalReportObject")) { - Report-Error "12. Type '$tv': External* type in configuration context (use DataProcessorObject/ReportObject instead)" - $typeOk = $false; $typeInvalid++ - } - continue - } - } - Report-Warn "12. Type '$tv': unrecognized cfg prefix" - $typeOk = $false - continue - } - if ($tv -match ':') { continue } - Report-Warn "12. Type '$tv': bare type without namespace prefix" - $typeOk = $false - } - if ($typeChecked -eq 0) { - Report-OK "12. Types: no type values to check" - } elseif ($typeOk) { - Report-OK "12. Types: $typeChecked values, all valid" - } -} - -# --- Summary --- - -$checks = $script:okCount + $errors + $warnings - -if ($errors -eq 0 -and $warnings -eq 0 -and -not $Detailed) { - Write-Host "=== Validation OK: Form.$formName ($checks checks) ===" -} else { - Write-Host "" - if ($Detailed) { - Write-Host "---" - Write-Host "Total: $($allElements.Count) elements, $($attrNodes.Count) attributes, $($cmdNodes.Count) commands" - } - - if ($stopped) { - Write-Host "Stopped after $MaxErrors errors. Fix and re-run." - } - - Write-Host "=== Result: $errors errors, $warnings warnings ($checks checks) ===" -} - -if ($errors -gt 0) { - exit 1 -} else { - exit 0 -} +# form-validate v1.8 — Validate 1C managed form +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$FormPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +# A: Directory → Ext/Form.xml +if (Test-Path $FormPath -PathType Container) { + $FormPath = Join-Path (Join-Path $FormPath "Ext") "Form.xml" +} +# B1: Missing Ext/ (e.g. Forms/Форма/Form.xml → Forms/Форма/Ext/Form.xml) +if (-not (Test-Path $FormPath)) { + $fn = [System.IO.Path]::GetFileName($FormPath) + if ($fn -eq "Form.xml") { + $c = Join-Path (Join-Path (Split-Path $FormPath) "Ext") $fn + if (Test-Path $c) { $FormPath = $c } + } +} +# B2: Descriptor (Forms/Форма.xml → Forms/Форма/Ext/Form.xml) +if (-not (Test-Path $FormPath) -and $FormPath.EndsWith(".xml")) { + $stem = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) + $dir = Split-Path $FormPath + $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Form.xml" + if (Test-Path $c) { $FormPath = $c } +} + +# --- Load XML --- + +if (-not (Test-Path $FormPath)) { + Write-Error "File not found: $FormPath" + exit 1 +} + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +try { + $xmlDoc.Load((Resolve-Path $FormPath).Path) +} catch { + Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" + Write-Host "" + Write-Host "---" + Write-Host "Errors: 1, Warnings: 0" + exit 1 +} + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") +$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + +$root = $xmlDoc.DocumentElement + +# --- Detect context: config vs EPF/ERF --- +# Walk up from FormPath looking for Configuration.xml → config context +# No Configuration.xml → external data processor / report (EPF/ERF) +$script:isConfigContext = $false +$walkDir = Split-Path (Resolve-Path $FormPath) -Parent +for ($i = 0; $i -lt 15; $i++) { + if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break } + if (Test-Path (Join-Path $walkDir "Configuration.xml")) { + $script:isConfigContext = $true + break + } + $walkDir = Split-Path $walkDir +} + +# --- Counters --- + +$errors = 0 +$warnings = 0 +$stopped = $false +$script:okCount = 0 + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Write-Host "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Write-Host "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Write-Host "[WARN] $msg" +} + +# --- Form name from path --- + +$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) +$parentDir = [System.IO.Path]::GetDirectoryName($FormPath) +if ($parentDir) { + $extDir = [System.IO.Path]::GetFileName($parentDir) + if ($extDir -eq "Ext") { + $formDir = [System.IO.Path]::GetDirectoryName($parentDir) + if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } + } +} + +if ($Detailed) { + Write-Host "=== Validation: $formName ===" + Write-Host "" +} + +# Early BaseForm detection (used in Check 5 to skip base element DataPath validation) +$hasBaseForm = ($root.SelectSingleNode("f:BaseForm", $nsMgr) -ne $null) + +# --- Check 1: Root element and version --- + +if ($root.LocalName -ne "Form") { + Report-Error "Root element is '$($root.LocalName)', expected 'Form'" +} else { + $version = $root.GetAttribute("version") + if ($version -eq "2.17" -or $version -eq "2.20") { + Report-OK "Root element: Form version=$version" + } elseif ($version) { + Report-Warn "Form version='$version' (expected 2.17 or 2.20)" + } else { + Report-Warn "Form version attribute missing" + } +} + +# --- Check 2: AutoCommandBar --- + +if (-not $stopped) { + $acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + if ($acb) { + $acbName = $acb.GetAttribute("name") + $acbId = $acb.GetAttribute("id") + if ($acbId -eq "-1") { + Report-OK "AutoCommandBar: name='$acbName', id=$acbId" + } else { + Report-Error "AutoCommandBar id='$acbId', expected '-1'" + } + } else { + Report-Error "AutoCommandBar element missing" + } +} + +# --- Collect all elements with IDs --- + +$elementIds = @{} # id -> name (element ID pool) +$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы) +$allElements = @() # @{Name; Tag; Id; ParentName; Node} + +function Collect-Elements { + param($node, [string]$parentName) + + foreach ($child in $node.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + + $name = $child.GetAttribute("name") + $id = $child.GetAttribute("id") + + if ($name -and $id) { + $tag = $child.LocalName + + $script:allElements += @{ + Name = $name + Tag = $tag + Id = $id + ParentName = $parentName + Node = $child + } + + # Track element IDs (skip AutoCommandBar which has -1) + if ($id -ne "-1") { + if ($elementIds.ContainsKey($id)) { + Report-Error "Duplicate element id=${id}: '$name' and '$($elementIds[$id])'" + } else { + $elementIds[$id] = $name + } + + # Имена элементов уникальны (требование 1С) + if ($elementNames.ContainsKey($name)) { + Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])" + } else { + $elementNames[$name] = $id + } + } + + # Recurse into ChildItems + $childItems = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($childItems) { + Collect-Elements -node $childItems -parentName $name + } + } + } +} + +# Collect from ChildItems +$childItemsRoot = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($childItemsRoot) { + Collect-Elements -node $childItemsRoot -parentName "(root)" +} + +# Also collect from AutoCommandBar's ChildItems +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $acbChildren = $acb.SelectSingleNode("f:ChildItems", $nsMgr) + if ($acbChildren) { + Collect-Elements -node $acbChildren -parentName "ФормаКоманднаяПанель" + } +} + +# --- Check 3: Unique element IDs --- + +if (-not $stopped) { + $dupCount = ($allElements | Group-Object { $_.Id } | Where-Object { $_.Count -gt 1 -and $_.Name -ne "-1" }).Count + if ($dupCount -eq 0) { + Report-OK "Unique element IDs: $($elementIds.Count) elements" + } +} + +# --- Collect attributes (separate ID pool) --- + +$attrMap = @{} # name -> node +$attrIds = @{} # id -> name +$attrNodes = $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr) +foreach ($attr in $attrNodes) { + $attrName = $attr.GetAttribute("name") + $attrId = $attr.GetAttribute("id") + if ($attrName) { + # Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов) + if ($attrMap.ContainsKey($attrName)) { + Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))" + } + $attrMap[$attrName] = $attr + } + if ($attrId -and $attrId -ne "") { + if ($attrIds.ContainsKey($attrId)) { + Report-Error "Duplicate attribute id=${attrId}: '$attrName' and '$($attrIds[$attrId])'" + } else { + $attrIds[$attrId] = $attrName + } + } + + # Column IDs are a separate sub-pool per attribute — check uniqueness within parent + $colIds = @{} + $colNames = @{} # имена колонок уникальны в пределах своего реквизита + foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { + $colId = $col.GetAttribute("id") + $colName = $col.GetAttribute("name") + if ($colId -and $colId -ne "") { + if ($colIds.ContainsKey($colId)) { + Report-Error "Duplicate column id=${colId} in '$attrName': '$colName' and '$($colIds[$colId])'" + } else { + $colIds[$colId] = $colName + } + } + if ($colName) { + if ($colNames.ContainsKey($colName)) { + Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])" + } else { + $colNames[$colName] = $colId + } + } + } +} + +if (-not $stopped) { + $attrDupCount = ($attrIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count + if ($attrDupCount -eq 0 -and $attrIds.Count -gt 0) { + Report-OK "Unique attribute IDs: $($attrIds.Count) entries" + } +} + +# --- Collect commands (separate ID pool) --- + +$cmdMap = @{} # name -> node +$cmdIds = @{} # id -> name +$cmdNodes = $root.SelectNodes("f:Commands/f:Command", $nsMgr) +foreach ($cmd in $cmdNodes) { + $cmdName = $cmd.GetAttribute("name") + $cmdId = $cmd.GetAttribute("id") + if ($cmdName) { + # Имена команд уникальны среди команд (отдельный неймспейс) + if ($cmdMap.ContainsKey($cmdName)) { + Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))" + } + $cmdMap[$cmdName] = $cmd + } + if ($cmdId -and $cmdId -ne "") { + if ($cmdIds.ContainsKey($cmdId)) { + Report-Error "Duplicate command id=${cmdId}: '$cmdName' and '$($cmdIds[$cmdId])'" + } else { + $cmdIds[$cmdId] = $cmdName + } + } +} + +if (-not $stopped) { + if ($cmdIds.Count -gt 0) { + $cmdDupCount = ($cmdIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count + if ($cmdDupCount -eq 0) { + Report-OK "Unique command IDs: $($cmdIds.Count) entries" + } + } +} + +# --- Collect parameters (separate name pool, без id) --- + +$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров) +foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) { + $paramName = $param.GetAttribute("name") + if ($paramName) { + if ($paramNames.ContainsKey($paramName)) { + Report-Error "Duplicate parameter name '$paramName'" + } else { + $paramNames[$paramName] = $true + } + } +} + +# --- Check 4: Companion elements --- + +# Define required companions per element type +$companionRules = @{ + "InputField" = @("ContextMenu", "ExtendedTooltip") + "CheckBoxField" = @("ContextMenu", "ExtendedTooltip") + "LabelDecoration" = @("ContextMenu", "ExtendedTooltip") + "LabelField" = @("ContextMenu", "ExtendedTooltip") + "PictureDecoration" = @("ContextMenu", "ExtendedTooltip") + "PictureField" = @("ContextMenu", "ExtendedTooltip") + "CalendarField" = @("ContextMenu", "ExtendedTooltip") + "UsualGroup" = @("ExtendedTooltip") + "Pages" = @("ExtendedTooltip") + "Page" = @("ExtendedTooltip") + "Button" = @("ExtendedTooltip") + "Table" = @("ContextMenu", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition") +} + +if (-not $stopped) { + $companionErrors = 0 + $companionChecked = 0 + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + if (-not $companionRules.ContainsKey($tag)) { continue } + + $required = $companionRules[$tag] + $companionChecked++ + + foreach ($compTag in $required) { + $compNode = $node.SelectSingleNode("f:$compTag", $nsMgr) + if (-not $compNode) { + Report-Error "[$tag] '$elName': missing companion <$compTag>" + $companionErrors++ + } + } + } + + if ($companionErrors -eq 0 -and $companionChecked -gt 0) { + Report-OK "Companion elements: $companionChecked elements checked" + } +} + +# --- Check 5: DataPath -> Attribute references --- + +if (-not $stopped) { + $pathErrors = 0 + $pathChecked = 0 + $pathBaseSkipped = 0 + + # All data-binding tags whose value is an attribute path (root must exist in ). + $bindingTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath', + 'MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath','MultipleValuePictureDataPath') + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + # Skip companion elements + if ($tag -in @("ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition")) { + continue + } + + # In borrowed forms, skip DataPath check for base elements (id < 1000000) + if ($hasBaseForm -and $el.Id) { + try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {} + } + + foreach ($bTag in $bindingTags) { + if ($stopped) { break } + $dpNode = $node.SelectSingleNode("f:$bTag", $nsMgr) + if (-not $dpNode) { continue } + + $dataPath = $dpNode.InnerText.Trim() + if (-not $dataPath) { continue } + + # Opaque platform-internal shapes — not validatable from Form.xml alone: + # - bare numeric (e.g. "10", "1000003") — internal index + # - "N/M:" — metadata reference by UUID + if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') { + continue + } + + $pathChecked++ + + # Extract root segment of path, strip array indices like [0] + $cleanPath = $dataPath -replace '\[\d+\]', '' + # Strip leading '~' (current row of DynamicList: ~Список.Поле) + if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) } + $segments = $cleanPath -split '\.' + $rootAttr = $segments[0] + + # Resolve Items..CurrentData.... — table element, not attribute + if ($rootAttr -eq 'Items') { + if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') { + Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.
.CurrentData.*" + continue + } + $tableName = $segments[1] + $tableEl = $null + foreach ($candidate in $allElements) { + if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) { + $tableEl = $candidate + break + } + } + if (-not $tableEl) { + Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found" + $pathErrors++ + continue + } + $tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr) + if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) { + # Table without DataPath — can't resolve further, accept silently + continue + } + $tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', '' + if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) } + $rootAttr = ($tableDp -split '\.')[0] + } + + if (-not $attrMap.ContainsKey($rootAttr)) { + Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found" + $pathErrors++ + } + } + } + + $pathMsg = "" + if ($pathChecked -gt 0) { $pathMsg = "$pathChecked paths checked" } + if ($pathBaseSkipped -gt 0) { + $skipNote = "$pathBaseSkipped base skipped" + $pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote } + } + if ($pathErrors -eq 0 -and $pathMsg) { + Report-OK "Data bindings: $pathMsg" + } elseif ($pathErrors -eq 0) { + Report-OK "Data bindings: none" + } +} + +# --- Check 6: Button command references --- + +if (-not $stopped) { + $cmdErrors = 0 + $cmdChecked = 0 + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + if ($tag -ne "Button") { continue } + + $cmdNode = $node.SelectSingleNode("f:CommandName", $nsMgr) + if (-not $cmdNode) { continue } + + $cmdRef = $cmdNode.InnerText.Trim() + if (-not $cmdRef) { continue } + + # Form.Command.XXX -> check command XXX exists + if ($cmdRef -match '^Form\.Command\.(.+)$') { + $cmdName = $Matches[1] + $cmdChecked++ + if (-not $cmdMap.ContainsKey($cmdName)) { + Report-Error "[Button] '$elName': CommandName='$cmdRef' — command '$cmdName' not found in Commands" + $cmdErrors++ + } + } + # Form.StandardCommand.XXX — skip, standard commands always exist + } + + if ($cmdErrors -eq 0 -and $cmdChecked -gt 0) { + Report-OK "Command references: $cmdChecked buttons checked" + } elseif ($cmdChecked -eq 0) { + Report-OK "Command references: none" + } +} + +# --- Check 7: Events have handler names --- + +if (-not $stopped) { + $eventErrors = 0 + $eventChecked = 0 + + # Form-level events + $formEvents = $root.SelectSingleNode("f:Events", $nsMgr) + if ($formEvents) { + foreach ($evt in $formEvents.SelectNodes("f:Event", $nsMgr)) { + $evtName = $evt.GetAttribute("name") + $handler = $evt.InnerText.Trim() + $eventChecked++ + if (-not $handler) { + Report-Error "Form event '$evtName': empty handler name" + $eventErrors++ + } + } + } + + # Element-level events + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + $eventsNode = $node.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsNode) { continue } + + foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { + $evtName = $evt.GetAttribute("name") + $handler = $evt.InnerText.Trim() + $eventChecked++ + if (-not $handler) { + Report-Error "[$tag] '$elName' event '$evtName': empty handler name" + $eventErrors++ + } + } + } + + if ($eventErrors -eq 0 -and $eventChecked -gt 0) { + Report-OK "Event handlers: $eventChecked events checked" + } elseif ($eventChecked -eq 0) { + Report-OK "Event handlers: none" + } +} + +# --- Check 8: Command actions --- + +if (-not $stopped) { + $actionErrors = 0 + $actionChecked = 0 + + foreach ($cmd in $cmdNodes) { + if ($stopped) { break } + $cmdName = $cmd.GetAttribute("name") + $actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr) + $actionChecked++ + if (-not $actionNode -or -not $actionNode.InnerText.Trim()) { + Report-Error "Command '$cmdName': missing or empty Action" + $actionErrors++ + } + } + + if ($actionErrors -eq 0 -and $actionChecked -gt 0) { + Report-OK "Command actions: $actionChecked commands checked" + } elseif ($actionChecked -eq 0) { + Report-OK "Command actions: none" + } +} + +# --- Check 9: MainAttribute count --- + +if (-not $stopped) { + $mainCount = 0 + foreach ($attr in $attrNodes) { + $mainNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr) + if ($mainNode -and $mainNode.InnerText -eq "true") { + $mainCount++ + } + } + + if ($mainCount -le 1) { + $mainInfo = if ($mainCount -eq 1) { "1 main attribute" } else { "no main attribute" } + Report-OK "MainAttribute: $mainInfo" + } else { + Report-Error "Multiple MainAttribute=true ($mainCount found, expected 0 or 1)" + } +} + +# --- Check 10: Title must be multilingual XML (not plain text) --- + +if (-not $stopped) { + $titleNode = $root.SelectSingleNode("f:Title", $nsMgr) + if ($titleNode) { + $v8items = $titleNode.SelectNodes("v8:item", $nsMgr) + if ($v8items.Count -eq 0 -and $titleNode.InnerText.Trim() -ne "") { + Report-Error "Form Title is plain text ('$($titleNode.InnerText.Trim())') — must be multilingual XML (). Use top-level 'title' key in form-compile DSL." + } else { + Report-OK "Title: multilingual XML" + } + } +} + +# --- Check 11: Extension-specific validations --- + +$baseFormNode = $root.SelectSingleNode("f:BaseForm", $nsMgr) +$isExtension = ($baseFormNode -ne $null) + +if (-not $stopped -and $isExtension) { + # 11a. BaseForm version + $bfVersion = $baseFormNode.GetAttribute("version") + if ($bfVersion) { + Report-OK "BaseForm: version=$bfVersion" + } else { + Report-Warn "BaseForm: version attribute missing" + } + + # 11b. callType values validation (Before, After, Override) + $validCallTypes = @("Before", "After", "Override") + $ctErrors = 0 + $ctChecked = 0 + + # Check form-level events + $formEventsNode = $root.SelectSingleNode("f:Events", $nsMgr) + if ($formEventsNode) { + foreach ($evt in $formEventsNode.SelectNodes("f:Event", $nsMgr)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "Form event '$($evt.GetAttribute('name'))': invalid callType='$ct' (expected: Before, After, Override)" + $ctErrors++ + } + } + } + } + + # Check element-level events + foreach ($el in $allElements) { + if ($stopped) { break } + $eventsNode = $el.Node.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsNode) { continue } + foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "[$($el.Tag)] '$($el.Name)' event '$($evt.GetAttribute('name'))': invalid callType='$ct'" + $ctErrors++ + } + } + } + } + + # Check command actions + foreach ($cmd in $cmdNodes) { + if ($stopped) { break } + $cmdName = $cmd.GetAttribute("name") + foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { + $ct = $action.GetAttribute("callType") + if ($ct) { + $ctChecked++ + if ($validCallTypes -notcontains $ct) { + Report-Error "Command '$cmdName' Action: invalid callType='$ct'" + $ctErrors++ + } + } + } + } + + if (-not $stopped -and $ctErrors -eq 0 -and $ctChecked -gt 0) { + Report-OK "callType values: $ctChecked checked" + } + + # 11c. Extension ID ranges — warn if extension-added attrs/commands have id < 1000000 + # Collect BaseForm attribute names to distinguish added ones + $baseAttrNames = @{} + $baseCmdNames = @{} + $bfNs = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $bfNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + foreach ($bAttr in $baseFormNode.SelectNodes("f:Attributes/f:Attribute", $bfNs)) { + $baName = $bAttr.GetAttribute("name") + if ($baName) { $baseAttrNames[$baName] = $true } + } + foreach ($bCmd in $baseFormNode.SelectNodes("f:Commands/f:Command", $bfNs)) { + $bcName = $bCmd.GetAttribute("name") + if ($bcName) { $baseCmdNames[$bcName] = $true } + } + + $idWarnCount = 0 + foreach ($attr in $attrNodes) { + $aName = $attr.GetAttribute("name") + $aId = $attr.GetAttribute("id") + if ($aName -and -not $baseAttrNames.ContainsKey($aName) -and $aId) { + try { + $intId = [int]$aId + if ($intId -lt 1000000) { + Report-Warn "Attribute '$aName' (id=$aId): extension-added attribute has id < 1000000" + $idWarnCount++ + } + } catch {} + } + } + + foreach ($cmd in $cmdNodes) { + $cName = $cmd.GetAttribute("name") + $cId = $cmd.GetAttribute("id") + if ($cName -and -not $baseCmdNames.ContainsKey($cName) -and $cId) { + try { + $intId = [int]$cId + if ($intId -lt 1000000) { + Report-Warn "Command '$cName' (id=$cId): extension-added command has id < 1000000" + $idWarnCount++ + } + } catch {} + } + } + + if (-not $stopped -and $idWarnCount -eq 0) { + $extAttrCount = ($attrNodes | Where-Object { -not $baseAttrNames.ContainsKey($_.GetAttribute("name")) }).Count + $extCmdCount = ($cmdNodes | Where-Object { -not $baseCmdNames.ContainsKey($_.GetAttribute("name")) }).Count + if (($extAttrCount + $extCmdCount) -gt 0) { + Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000" + } + } +} + +# Check callType without BaseForm (structural warning) +if (-not $stopped -and -not $isExtension) { + $callTypeWithoutBase = $false + $feNode = $root.SelectSingleNode("f:Events", $nsMgr) + if ($feNode) { + foreach ($evt in $feNode.SelectNodes("f:Event", $nsMgr)) { + if ($evt.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } + } + } + if (-not $callTypeWithoutBase) { + foreach ($cmd in $cmdNodes) { + foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) { + if ($action.GetAttribute("callType")) { $callTypeWithoutBase = $true; break } + } + if ($callTypeWithoutBase) { break } + } + } + if ($callTypeWithoutBase) { + Report-Warn "callType attributes found but no BaseForm — possible incorrect structure" + } +} + +# --- Check 12: Type values validation --- + +$knownInvalidTypes = @( + "FormDataStructure","FormDataCollection","FormDataTree","FormDataTreeItem","FormDataCollectionItem" + "FormGroup","FormField","FormButton","FormDecoration","FormTable" +) +$validClosedTypes = @( + "xs:boolean","xs:string","xs:decimal","xs:dateTime","xs:binary" + "v8:FillChecking","v8:Null","v8:StandardPeriod","v8:StandardBeginningDate","v8:Type" + "v8:TypeDescription","v8:UUID","v8:ValueListType","v8:ValueTable","v8:ValueTree" + "v8:Universal","v8:FixedArray","v8:FixedStructure" + "v8ui:Color","v8ui:Font","v8ui:FormattedString","v8ui:HorizontalAlign" + "v8ui:Picture","v8ui:SizeChangeMode","v8ui:VerticalAlign" + "dcsset:DataCompositionComparisonType","dcsset:DataCompositionFieldPlacement" + "dcsset:Filter","dcsset:SettingsComposer","dcsset:DataCompositionSettings" + "dcssch:DataCompositionSchema" + "dcscor:DataCompositionComparisonType","dcscor:DataCompositionGroupType" + "dcscor:DataCompositionPeriodAdditionType","dcscor:DataCompositionSortDirection","dcscor:Field" + "ent:AccountType","ent:AccumulationRecordType","ent:AccountingRecordType" +) +$validCfgPrefixes = @( + "AccountingRegisterRecordSet","AccumulationRegisterRecordSet" + "BusinessProcessObject","BusinessProcessRef" + "CatalogObject","CatalogRef" + "ChartOfAccountsObject","ChartOfAccountsRef" + "ChartOfCalculationTypesObject","ChartOfCalculationTypesRef" + "ChartOfCharacteristicTypesObject","ChartOfCharacteristicTypesRef" + "ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef" + "DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef" + "ExternalDataProcessorObject","ExternalReportObject" + "InformationRegisterRecordManager","InformationRegisterRecordSet" + "ReportObject","TaskObject","TaskRef" +) + +if (-not $stopped) { + $typeNodes = $root.SelectNodes("//v8:Type", $nsMgr) + $typeOk = $true + $typeChecked = 0 + $typeInvalid = 0 + foreach ($tn in $typeNodes) { + $tv = $tn.InnerText.Trim() + if (-not $tv) { continue } + $typeChecked++ + if ($tv -in $knownInvalidTypes) { + Report-Error "12. Type '$tv': invalid runtime/UI type (not valid in XDTO schema)" + $typeOk = $false; $typeInvalid++ + continue + } + if ($tv -in $validClosedTypes) { continue } + if ($tv -match '^cfg:(.+)$') { + $cfgVal = $Matches[1] + if ($cfgVal -eq "DynamicList") { continue } + if ($cfgVal -match '^([^.]+)\.') { + $pfx = $Matches[1] + if ($pfx -in $validCfgPrefixes) { + # ExternalDataProcessorObject/ExternalReportObject valid only for EPF/ERF, not config + if ($script:isConfigContext -and ($pfx -eq "ExternalDataProcessorObject" -or $pfx -eq "ExternalReportObject")) { + Report-Error "12. Type '$tv': External* type in configuration context (use DataProcessorObject/ReportObject instead)" + $typeOk = $false; $typeInvalid++ + } + continue + } + } + Report-Warn "12. Type '$tv': unrecognized cfg prefix" + $typeOk = $false + continue + } + if ($tv -match ':') { continue } + Report-Warn "12. Type '$tv': bare type without namespace prefix" + $typeOk = $false + } + if ($typeChecked -eq 0) { + Report-OK "12. Types: no type values to check" + } elseif ($typeOk) { + Report-OK "12. Types: $typeChecked values, all valid" + } +} + +# --- Summary --- + +$checks = $script:okCount + $errors + $warnings + +if ($errors -eq 0 -and $warnings -eq 0 -and -not $Detailed) { + Write-Host "=== Validation OK: Form.$formName ($checks checks) ===" +} else { + Write-Host "" + if ($Detailed) { + Write-Host "---" + Write-Host "Total: $($allElements.Count) elements, $($attrNodes.Count) attributes, $($cmdNodes.Count) commands" + } + + if ($stopped) { + Write-Host "Stopped after $MaxErrors errors. Fix and re-run." + } + + Write-Host "=== Result: $errors errors, $warnings warnings ($checks checks) ===" +} + +if ($errors -gt 0) { + exit 1 +} else { + exit 0 +} diff --git a/.claude/skills/help-add/SKILL.md b/.claude/skills/help-add/SKILL.md index 46b59630..ffde789c 100644 --- a/.claude/skills/help-add/SKILL.md +++ b/.claude/skills/help-add/SKILL.md @@ -1,44 +1,44 @@ ---- -name: help-add -description: Добавить встроенную справку к объекту 1С (обработка, отчёт, справочник, документ и др.). Используй когда пользователь просит добавить справку, help, встроенную помощь к объекту -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /help-add — Добавление справки - -Добавляет встроенную справку к объекту: файл метаданных `Help.xml`, HTML-страницу и при необходимости обновляет метаданные форм. - -## Usage - -``` -/help-add [Lang] [SrcDir] -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|------------|:------------:|--------------|-------------------------------------| -| ObjectName | да | — | Путь объекта относительно SrcDir (например `Catalogs/МойСправочник`, `DataProcessors/МояОбработка`) | -| Lang | нет | `ru` | Код языка справки | -| SrcDir | нет | `src` | Каталог исходников | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-help.ps1" -ObjectName "" [-Lang ""] [-SrcDir ""] -``` - -## Что делает скрипт - -- Создаёт `Ext/Help.xml` и `Ext/Help/ru.html` — шаблон справки -- Если у объекта есть формы — добавляет `` в метаданные форм (если отсутствует) -- Справка **не регистрируется** в `ChildObjects` — достаточно наличия файлов - -## После запуска - -Отредактируй `Ext/Help/ru.html` — наполни содержимым справки (стандартный HTML: `

`..`

`, `

`, `

    `, `

` и т.д.). Кнопка справки появится автоматически через `Autofill` в AutoCommandBar формы. +--- +name: help-add +description: Добавить встроенную справку к объекту 1С (обработка, отчёт, справочник, документ и др.). Используй когда пользователь просит добавить справку, help, встроенную помощь к объекту +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /help-add — Добавление справки + +Добавляет встроенную справку к объекту: файл метаданных `Help.xml`, HTML-страницу и при необходимости обновляет метаданные форм. + +## Usage + +``` +/help-add [Lang] [SrcDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|-------------------------------------| +| ObjectName | да | — | Путь объекта относительно SrcDir (например `Catalogs/МойСправочник`, `DataProcessors/МояОбработка`) | +| Lang | нет | `ru` | Код языка справки | +| SrcDir | нет | `src` | Каталог исходников | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-help.ps1" -ObjectName "" [-Lang ""] [-SrcDir ""] +``` + +## Что делает скрипт + +- Создаёт `Ext/Help.xml` и `Ext/Help/ru.html` — шаблон справки +- Если у объекта есть формы — добавляет `` в метаданные форм (если отсутствует) +- Справка **не регистрируется** в `ChildObjects` — достаточно наличия файлов + +## После запуска + +Отредактируй `Ext/Help/ru.html` — наполни содержимым справки (стандартный HTML: `

`..`

`, `

`, `

    `, `

` и т.д.). Кнопка справки появится автоматически через `Autofill` в AutoCommandBar формы. diff --git a/.claude/skills/help-add/scripts/add-help.ps1 b/.claude/skills/help-add/scripts/add-help.ps1 index 6f300ed6..df9a2ac8 100644 --- a/.claude/skills/help-add/scripts/add-help.ps1 +++ b/.claude/skills/help-add/scripts/add-help.ps1 @@ -1,259 +1,259 @@ -# help-add v1.7 — Add built-in help to 1C object -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$ObjectName, - - [string]$Lang = "ru", - - [string]$SrcDir = "src" -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -# --- Detect format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) - $head = $content.Substring(0, [Math]::Min(2000, $content.Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path - -# --- Проверки --- - -$objectDir = Join-Path $SrcDir $ObjectName -$extDir = Join-Path $objectDir "Ext" - -if (-not (Test-Path $extDir)) { - Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)." - exit 1 -} - -$helpXmlPath = Join-Path $extDir "Help.xml" -if (Test-Path $helpXmlPath) { - Write-Error "Справка уже существует: $helpXmlPath" - exit 1 -} - -Assert-EditAllowed $objectDir 'editable' - -# --- Кодировка --- - -$encBom = New-Object System.Text.UTF8Encoding($true) - -# --- 1. Help.xml --- - -$helpXml = @" - - - $Lang - -"@ - -[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom) - -# --- 2. Help/.html --- - -$helpDir = Join-Path $extDir "Help" -New-Item -ItemType Directory -Path $helpDir -Force | Out-Null - -$helpHtmlPath = Join-Path $helpDir "$Lang.html" - -$helpHtml = @" - - - - - - - -

$ObjectName

-

Описание.

- - -"@ - -[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom) - -# --- 3. Проверка IncludeHelpInContents в метаданных форм --- - -$formsDir = Join-Path $objectDir "Forms" -if (Test-Path $formsDir) { - $formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File - foreach ($formMeta in $formMetaFiles) { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $true - $xmlDoc.Load($formMeta.FullName) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr) - if (-not $includeHelp) { - # Добавить после - $formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr) - if ($formType) { - $newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses") - $newElem.InnerText = "false" - $parent = $formType.ParentNode - $nextSibling = $formType.NextSibling - # Вставить перенос + табуляцию + элемент - $ws = $xmlDoc.CreateWhitespace("`n`t`t`t") - if ($nextSibling) { - $parent.InsertBefore($ws, $nextSibling) | Out-Null - $parent.InsertBefore($newElem, $ws) | Out-Null - } else { - $parent.AppendChild($ws) | Out-Null - $parent.AppendChild($newElem) | Out-Null - } - - $settings = New-Object System.Xml.XmlWriterSettings - $settings.Encoding = $encBom - $settings.Indent = $false - $stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create) - $writer = [System.Xml.XmlWriter]::Create($stream, $settings) - $xmlDoc.Save($writer) - $writer.Close() - $stream.Close() - - Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)" - } - } - } -} - -Write-Host "[OK] Создана справка: $ObjectName" -Write-Host " Метаданные: $helpXmlPath" -Write-Host " Страница: $helpHtmlPath" +# help-add v1.7 — Add built-in help to 1C object +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ObjectName, + + [string]$Lang = "ru", + + [string]$SrcDir = "src" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +# --- Detect format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) + $head = $content.Substring(0, [Math]::Min(2000, $content.Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path + +# --- Проверки --- + +$objectDir = Join-Path $SrcDir $ObjectName +$extDir = Join-Path $objectDir "Ext" + +if (-not (Test-Path $extDir)) { + Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)." + exit 1 +} + +$helpXmlPath = Join-Path $extDir "Help.xml" +if (Test-Path $helpXmlPath) { + Write-Error "Справка уже существует: $helpXmlPath" + exit 1 +} + +Assert-EditAllowed $objectDir 'editable' + +# --- Кодировка --- + +$encBom = New-Object System.Text.UTF8Encoding($true) + +# --- 1. Help.xml --- + +$helpXml = @" + + + $Lang + +"@ + +[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom) + +# --- 2. Help/.html --- + +$helpDir = Join-Path $extDir "Help" +New-Item -ItemType Directory -Path $helpDir -Force | Out-Null + +$helpHtmlPath = Join-Path $helpDir "$Lang.html" + +$helpHtml = @" + + + + + + + +

$ObjectName

+

Описание.

+ + +"@ + +[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom) + +# --- 3. Проверка IncludeHelpInContents в метаданных форм --- + +$formsDir = Join-Path $objectDir "Forms" +if (Test-Path $formsDir) { + $formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File + foreach ($formMeta in $formMetaFiles) { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $true + $xmlDoc.Load($formMeta.FullName) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr) + if (-not $includeHelp) { + # Добавить после + $formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr) + if ($formType) { + $newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses") + $newElem.InnerText = "false" + $parent = $formType.ParentNode + $nextSibling = $formType.NextSibling + # Вставить перенос + табуляцию + элемент + $ws = $xmlDoc.CreateWhitespace("`n`t`t`t") + if ($nextSibling) { + $parent.InsertBefore($ws, $nextSibling) | Out-Null + $parent.InsertBefore($newElem, $ws) | Out-Null + } else { + $parent.AppendChild($ws) | Out-Null + $parent.AppendChild($newElem) | Out-Null + } + + $settings = New-Object System.Xml.XmlWriterSettings + $settings.Encoding = $encBom + $settings.Indent = $false + $stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create) + $writer = [System.Xml.XmlWriter]::Create($stream, $settings) + $xmlDoc.Save($writer) + $writer.Close() + $stream.Close() + + Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)" + } + } + } +} + +Write-Host "[OK] Создана справка: $ObjectName" +Write-Host " Метаданные: $helpXmlPath" +Write-Host " Страница: $helpHtmlPath" diff --git a/.claude/skills/img-grid/SKILL.md b/.claude/skills/img-grid/SKILL.md index 4f3cc2db..c5f0b53a 100644 --- a/.claude/skills/img-grid/SKILL.md +++ b/.claude/skills/img-grid/SKILL.md @@ -1,77 +1,77 @@ ---- -name: img-grid -description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа -argument-hint: [-c COLS] -allowed-tools: - - Bash - - Read ---- - -# /img-grid — Сетка для анализа макета - -Накладывает пронумерованную сетку на изображение печатной формы. Позволяет точно определить границы колонок, их пропорции и span-ы для генерации макета табличного документа. - -## Использование - -``` -/img-grid [-c COLS] [-o OUTPUT] -``` - -## Параметры - -| Параметр | Обязательный | По умолчанию | Описание | -|-----------|:------------:|--------------|-----------------------------------------------| -| ImagePath | да | — | Путь к изображению (PNG, JPG) | -| -c COLS | нет | 50 | Количество вертикальных делений | -| -r ROWS | нет | авто | Количество горизонтальных делений (авто = квадратные ячейки) | -| -o OUTPUT | нет | `-grid.` | Путь для результата | - -## Команда - -```bash -python "${CLAUDE_SKILL_DIR}/scripts/overlay-grid.py" "" [-c 50] [-o ""] -``` - -Требуется Python 3 с библиотекой Pillow (`pip install Pillow`). - -## Что делает - -1. Рисует полупрозрачные вертикальные (красные) и горизонтальные (синие) линии -2. Нумерует линии в отдельных полях сверху и слева (не перекрывает содержимое) -3. Каждая 5-я и 10-я линия выделены ярче для удобства счёта - -## Как использовать результат - -### 1. Определить границы колонок - -Посмотреть на изображение с сеткой и записать координаты вертикальных границ каждой колонки таблицы (в номерах grid-линий). - -### 2. Найти базовую решётку - -Если на форме несколько таблиц с разной раскладкой (например, шапка документа и основная таблица), объединить все граничные точки. Каждый сегмент между соседними границами — одна колонка MXL. - -Пример для М-11: -- Шапка: границы 0, 2, 4, 9, 14, 21, 28, 34, 40, 48 -- Таблица: границы 0, 2, 4, 11, 16, 19, 23, 28, 32, 36, 42, 48 -- Объединение: 0, 2, 4, 9, 11, 14, 16, 19, 21, 23, 28, 32, 34, 36, 40, 42, 48 -- Результат: **16 базовых колонок** с пропорциями 2, 2, 5, 2, 3, 2, 3, 2, 2, 5, 4, 2, 2, 4, 2, 6 - -### 3. Записать в JSON DSL - -```json -{ - "columns": 16, - "page": "A4-landscape", - "columnWidths": { - "1": "2x", "2": "2x", "3": "5x", "4": "2x", "5": "3x", - "6": "2x", "7": "3x", "8": "2x", "9": "2x", "10": "5x", - "11": "4x", "12": "2x", "13": "2x", "14": "4x", "15": "2x", "16": "6x" - } -} -``` - -Поле `"page"` позволяет компилятору автоматически вычислить абсолютные ширины из пропорций. - -### 4. Скомпилировать - -`/mxl-compile` → `/mxl-validate` → `/mxl-info` +--- +name: img-grid +description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа +argument-hint: [-c COLS] +allowed-tools: + - Bash + - Read +--- + +# /img-grid — Сетка для анализа макета + +Накладывает пронумерованную сетку на изображение печатной формы. Позволяет точно определить границы колонок, их пропорции и span-ы для генерации макета табличного документа. + +## Использование + +``` +/img-grid [-c COLS] [-o OUTPUT] +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|-----------------------------------------------| +| ImagePath | да | — | Путь к изображению (PNG, JPG) | +| -c COLS | нет | 50 | Количество вертикальных делений | +| -r ROWS | нет | авто | Количество горизонтальных делений (авто = квадратные ячейки) | +| -o OUTPUT | нет | `-grid.` | Путь для результата | + +## Команда + +```bash +python "${CLAUDE_SKILL_DIR}/scripts/overlay-grid.py" "" [-c 50] [-o ""] +``` + +Требуется Python 3 с библиотекой Pillow (`pip install Pillow`). + +## Что делает + +1. Рисует полупрозрачные вертикальные (красные) и горизонтальные (синие) линии +2. Нумерует линии в отдельных полях сверху и слева (не перекрывает содержимое) +3. Каждая 5-я и 10-я линия выделены ярче для удобства счёта + +## Как использовать результат + +### 1. Определить границы колонок + +Посмотреть на изображение с сеткой и записать координаты вертикальных границ каждой колонки таблицы (в номерах grid-линий). + +### 2. Найти базовую решётку + +Если на форме несколько таблиц с разной раскладкой (например, шапка документа и основная таблица), объединить все граничные точки. Каждый сегмент между соседними границами — одна колонка MXL. + +Пример для М-11: +- Шапка: границы 0, 2, 4, 9, 14, 21, 28, 34, 40, 48 +- Таблица: границы 0, 2, 4, 11, 16, 19, 23, 28, 32, 36, 42, 48 +- Объединение: 0, 2, 4, 9, 11, 14, 16, 19, 21, 23, 28, 32, 34, 36, 40, 42, 48 +- Результат: **16 базовых колонок** с пропорциями 2, 2, 5, 2, 3, 2, 3, 2, 2, 5, 4, 2, 2, 4, 2, 6 + +### 3. Записать в JSON DSL + +```json +{ + "columns": 16, + "page": "A4-landscape", + "columnWidths": { + "1": "2x", "2": "2x", "3": "5x", "4": "2x", "5": "3x", + "6": "2x", "7": "3x", "8": "2x", "9": "2x", "10": "5x", + "11": "4x", "12": "2x", "13": "2x", "14": "4x", "15": "2x", "16": "6x" + } +} +``` + +Поле `"page"` позволяет компилятору автоматически вычислить абсолютные ширины из пропорций. + +### 4. Скомпилировать + +`/mxl-compile` → `/mxl-validate` → `/mxl-info` diff --git a/.claude/skills/interface-edit/SKILL.md b/.claude/skills/interface-edit/SKILL.md index f395fea3..17b1bf02 100644 --- a/.claude/skills/interface-edit/SKILL.md +++ b/.claude/skills/interface-edit/SKILL.md @@ -1,75 +1,75 @@ ---- -name: interface-edit -description: Настройка командного интерфейса подсистемы 1С. Используй когда нужно скрыть или показать команды, разместить в группах, настроить порядок -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /interface-edit — редактирование CommandInterface.xml - -Точечное редактирование файла командного интерфейса подсистемы 1С. - -## Параметры - -| Параметр | Обяз. | Описание | -|----------|:-----:|----------| -| CIPath | да | Путь к CommandInterface.xml | -| Operation | нет | Операция: hide, show, place, order, subsystem-order, group-order | -| Value | нет | Значение для операции | -| DefinitionFile | нет | JSON-файл с массивом операций (альтернатива Operation) | -| CreateIfMissing | нет | Создать файл если не существует | -| NoValidate | нет | Пропустить авто-валидацию | - -## Команда - -### Inline mode - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '' -Operation hide -Value '' -``` - -### JSON mode - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '' -DefinitionFile '' -``` - -## Операции - -| Операция | Значение | Описание | -|----------|----------|----------| -| hide | Cmd.Name или массив | Скрыть команду (CommandsVisibility, false) | -| show | Cmd.Name или массив | Показать команду (visibility, true) | -| place | {"command":"...","group":"CommandGroup.X"} | Разместить команду в группе | -| order | {"group":"...","commands":[...]} | Задать порядок команд в группе | -| subsystem-order | ["Subsystem.X.Subsystem.A",...] | Порядок дочерних подсистем | -| group-order | ["NavigationPanelOrdinary",...] | Порядок групп | - -## Примеры - -```powershell -# Скрыть команду -... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList" - -# Показать команду -... -Operation show -Value "Report.Продажи.Command.Отчёт" - -# Разместить в группе -... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}' - -# Задать порядок подсистем -... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]' - -# Создать новый CI -... -CIPath -Operation subsystem-order -Value '[...]' -CreateIfMissing -``` - -## Верификация - -``` -/interface-validate -``` +--- +name: interface-edit +description: Настройка командного интерфейса подсистемы 1С. Используй когда нужно скрыть или показать команды, разместить в группах, настроить порядок +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /interface-edit — редактирование CommandInterface.xml + +Точечное редактирование файла командного интерфейса подсистемы 1С. + +## Параметры + +| Параметр | Обяз. | Описание | +|----------|:-----:|----------| +| CIPath | да | Путь к CommandInterface.xml | +| Operation | нет | Операция: hide, show, place, order, subsystem-order, group-order | +| Value | нет | Значение для операции | +| DefinitionFile | нет | JSON-файл с массивом операций (альтернатива Operation) | +| CreateIfMissing | нет | Создать файл если не существует | +| NoValidate | нет | Пропустить авто-валидацию | + +## Команда + +### Inline mode + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '' -Operation hide -Value '' +``` + +### JSON mode + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '' -DefinitionFile '' +``` + +## Операции + +| Операция | Значение | Описание | +|----------|----------|----------| +| hide | Cmd.Name или массив | Скрыть команду (CommandsVisibility, false) | +| show | Cmd.Name или массив | Показать команду (visibility, true) | +| place | {"command":"...","group":"CommandGroup.X"} | Разместить команду в группе | +| order | {"group":"...","commands":[...]} | Задать порядок команд в группе | +| subsystem-order | ["Subsystem.X.Subsystem.A",...] | Порядок дочерних подсистем | +| group-order | ["NavigationPanelOrdinary",...] | Порядок групп | + +## Примеры + +```powershell +# Скрыть команду +... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList" + +# Показать команду +... -Operation show -Value "Report.Продажи.Command.Отчёт" + +# Разместить в группе +... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}' + +# Задать порядок подсистем +... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]' + +# Создать новый CI +... -CIPath -Operation subsystem-order -Value '[...]' -CreateIfMissing +``` + +## Верификация + +``` +/interface-validate +``` diff --git a/.claude/skills/interface-edit/scripts/interface-edit.ps1 b/.claude/skills/interface-edit/scripts/interface-edit.ps1 index 63a57735..4cc77886 100644 --- a/.claude/skills/interface-edit/scripts/interface-edit.ps1 +++ b/.claude/skills/interface-edit/scripts/interface-edit.ps1 @@ -1,683 +1,683 @@ -# interface-edit v1.6 — Edit 1C CommandInterface.xml -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][Alias('Path')][string]$CIPath, - [string]$DefinitionFile, - [ValidateSet("hide","show","place","order","subsystem-order","group-order")] - [string]$Operation, - [string]$Value, - [switch]$CreateIfMissing, - [switch]$NoValidate -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Mode validation --- -if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } -if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($CIPath)) { - $CIPath = Join-Path (Get-Location).Path $CIPath -} -$resolvedPath = $CIPath - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. Walk-up from Subsystems/X/Ext/ -# CommandInterface.xml reaches Subsystems/X.xml (owning subsystem uuid). -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $CIPath 'editable' - -# --- Detect format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($CIPath)) - -# --- Namespaces --- -$script:ciNs = "http://v8.1c.ru/8.3/xcf/extrnprops" -$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" -$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" -$script:xsNs = "http://www.w3.org/2001/XMLSchema" - -# --- Create if missing --- -if (-not (Test-Path $CIPath)) { - if ($CreateIfMissing) { - $parentDir = [System.IO.Path]::GetDirectoryName($CIPath) - if (-not (Test-Path $parentDir)) { - New-Item -ItemType Directory -Path $parentDir -Force | Out-Null - } - $emptyCI = @" - - - -"@ - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom) - Write-Host "[INFO] Created new CommandInterface.xml: $CIPath" - } else { - Write-Error "File not found: $CIPath (use -CreateIfMissing to create)" - exit 1 - } -} -$resolvedPath = (Resolve-Path $CIPath).Path - -# --- Load XML --- -$script:xmlDoc = New-Object System.Xml.XmlDocument -$script:xmlDoc.PreserveWhitespace = $true -$script:xmlDoc.Load($resolvedPath) - -$script:addCount = 0 -$script:removeCount = 0 -$script:modifyCount = 0 - -function Info([string]$msg) { Write-Host "[INFO] $msg" } -function Warn([string]$msg) { Write-Host "[WARN] $msg" } - -# --- Detect structure --- -$root = $script:xmlDoc.DocumentElement -if ($root.LocalName -ne "CommandInterface") { - Write-Error "Expected root element, got <$($root.LocalName)>" - exit 1 -} - -# Section canonical order -$script:sectionOrder = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder") - -# --- XML manipulation helpers --- -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - $depth = 0; $current = $container - while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Remove-NodeWithWhitespace($node) { - $parent = $node.ParentNode - $prev = $node.PreviousSibling - $next = $node.NextSibling - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($prev) | Out-Null - } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($next) | Out-Null - } - $parent.RemoveChild($node) | Out-Null -} - -function Import-CIFragment([string]$xmlString) { - $wrapper = "<_W xmlns=`"$($script:ciNs)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:xs=`"$($script:xsNs)`">$xmlString" - $frag = New-Object System.Xml.XmlDocument - $frag.PreserveWhitespace = $true - $frag.LoadXml($wrapper) - $nodes = @() - foreach ($child in $frag.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $script:xmlDoc.ImportNode($child, $true) - } - } - return ,$nodes -} - -# --- Ensure section exists, creating it in correct order if needed --- -function Ensure-Section([string]$sectionName) { - # Find existing - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $sectionName) { - return $child - } - } - - # Create new section - $newSection = $script:xmlDoc.CreateElement($sectionName, $script:ciNs) - - # Find the correct insertion point: before the first section that comes AFTER us in canonical order - $myIdx = [array]::IndexOf($script:sectionOrder, $sectionName) - $refNode = $null - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childIdx = [array]::IndexOf($script:sectionOrder, $child.LocalName) - if ($childIdx -gt $myIdx) { - # Find the whitespace before this element to insert before it - $prev = $child.PreviousSibling - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $refNode = $prev - } else { - $refNode = $child - } - break - } - } - - $rootIndent = Get-ChildIndent $root - # Add closing whitespace inside the new section - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent") - $newSection.AppendChild($closeWs) | Out-Null - - if ($refNode) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent") - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($newSection, $ws) | Out-Null - } else { - Insert-BeforeElement $root $newSection $null $rootIndent - } - return $newSection -} - -# --- Parse value: string or JSON array --- -function Parse-ValueList([string]$val) { - $val = $val.Trim() - if ($val.StartsWith("[")) { - $arr = $val | ConvertFrom-Json - $result = @(); foreach ($item in $arr) { $result += "$item" } - return ,$result - } - return @($val) -} - -# --- Find Command element by name in a section --- -function Find-CommandByName($section, [string]$cmdName) { - foreach ($child in $section.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Command") { - if ($child.GetAttribute("name") -eq $cmdName) { return $child } - } - } - return $null -} - -# --- Command name normalization (plural/Russian type prefix → singular English) --- -$script:typeNormMap = @{ - "Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "Constants"="Constant" - "Reports"="Report"; "DataProcessors"="DataProcessor" - "InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister" - "AccountingRegisters"="AccountingRegister"; "CalculationRegisters"="CalculationRegister" - "ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes" - "ChartsOfCalculationTypes"="ChartOfCalculationTypes" - "BusinessProcesses"="BusinessProcess"; "Tasks"="Task" - "ExchangePlans"="ExchangePlan"; "DocumentJournals"="DocumentJournal" - "CommonModules"="CommonModule"; "CommonCommands"="CommonCommand" - "CommonForms"="CommonForm"; "CommonPictures"="CommonPicture" - "CommonTemplates"="CommonTemplate"; "CommonAttributes"="CommonAttribute" - "CommandGroups"="CommandGroup"; "Roles"="Role" - "Subsystems"="Subsystem"; "StyleItems"="StyleItem" - # Russian singular - "Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum" - "Константа"="Constant"; "Отчёт"="Report"; "Отчет"="Report"; "Обработка"="DataProcessor" - "РегистрСведений"="InformationRegister"; "РегистрНакопления"="AccumulationRegister" - "РегистрБухгалтерии"="AccountingRegister" - "ПланСчетов"="ChartOfAccounts"; "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" - "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" - "ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal" - "ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand" - "ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem" - # Russian plural - "Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum" - "Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor" - "РегистрыСведений"="InformationRegister"; "РегистрыНакопления"="AccumulationRegister" - "РегистрыБухгалтерии"="AccountingRegister" - "ПланыСчетов"="ChartOfAccounts"; "ПланыВидовХарактеристик"="ChartOfCharacteristicTypes" - "БизнесПроцессы"="BusinessProcess"; "Задачи"="Task" - "ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal" - "Подсистемы"="Subsystem" -} - -function Normalize-CmdName([string]$name) { - if (-not $name -or -not $name.Contains('.')) { return $name } - $dotIdx = $name.IndexOf('.') - $first = $name.Substring(0, $dotIdx) - $rest = $name.Substring($dotIdx) - if ($script:typeNormMap.ContainsKey($first)) { - $normalized = "$($script:typeNormMap[$first])$rest" - if ($normalized -ne $name) { Write-Host "[NORM] Command: $name -> $normalized" } - return $normalized - } - return $name -} - -# --- Operations --- - -function Do-Hide([string[]]$commands) { - $commands = @($commands | ForEach-Object { Normalize-CmdName $_ }) - $section = Ensure-Section "CommandsVisibility" - $sectionIndent = Get-ChildIndent $section - - foreach ($cmd in $commands) { - $existing = Find-CommandByName $section $cmd - if ($existing) { - # Check if already false - $commonEl = $null - foreach ($vis in $existing.ChildNodes) { - if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") { - foreach ($c in $vis.ChildNodes) { - if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break } - } - } - } - if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") { - Warn "Already hidden: $cmd" - continue - } - # Change true -> false - if ($commonEl) { - $commonEl.InnerText = "false" - $script:modifyCount++ - Info "Changed to hidden: $cmd" - continue - } - } - # Add new entry - $fragXml = "false" - $nodes = Import-CIFragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $section $nodes[0] $null $sectionIndent - $script:addCount++ - Info "Hidden: $cmd" - } - } -} - -function Do-Show([string[]]$commands) { - $commands = @($commands | ForEach-Object { Normalize-CmdName $_ }) - $section = $null - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandsVisibility") { - $section = $child; break - } - } - - foreach ($cmd in $commands) { - if (-not $section) { - # No CommandsVisibility section — showing means adding with true - $section = Ensure-Section "CommandsVisibility" - } - $existing = Find-CommandByName $section $cmd - if ($existing) { - $commonEl = $null - foreach ($vis in $existing.ChildNodes) { - if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") { - foreach ($c in $vis.ChildNodes) { - if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break } - } - } - } - if ($commonEl -and $commonEl.InnerText.Trim() -eq "true") { - Warn "Already shown: $cmd" - continue - } - if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") { - # Change false -> true - $commonEl.InnerText = "true" - $script:modifyCount++ - Info "Changed to shown: $cmd" - continue - } - } - # Add new entry with true - $sectionIndent = Get-ChildIndent $section - $fragXml = "true" - $nodes = Import-CIFragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $section $nodes[0] $null $sectionIndent - $script:addCount++ - Info "Shown: $cmd" - } - } -} - -function Do-Place([string]$jsonVal) { - $def = $jsonVal | ConvertFrom-Json - $cmdName = Normalize-CmdName "$($def.command)" - $groupName = "$($def.group)" - if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 } - - $section = Ensure-Section "CommandsPlacement" - $sectionIndent = Get-ChildIndent $section - - # Check existing - $existing = Find-CommandByName $section $cmdName - if ($existing) { - # Update group - foreach ($child in $existing.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandGroup") { - $child.InnerText = $groupName - $script:modifyCount++ - Info "Updated placement: $cmdName -> $groupName" - return - } - } - } - - # Add new - $fragXml = "$groupNameAuto" - $nodes = Import-CIFragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $section $nodes[0] $null $sectionIndent - $script:addCount++ - Info "Placed: $cmdName -> $groupName" - } -} - -function Do-Order([string]$jsonVal) { - $def = $jsonVal | ConvertFrom-Json - $groupName = "$($def.group)" - $commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" }) - if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 } - - $section = Ensure-Section "CommandsOrder" - $sectionIndent = Get-ChildIndent $section - - # Remove existing entries for this group - $toRemove = @() - foreach ($child in $section.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -ne "Command") { continue } - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "CommandGroup" -and $gc.InnerText.Trim() -eq $groupName) { - $toRemove += $child - break - } - } - } - foreach ($node in $toRemove) { - Remove-NodeWithWhitespace $node - $script:removeCount++ - } - - # Add new entries in order - foreach ($cmdName in $commands) { - $fragXml = "$groupName" - $nodes = Import-CIFragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $section $nodes[0] $null $sectionIndent - $script:addCount++ - } - } - Info "Set order for $groupName : $($commands.Count) commands" -} - -function Do-SubsystemOrder([string]$jsonVal) { - $parsed = $jsonVal | ConvertFrom-Json - $subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" } - if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 } - - $section = Ensure-Section "SubsystemsOrder" - $sectionIndent = Get-ChildIndent $section - - # Clear existing - $toRemove = @() - foreach ($child in @($section.ChildNodes)) { - if ($child.NodeType -eq 'Element') { $toRemove += $child } - } - foreach ($node in $toRemove) { - Remove-NodeWithWhitespace $node - $script:removeCount++ - } - - # Add new entries - foreach ($sub in $subsystems) { - $newEl = $script:xmlDoc.CreateElement("Subsystem", $script:ciNs) - $newEl.InnerText = $sub - Insert-BeforeElement $section $newEl $null $sectionIndent - $script:addCount++ - } - Info "Set subsystem order: $($subsystems.Count) entries" -} - -function Do-GroupOrder([string]$jsonVal) { - $parsed = $jsonVal | ConvertFrom-Json - $groups = @(); foreach ($g in $parsed) { $groups += "$g" } - if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 } - - $section = Ensure-Section "GroupsOrder" - $sectionIndent = Get-ChildIndent $section - - # Clear existing - $toRemove = @() - foreach ($child in @($section.ChildNodes)) { - if ($child.NodeType -eq 'Element') { $toRemove += $child } - } - foreach ($node in $toRemove) { - Remove-NodeWithWhitespace $node - $script:removeCount++ - } - - # Add new entries - foreach ($grp in $groups) { - $newEl = $script:xmlDoc.CreateElement("Group", $script:ciNs) - $newEl.InnerText = $grp - Insert-BeforeElement $section $newEl $null $sectionIndent - $script:addCount++ - } - Info "Set group order: $($groups.Count) entries" -} - -# --- Execute operations --- -$operations = @() -if ($DefinitionFile) { - if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { - $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile - } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile - $ops = $jsonText | ConvertFrom-Json - if ($ops -is [System.Array]) { - foreach ($op in $ops) { $operations += $op } - } else { - $operations += $ops - } -} else { - $operations += @{ operation = $Operation; value = $Value } -} - -foreach ($op in $operations) { - $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } - $opValueRaw = if ($op.value) { $op.value } else { "$Value" } - # For operations expecting JSON (place, order, etc.): accept object or string - $opValue = if ($opValueRaw -is [string]) { $opValueRaw } else { $opValueRaw | ConvertTo-Json -Compress } - - switch ($opName) { - "hide" { Do-Hide (Parse-ValueList $opValue) } - "show" { Do-Show (Parse-ValueList $opValue) } - "place" { Do-Place $opValue } - "order" { Do-Order $opValue } - "subsystem-order" { Do-SubsystemOrder $opValue } - "group-order" { Do-GroupOrder $opValue } - default { Write-Error "Unknown operation: $opName"; exit 1 } - } -} - -# --- Save --- -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = New-Object System.Text.UTF8Encoding($true) -$settings.Indent = $false -$settings.NewLineHandling = [System.Xml.NewLineHandling]::None - -$memStream = New-Object System.IO.MemoryStream -$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) -$script:xmlDoc.Save($writer) -$writer.Flush(); $writer.Close() - -$bytes = $memStream.ToArray() -$memStream.Close() -$text = [System.Text.Encoding]::UTF8.GetString($bytes) -if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } -$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') - -$utf8Bom = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) -Info "Saved: $resolvedPath" - -# --- Auto-validate --- -if (-not $NoValidate) { - $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\interface-validate") "scripts\interface-validate.ps1" - $validateScript = [System.IO.Path]::GetFullPath($validateScript) - if (Test-Path $validateScript) { - Write-Host "" - Write-Host "--- Running interface-validate ---" - & powershell.exe -NoProfile -File $validateScript -CIPath $resolvedPath - } -} - -# --- Summary --- -Write-Host "" -Write-Host "=== interface-edit summary ===" -Write-Host " Added: $($script:addCount)" -Write-Host " Removed: $($script:removeCount)" -Write-Host " Modified: $($script:modifyCount)" -exit 0 +# interface-edit v1.6 — Edit 1C CommandInterface.xml +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$CIPath, + [string]$DefinitionFile, + [ValidateSet("hide","show","place","order","subsystem-order","group-order")] + [string]$Operation, + [string]$Value, + [switch]$CreateIfMissing, + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } +if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($CIPath)) { + $CIPath = Join-Path (Get-Location).Path $CIPath +} +$resolvedPath = $CIPath + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. Walk-up from Subsystems/X/Ext/ +# CommandInterface.xml reaches Subsystems/X.xml (owning subsystem uuid). +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $CIPath 'editable' + +# --- Detect format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($CIPath)) + +# --- Namespaces --- +$script:ciNs = "http://v8.1c.ru/8.3/xcf/extrnprops" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:xsNs = "http://www.w3.org/2001/XMLSchema" + +# --- Create if missing --- +if (-not (Test-Path $CIPath)) { + if ($CreateIfMissing) { + $parentDir = [System.IO.Path]::GetDirectoryName($CIPath) + if (-not (Test-Path $parentDir)) { + New-Item -ItemType Directory -Path $parentDir -Force | Out-Null + } + $emptyCI = @" + + + +"@ + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom) + Write-Host "[INFO] Created new CommandInterface.xml: $CIPath" + } else { + Write-Error "File not found: $CIPath (use -CreateIfMissing to create)" + exit 1 + } +} +$resolvedPath = (Resolve-Path $CIPath).Path + +# --- Load XML --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- Detect structure --- +$root = $script:xmlDoc.DocumentElement +if ($root.LocalName -ne "CommandInterface") { + Write-Error "Expected root element, got <$($root.LocalName)>" + exit 1 +} + +# Section canonical order +$script:sectionOrder = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder") + +# --- XML manipulation helpers --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Import-CIFragment([string]$xmlString) { + $wrapper = "<_W xmlns=`"$($script:ciNs)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:xs=`"$($script:xsNs)`">$xmlString" + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +# --- Ensure section exists, creating it in correct order if needed --- +function Ensure-Section([string]$sectionName) { + # Find existing + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $sectionName) { + return $child + } + } + + # Create new section + $newSection = $script:xmlDoc.CreateElement($sectionName, $script:ciNs) + + # Find the correct insertion point: before the first section that comes AFTER us in canonical order + $myIdx = [array]::IndexOf($script:sectionOrder, $sectionName) + $refNode = $null + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childIdx = [array]::IndexOf($script:sectionOrder, $child.LocalName) + if ($childIdx -gt $myIdx) { + # Find the whitespace before this element to insert before it + $prev = $child.PreviousSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $refNode = $prev + } else { + $refNode = $child + } + break + } + } + + $rootIndent = Get-ChildIndent $root + # Add closing whitespace inside the new section + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent") + $newSection.AppendChild($closeWs) | Out-Null + + if ($refNode) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($newSection, $ws) | Out-Null + } else { + Insert-BeforeElement $root $newSection $null $rootIndent + } + return $newSection +} + +# --- Parse value: string or JSON array --- +function Parse-ValueList([string]$val) { + $val = $val.Trim() + if ($val.StartsWith("[")) { + $arr = $val | ConvertFrom-Json + $result = @(); foreach ($item in $arr) { $result += "$item" } + return ,$result + } + return @($val) +} + +# --- Find Command element by name in a section --- +function Find-CommandByName($section, [string]$cmdName) { + foreach ($child in $section.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Command") { + if ($child.GetAttribute("name") -eq $cmdName) { return $child } + } + } + return $null +} + +# --- Command name normalization (plural/Russian type prefix → singular English) --- +$script:typeNormMap = @{ + "Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "Constants"="Constant" + "Reports"="Report"; "DataProcessors"="DataProcessor" + "InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister" + "AccountingRegisters"="AccountingRegister"; "CalculationRegisters"="CalculationRegister" + "ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes" + "ChartsOfCalculationTypes"="ChartOfCalculationTypes" + "BusinessProcesses"="BusinessProcess"; "Tasks"="Task" + "ExchangePlans"="ExchangePlan"; "DocumentJournals"="DocumentJournal" + "CommonModules"="CommonModule"; "CommonCommands"="CommonCommand" + "CommonForms"="CommonForm"; "CommonPictures"="CommonPicture" + "CommonTemplates"="CommonTemplate"; "CommonAttributes"="CommonAttribute" + "CommandGroups"="CommandGroup"; "Roles"="Role" + "Subsystems"="Subsystem"; "StyleItems"="StyleItem" + # Russian singular + "Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum" + "Константа"="Constant"; "Отчёт"="Report"; "Отчет"="Report"; "Обработка"="DataProcessor" + "РегистрСведений"="InformationRegister"; "РегистрНакопления"="AccumulationRegister" + "РегистрБухгалтерии"="AccountingRegister" + "ПланСчетов"="ChartOfAccounts"; "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" + "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" + "ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal" + "ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand" + "ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem" + # Russian plural + "Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum" + "Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor" + "РегистрыСведений"="InformationRegister"; "РегистрыНакопления"="AccumulationRegister" + "РегистрыБухгалтерии"="AccountingRegister" + "ПланыСчетов"="ChartOfAccounts"; "ПланыВидовХарактеристик"="ChartOfCharacteristicTypes" + "БизнесПроцессы"="BusinessProcess"; "Задачи"="Task" + "ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal" + "Подсистемы"="Subsystem" +} + +function Normalize-CmdName([string]$name) { + if (-not $name -or -not $name.Contains('.')) { return $name } + $dotIdx = $name.IndexOf('.') + $first = $name.Substring(0, $dotIdx) + $rest = $name.Substring($dotIdx) + if ($script:typeNormMap.ContainsKey($first)) { + $normalized = "$($script:typeNormMap[$first])$rest" + if ($normalized -ne $name) { Write-Host "[NORM] Command: $name -> $normalized" } + return $normalized + } + return $name +} + +# --- Operations --- + +function Do-Hide([string[]]$commands) { + $commands = @($commands | ForEach-Object { Normalize-CmdName $_ }) + $section = Ensure-Section "CommandsVisibility" + $sectionIndent = Get-ChildIndent $section + + foreach ($cmd in $commands) { + $existing = Find-CommandByName $section $cmd + if ($existing) { + # Check if already false + $commonEl = $null + foreach ($vis in $existing.ChildNodes) { + if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") { + foreach ($c in $vis.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break } + } + } + } + if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") { + Warn "Already hidden: $cmd" + continue + } + # Change true -> false + if ($commonEl) { + $commonEl.InnerText = "false" + $script:modifyCount++ + Info "Changed to hidden: $cmd" + continue + } + } + # Add new entry + $fragXml = "false" + $nodes = Import-CIFragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $section $nodes[0] $null $sectionIndent + $script:addCount++ + Info "Hidden: $cmd" + } + } +} + +function Do-Show([string[]]$commands) { + $commands = @($commands | ForEach-Object { Normalize-CmdName $_ }) + $section = $null + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandsVisibility") { + $section = $child; break + } + } + + foreach ($cmd in $commands) { + if (-not $section) { + # No CommandsVisibility section — showing means adding with true + $section = Ensure-Section "CommandsVisibility" + } + $existing = Find-CommandByName $section $cmd + if ($existing) { + $commonEl = $null + foreach ($vis in $existing.ChildNodes) { + if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") { + foreach ($c in $vis.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break } + } + } + } + if ($commonEl -and $commonEl.InnerText.Trim() -eq "true") { + Warn "Already shown: $cmd" + continue + } + if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") { + # Change false -> true + $commonEl.InnerText = "true" + $script:modifyCount++ + Info "Changed to shown: $cmd" + continue + } + } + # Add new entry with true + $sectionIndent = Get-ChildIndent $section + $fragXml = "true" + $nodes = Import-CIFragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $section $nodes[0] $null $sectionIndent + $script:addCount++ + Info "Shown: $cmd" + } + } +} + +function Do-Place([string]$jsonVal) { + $def = $jsonVal | ConvertFrom-Json + $cmdName = Normalize-CmdName "$($def.command)" + $groupName = "$($def.group)" + if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 } + + $section = Ensure-Section "CommandsPlacement" + $sectionIndent = Get-ChildIndent $section + + # Check existing + $existing = Find-CommandByName $section $cmdName + if ($existing) { + # Update group + foreach ($child in $existing.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandGroup") { + $child.InnerText = $groupName + $script:modifyCount++ + Info "Updated placement: $cmdName -> $groupName" + return + } + } + } + + # Add new + $fragXml = "$groupNameAuto" + $nodes = Import-CIFragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $section $nodes[0] $null $sectionIndent + $script:addCount++ + Info "Placed: $cmdName -> $groupName" + } +} + +function Do-Order([string]$jsonVal) { + $def = $jsonVal | ConvertFrom-Json + $groupName = "$($def.group)" + $commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" }) + if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 } + + $section = Ensure-Section "CommandsOrder" + $sectionIndent = Get-ChildIndent $section + + # Remove existing entries for this group + $toRemove = @() + foreach ($child in $section.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -ne "Command") { continue } + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "CommandGroup" -and $gc.InnerText.Trim() -eq $groupName) { + $toRemove += $child + break + } + } + } + foreach ($node in $toRemove) { + Remove-NodeWithWhitespace $node + $script:removeCount++ + } + + # Add new entries in order + foreach ($cmdName in $commands) { + $fragXml = "$groupName" + $nodes = Import-CIFragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $section $nodes[0] $null $sectionIndent + $script:addCount++ + } + } + Info "Set order for $groupName : $($commands.Count) commands" +} + +function Do-SubsystemOrder([string]$jsonVal) { + $parsed = $jsonVal | ConvertFrom-Json + $subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" } + if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 } + + $section = Ensure-Section "SubsystemsOrder" + $sectionIndent = Get-ChildIndent $section + + # Clear existing + $toRemove = @() + foreach ($child in @($section.ChildNodes)) { + if ($child.NodeType -eq 'Element') { $toRemove += $child } + } + foreach ($node in $toRemove) { + Remove-NodeWithWhitespace $node + $script:removeCount++ + } + + # Add new entries + foreach ($sub in $subsystems) { + $newEl = $script:xmlDoc.CreateElement("Subsystem", $script:ciNs) + $newEl.InnerText = $sub + Insert-BeforeElement $section $newEl $null $sectionIndent + $script:addCount++ + } + Info "Set subsystem order: $($subsystems.Count) entries" +} + +function Do-GroupOrder([string]$jsonVal) { + $parsed = $jsonVal | ConvertFrom-Json + $groups = @(); foreach ($g in $parsed) { $groups += "$g" } + if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 } + + $section = Ensure-Section "GroupsOrder" + $sectionIndent = Get-ChildIndent $section + + # Clear existing + $toRemove = @() + foreach ($child in @($section.ChildNodes)) { + if ($child.NodeType -eq 'Element') { $toRemove += $child } + } + foreach ($node in $toRemove) { + Remove-NodeWithWhitespace $node + $script:removeCount++ + } + + # Add new entries + foreach ($grp in $groups) { + $newEl = $script:xmlDoc.CreateElement("Group", $script:ciNs) + $newEl.InnerText = $grp + Insert-BeforeElement $section $newEl $null $sectionIndent + $script:addCount++ + } + Info "Set group order: $($groups.Count) entries" +} + +# --- Execute operations --- +$operations = @() +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $ops = $jsonText | ConvertFrom-Json + if ($ops -is [System.Array]) { + foreach ($op in $ops) { $operations += $op } + } else { + $operations += $ops + } +} else { + $operations += @{ operation = $Operation; value = $Value } +} + +foreach ($op in $operations) { + $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } + $opValueRaw = if ($op.value) { $op.value } else { "$Value" } + # For operations expecting JSON (place, order, etc.): accept object or string + $opValue = if ($opValueRaw -is [string]) { $opValueRaw } else { $opValueRaw | ConvertTo-Json -Compress } + + switch ($opName) { + "hide" { Do-Hide (Parse-ValueList $opValue) } + "show" { Do-Show (Parse-ValueList $opValue) } + "place" { Do-Place $opValue } + "order" { Do-Order $opValue } + "subsystem-order" { Do-SubsystemOrder $opValue } + "group-order" { Do-GroupOrder $opValue } + default { Write-Error "Unknown operation: $opName"; exit 1 } + } +} + +# --- Save --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) +Info "Saved: $resolvedPath" + +# --- Auto-validate --- +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\interface-validate") "scripts\interface-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running interface-validate ---" + & powershell.exe -NoProfile -File $validateScript -CIPath $resolvedPath + } +} + +# --- Summary --- +Write-Host "" +Write-Host "=== interface-edit summary ===" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +exit 0 diff --git a/.claude/skills/interface-validate/SKILL.md b/.claude/skills/interface-validate/SKILL.md index e8db63f0..1fac489f 100644 --- a/.claude/skills/interface-validate/SKILL.md +++ b/.claude/skills/interface-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: interface-validate -description: Валидация командного интерфейса 1С. Используй после настройки командного интерфейса подсистемы для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /interface-validate — валидация CommandInterface.xml - -Проверяет XML командного интерфейса на структурные ошибки: корневой элемент, допустимые секции, порядок, формат ссылок на команды, дубликаты. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|-----------|:-----:|---------|-----------------------------------------| -| CIPath | да | — | Путь к CommandInterface.xml | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml" -``` +--- +name: interface-validate +description: Валидация командного интерфейса 1С. Используй после настройки командного интерфейса подсистемы для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /interface-validate — валидация CommandInterface.xml + +Проверяет XML командного интерфейса на структурные ошибки: корневой элемент, допустимые секции, порядок, формат ссылок на команды, дубликаты. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|-----------|:-----:|---------|-----------------------------------------| +| CIPath | да | — | Путь к CommandInterface.xml | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml" +``` diff --git a/.claude/skills/interface-validate/scripts/interface-validate.ps1 b/.claude/skills/interface-validate/scripts/interface-validate.ps1 index e76dd667..e99fcf49 100644 --- a/.claude/skills/interface-validate/scripts/interface-validate.ps1 +++ b/.claude/skills/interface-validate/scripts/interface-validate.ps1 @@ -1,403 +1,403 @@ -# interface-validate v1.1 — Validate 1C CommandInterface.xml structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][Alias('Path')][string]$CIPath, - [switch]$Detailed, - [int]$MaxErrors = 30, - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($CIPath)) { - $CIPath = Join-Path (Get-Location).Path $CIPath -} -# A: Directory → Ext/CommandInterface.xml -if (Test-Path $CIPath -PathType Container) { - $CIPath = Join-Path (Join-Path $CIPath "Ext") "CommandInterface.xml" -} -# B1: Missing Ext/ (e.g. Subsystems/X/CommandInterface.xml → Subsystems/X/Ext/CommandInterface.xml) -if (-not (Test-Path $CIPath)) { - $fn = [System.IO.Path]::GetFileName($CIPath) - if ($fn -eq "CommandInterface.xml") { - $c = Join-Path (Join-Path (Split-Path $CIPath) "Ext") $fn - if (Test-Path $c) { $CIPath = $c } - } -} -if (-not (Test-Path $CIPath)) { - Write-Host "[ERROR] File not found: $CIPath" - exit 1 -} -$resolvedPath = (Resolve-Path $CIPath).Path - -# --- Derive context name from path --- -$contextName = "" -$parentParts = $resolvedPath -split '[/\\]' -for ($i = 0; $i -lt $parentParts.Count; $i++) { - if ($parentParts[$i] -eq "Subsystems" -and ($i + 1) -lt $parentParts.Count) { - $contextName = $parentParts[$i + 1] - } -} -if (-not $contextName) { $contextName = "Root" } - -# --- Output infrastructure --- -$script:errors = 0 -$script:warnings = 0 -$script:stopped = $false -$script:okCount = 0 -$script:output = New-Object System.Text.StringBuilder 8192 -$script:allCommandNames = @() - -function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null } -function Report-OK([string]$msg) { - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} -function Report-Error([string]$msg) { - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { $script:stopped = $true } -} -function Report-Warn([string]$msg) { - $script:warnings++ - Out-Line "[WARN] $msg" -} - -Out-Line "=== Validation: CommandInterface ($contextName) ===" -Out-Line "" - -# --- Valid section names and order --- -$validSections = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder") - -# Command reference patterns -$stdCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$' -$customCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$' -$commonCmdPattern = '^CommonCommand\.\w+$' -$uuidCmdPattern = '^0:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' - -# --- 1. XML well-formedness + root structure --- -$xmlDoc = $null -try { - [xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8 -} catch { - Report-Error "1. XML parse error: $($_.Exception.Message)" - $script:stopped = $true -} - -if (-not $script:stopped) { - $root = $xmlDoc.DocumentElement - - if ($root.LocalName -ne "CommandInterface") { - Report-Error "1. Root element: expected , got <$($root.LocalName)>" - $script:stopped = $true - } else { - $nsUri = $root.NamespaceURI - $version = $root.GetAttribute("version") - $expectedNs = "http://v8.1c.ru/8.3/xcf/extrnprops" - if ($nsUri -ne $expectedNs) { - Report-Error "1. Root namespace: expected $expectedNs, got $nsUri" - } elseif (-not $version) { - Report-Warn "1. Root structure: CommandInterface, namespace valid, but no version attribute" - } else { - Report-OK "1. Root structure: CommandInterface, version $version, namespace valid" - } - } -} - -# --- Setup namespace manager --- -$ns = $null -if (-not $script:stopped) { - $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $ns.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops") - $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - $ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -} - -# --- 2. Valid child elements --- -if (-not $script:stopped) { - $root = $xmlDoc.DocumentElement - $foundSections = @() - $invalidElements = @() - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -in $validSections) { - $foundSections += $child.LocalName - } else { - $invalidElements += $child.LocalName - } - } - if ($invalidElements.Count -gt 0) { - Report-Error "2. Invalid child elements: $($invalidElements -join ', ')" - } else { - Report-OK "2. Child elements: $($foundSections.Count) valid sections" - } -} - -# --- 3. Section order --- -if (-not $script:stopped) { - $orderOk = $true - $lastIdx = -1 - foreach ($sec in $foundSections) { - $idx = [array]::IndexOf($validSections, $sec) - if ($idx -lt $lastIdx) { - Report-Error "3. Section order: '$sec' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)" - $orderOk = $false - break - } - $lastIdx = $idx - } - if ($orderOk) { Report-OK "3. Section order: correct" } -} - -# --- 4. No duplicate sections --- -if (-not $script:stopped) { - $dupes = $foundSections | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Error "4. Duplicate sections: $dupeNames" - } else { - Report-OK "4. No duplicate sections" - } -} - -# --- 5. CommandsVisibility --- -if (-not $script:stopped) { - $visSection = $root.SelectSingleNode("ci:CommandsVisibility", $ns) - if ($visSection) { - $visOk = $true - $visNames = @() - $visCount = 0 - foreach ($cmd in $visSection.ChildNodes) { - if ($cmd.NodeType -ne 'Element') { continue } - $visCount++ - $cmdName = $cmd.GetAttribute("name") - if (-not $cmdName) { - Report-Error "5. CommandsVisibility: Command element without 'name' attribute" - $visOk = $false; continue - } - $visNames += $cmdName - $script:allCommandNames += $cmdName - $visibility = $cmd.SelectSingleNode("ci:Visibility", $ns) - if (-not $visibility) { - Report-Error "5. CommandsVisibility[$cmdName]: missing " - $visOk = $false; continue - } - $common = $visibility.SelectSingleNode("xr:Common", $ns) - if (-not $common) { - Report-Error "5. CommandsVisibility[$cmdName]: missing " - $visOk = $false; continue - } - $val = $common.InnerText.Trim() - if ($val -ne "true" -and $val -ne "false") { - Report-Error "5. CommandsVisibility[$cmdName]: xr:Common='$val' (expected true/false)" - $visOk = $false - } - } - if ($visOk) { Report-OK "5. CommandsVisibility: $visCount entries, all valid" } - } else { - Report-OK "5. CommandsVisibility: not present" - $visNames = @() - } -} - -# --- 6. CommandsVisibility duplicates --- -if (-not $script:stopped) { - if ($visNames.Count -gt 0) { - $dupes = $visNames | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Warn "6. CommandsVisibility: duplicates: $dupeNames" - } else { - Report-OK "6. CommandsVisibility: no duplicates" - } - } else { - Report-OK "6. CommandsVisibility: no duplicates (empty)" - } -} - -# --- 7. CommandsPlacement --- -if (-not $script:stopped) { - $plcSection = $root.SelectSingleNode("ci:CommandsPlacement", $ns) - if ($plcSection) { - $plcOk = $true - $plcCount = 0 - foreach ($cmd in $plcSection.ChildNodes) { - if ($cmd.NodeType -ne 'Element') { continue } - $plcCount++ - $cmdName = $cmd.GetAttribute("name") - if (-not $cmdName) { - Report-Error "7. CommandsPlacement: Command without 'name' attribute" - $plcOk = $false; continue - } - $script:allCommandNames += $cmdName - $grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns) - if (-not $grpEl -or -not $grpEl.InnerText.Trim()) { - Report-Error "7. CommandsPlacement[$cmdName]: missing or empty " - $plcOk = $false; continue - } - $placementEl = $cmd.SelectSingleNode("ci:Placement", $ns) - if (-not $placementEl) { - Report-Error "7. CommandsPlacement[$cmdName]: missing " - $plcOk = $false - } elseif ($placementEl.InnerText.Trim() -ne "Auto") { - Report-Warn "7. CommandsPlacement[$cmdName]: Placement='$($placementEl.InnerText.Trim())' (expected Auto)" - } - } - if ($plcOk) { Report-OK "7. CommandsPlacement: $plcCount entries, all valid" } - } else { - Report-OK "7. CommandsPlacement: not present" - } -} - -# --- 8. CommandsOrder --- -if (-not $script:stopped) { - $ordSection = $root.SelectSingleNode("ci:CommandsOrder", $ns) - if ($ordSection) { - $ordOk = $true - $ordCount = 0 - foreach ($cmd in $ordSection.ChildNodes) { - if ($cmd.NodeType -ne 'Element') { continue } - $ordCount++ - $cmdName = $cmd.GetAttribute("name") - if (-not $cmdName) { - Report-Error "8. CommandsOrder: Command without 'name' attribute" - $ordOk = $false; continue - } - $script:allCommandNames += $cmdName - $grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns) - if (-not $grpEl -or -not $grpEl.InnerText.Trim()) { - Report-Error "8. CommandsOrder[$cmdName]: missing or empty " - $ordOk = $false - } - } - if ($ordOk) { Report-OK "8. CommandsOrder: $ordCount entries, all valid" } - } else { - Report-OK "8. CommandsOrder: not present" - } -} - -# --- 9. SubsystemsOrder format --- -if (-not $script:stopped) { - $subSection = $root.SelectSingleNode("ci:SubsystemsOrder", $ns) - $subNames = @() - if ($subSection) { - $subOk = $true - $subCount = 0 - foreach ($sub in $subSection.ChildNodes) { - if ($sub.NodeType -ne 'Element') { continue } - $subCount++ - $text = $sub.InnerText.Trim() - $subNames += $text - if (-not $text) { - Report-Error "9. SubsystemsOrder: empty element" - $subOk = $false - } elseif ($text -notmatch '^Subsystem\.') { - Report-Error "9. SubsystemsOrder: '$text' - expected format Subsystem.X..." - $subOk = $false - } - } - if ($subOk) { Report-OK "9. SubsystemsOrder: $subCount entries, all valid format" } - } else { - Report-OK "9. SubsystemsOrder: not present" - } -} - -# --- 10. SubsystemsOrder duplicates --- -if (-not $script:stopped) { - if ($subNames.Count -gt 0) { - $dupes = $subNames | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Warn "10. SubsystemsOrder: duplicates: $dupeNames" - } else { - Report-OK "10. SubsystemsOrder: no duplicates" - } - } else { - Report-OK "10. SubsystemsOrder: no duplicates (empty)" - } -} - -# --- 11. GroupsOrder entries --- -if (-not $script:stopped) { - $grpSection = $root.SelectSingleNode("ci:GroupsOrder", $ns) - $grpNames = @() - if ($grpSection) { - $grpOk = $true - $grpCount = 0 - foreach ($grp in $grpSection.ChildNodes) { - if ($grp.NodeType -ne 'Element') { continue } - $grpCount++ - $text = $grp.InnerText.Trim() - $grpNames += $text - if (-not $text) { - Report-Error "11. GroupsOrder: empty element" - $grpOk = $false - } - } - if ($grpOk) { Report-OK "11. GroupsOrder: $grpCount entries, all valid" } - } else { - Report-OK "11. GroupsOrder: not present" - } -} - -# --- 12. GroupsOrder duplicates --- -if (-not $script:stopped) { - if ($grpNames.Count -gt 0) { - $dupes = $grpNames | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Warn "12. GroupsOrder: duplicates: $dupeNames" - } else { - Report-OK "12. GroupsOrder: no duplicates" - } - } else { - Report-OK "12. GroupsOrder: no duplicates (empty)" - } -} - -# --- 13. Command reference format --- -if (-not $script:stopped) { - if ($script:allCommandNames.Count -gt 0) { - $badRefs = @() - foreach ($ref in $script:allCommandNames) { - if ($ref -match $stdCmdPattern) { continue } - if ($ref -match $customCmdPattern) { continue } - if ($ref -match $commonCmdPattern) { continue } - if ($ref -match $uuidCmdPattern) { continue } - $badRefs += $ref - } - if ($badRefs.Count -eq 0) { - Report-OK "13. Command reference format: all $($script:allCommandNames.Count) valid" - } else { - $shown = $badRefs[0..([Math]::Min(4, $badRefs.Count - 1))] - Report-Warn "13. Command reference format: $($badRefs.Count) unrecognized: $($shown -join ', ')$(if($badRefs.Count -gt 5){' ...'})" - } - } -} - -# --- Finalize --- -$checks = $script:okCount + $script:errors + $script:warnings - -if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: CommandInterface ($contextName) ($checks checks) ===" -} else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() -} - -Write-Host $result - -if ($OutFile) { - if (-not [System.IO.Path]::IsPathRooted($OutFile)) { - $OutFile = Join-Path (Get-Location).Path $OutFile - } - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" -} - -if ($script:errors -gt 0) { exit 1 } else { exit 0 } +# interface-validate v1.1 — Validate 1C CommandInterface.xml structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$CIPath, + [switch]$Detailed, + [int]$MaxErrors = 30, + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($CIPath)) { + $CIPath = Join-Path (Get-Location).Path $CIPath +} +# A: Directory → Ext/CommandInterface.xml +if (Test-Path $CIPath -PathType Container) { + $CIPath = Join-Path (Join-Path $CIPath "Ext") "CommandInterface.xml" +} +# B1: Missing Ext/ (e.g. Subsystems/X/CommandInterface.xml → Subsystems/X/Ext/CommandInterface.xml) +if (-not (Test-Path $CIPath)) { + $fn = [System.IO.Path]::GetFileName($CIPath) + if ($fn -eq "CommandInterface.xml") { + $c = Join-Path (Join-Path (Split-Path $CIPath) "Ext") $fn + if (Test-Path $c) { $CIPath = $c } + } +} +if (-not (Test-Path $CIPath)) { + Write-Host "[ERROR] File not found: $CIPath" + exit 1 +} +$resolvedPath = (Resolve-Path $CIPath).Path + +# --- Derive context name from path --- +$contextName = "" +$parentParts = $resolvedPath -split '[/\\]' +for ($i = 0; $i -lt $parentParts.Count; $i++) { + if ($parentParts[$i] -eq "Subsystems" -and ($i + 1) -lt $parentParts.Count) { + $contextName = $parentParts[$i + 1] + } +} +if (-not $contextName) { $contextName = "Root" } + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:okCount = 0 +$script:output = New-Object System.Text.StringBuilder 8192 +$script:allCommandNames = @() + +function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null } +function Report-OK([string]$msg) { + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} +function Report-Error([string]$msg) { + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { $script:stopped = $true } +} +function Report-Warn([string]$msg) { + $script:warnings++ + Out-Line "[WARN] $msg" +} + +Out-Line "=== Validation: CommandInterface ($contextName) ===" +Out-Line "" + +# --- Valid section names and order --- +$validSections = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder") + +# Command reference patterns +$stdCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$' +$customCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$' +$commonCmdPattern = '^CommonCommand\.\w+$' +$uuidCmdPattern = '^0:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + +# --- 1. XML well-formedness + root structure --- +$xmlDoc = $null +try { + [xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8 +} catch { + Report-Error "1. XML parse error: $($_.Exception.Message)" + $script:stopped = $true +} + +if (-not $script:stopped) { + $root = $xmlDoc.DocumentElement + + if ($root.LocalName -ne "CommandInterface") { + Report-Error "1. Root element: expected , got <$($root.LocalName)>" + $script:stopped = $true + } else { + $nsUri = $root.NamespaceURI + $version = $root.GetAttribute("version") + $expectedNs = "http://v8.1c.ru/8.3/xcf/extrnprops" + if ($nsUri -ne $expectedNs) { + Report-Error "1. Root namespace: expected $expectedNs, got $nsUri" + } elseif (-not $version) { + Report-Warn "1. Root structure: CommandInterface, namespace valid, but no version attribute" + } else { + Report-OK "1. Root structure: CommandInterface, version $version, namespace valid" + } + } +} + +# --- Setup namespace manager --- +$ns = $null +if (-not $script:stopped) { + $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $ns.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + $ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +} + +# --- 2. Valid child elements --- +if (-not $script:stopped) { + $root = $xmlDoc.DocumentElement + $foundSections = @() + $invalidElements = @() + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -in $validSections) { + $foundSections += $child.LocalName + } else { + $invalidElements += $child.LocalName + } + } + if ($invalidElements.Count -gt 0) { + Report-Error "2. Invalid child elements: $($invalidElements -join ', ')" + } else { + Report-OK "2. Child elements: $($foundSections.Count) valid sections" + } +} + +# --- 3. Section order --- +if (-not $script:stopped) { + $orderOk = $true + $lastIdx = -1 + foreach ($sec in $foundSections) { + $idx = [array]::IndexOf($validSections, $sec) + if ($idx -lt $lastIdx) { + Report-Error "3. Section order: '$sec' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)" + $orderOk = $false + break + } + $lastIdx = $idx + } + if ($orderOk) { Report-OK "3. Section order: correct" } +} + +# --- 4. No duplicate sections --- +if (-not $script:stopped) { + $dupes = $foundSections | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Error "4. Duplicate sections: $dupeNames" + } else { + Report-OK "4. No duplicate sections" + } +} + +# --- 5. CommandsVisibility --- +if (-not $script:stopped) { + $visSection = $root.SelectSingleNode("ci:CommandsVisibility", $ns) + if ($visSection) { + $visOk = $true + $visNames = @() + $visCount = 0 + foreach ($cmd in $visSection.ChildNodes) { + if ($cmd.NodeType -ne 'Element') { continue } + $visCount++ + $cmdName = $cmd.GetAttribute("name") + if (-not $cmdName) { + Report-Error "5. CommandsVisibility: Command element without 'name' attribute" + $visOk = $false; continue + } + $visNames += $cmdName + $script:allCommandNames += $cmdName + $visibility = $cmd.SelectSingleNode("ci:Visibility", $ns) + if (-not $visibility) { + Report-Error "5. CommandsVisibility[$cmdName]: missing " + $visOk = $false; continue + } + $common = $visibility.SelectSingleNode("xr:Common", $ns) + if (-not $common) { + Report-Error "5. CommandsVisibility[$cmdName]: missing " + $visOk = $false; continue + } + $val = $common.InnerText.Trim() + if ($val -ne "true" -and $val -ne "false") { + Report-Error "5. CommandsVisibility[$cmdName]: xr:Common='$val' (expected true/false)" + $visOk = $false + } + } + if ($visOk) { Report-OK "5. CommandsVisibility: $visCount entries, all valid" } + } else { + Report-OK "5. CommandsVisibility: not present" + $visNames = @() + } +} + +# --- 6. CommandsVisibility duplicates --- +if (-not $script:stopped) { + if ($visNames.Count -gt 0) { + $dupes = $visNames | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Warn "6. CommandsVisibility: duplicates: $dupeNames" + } else { + Report-OK "6. CommandsVisibility: no duplicates" + } + } else { + Report-OK "6. CommandsVisibility: no duplicates (empty)" + } +} + +# --- 7. CommandsPlacement --- +if (-not $script:stopped) { + $plcSection = $root.SelectSingleNode("ci:CommandsPlacement", $ns) + if ($plcSection) { + $plcOk = $true + $plcCount = 0 + foreach ($cmd in $plcSection.ChildNodes) { + if ($cmd.NodeType -ne 'Element') { continue } + $plcCount++ + $cmdName = $cmd.GetAttribute("name") + if (-not $cmdName) { + Report-Error "7. CommandsPlacement: Command without 'name' attribute" + $plcOk = $false; continue + } + $script:allCommandNames += $cmdName + $grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns) + if (-not $grpEl -or -not $grpEl.InnerText.Trim()) { + Report-Error "7. CommandsPlacement[$cmdName]: missing or empty " + $plcOk = $false; continue + } + $placementEl = $cmd.SelectSingleNode("ci:Placement", $ns) + if (-not $placementEl) { + Report-Error "7. CommandsPlacement[$cmdName]: missing " + $plcOk = $false + } elseif ($placementEl.InnerText.Trim() -ne "Auto") { + Report-Warn "7. CommandsPlacement[$cmdName]: Placement='$($placementEl.InnerText.Trim())' (expected Auto)" + } + } + if ($plcOk) { Report-OK "7. CommandsPlacement: $plcCount entries, all valid" } + } else { + Report-OK "7. CommandsPlacement: not present" + } +} + +# --- 8. CommandsOrder --- +if (-not $script:stopped) { + $ordSection = $root.SelectSingleNode("ci:CommandsOrder", $ns) + if ($ordSection) { + $ordOk = $true + $ordCount = 0 + foreach ($cmd in $ordSection.ChildNodes) { + if ($cmd.NodeType -ne 'Element') { continue } + $ordCount++ + $cmdName = $cmd.GetAttribute("name") + if (-not $cmdName) { + Report-Error "8. CommandsOrder: Command without 'name' attribute" + $ordOk = $false; continue + } + $script:allCommandNames += $cmdName + $grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns) + if (-not $grpEl -or -not $grpEl.InnerText.Trim()) { + Report-Error "8. CommandsOrder[$cmdName]: missing or empty " + $ordOk = $false + } + } + if ($ordOk) { Report-OK "8. CommandsOrder: $ordCount entries, all valid" } + } else { + Report-OK "8. CommandsOrder: not present" + } +} + +# --- 9. SubsystemsOrder format --- +if (-not $script:stopped) { + $subSection = $root.SelectSingleNode("ci:SubsystemsOrder", $ns) + $subNames = @() + if ($subSection) { + $subOk = $true + $subCount = 0 + foreach ($sub in $subSection.ChildNodes) { + if ($sub.NodeType -ne 'Element') { continue } + $subCount++ + $text = $sub.InnerText.Trim() + $subNames += $text + if (-not $text) { + Report-Error "9. SubsystemsOrder: empty element" + $subOk = $false + } elseif ($text -notmatch '^Subsystem\.') { + Report-Error "9. SubsystemsOrder: '$text' - expected format Subsystem.X..." + $subOk = $false + } + } + if ($subOk) { Report-OK "9. SubsystemsOrder: $subCount entries, all valid format" } + } else { + Report-OK "9. SubsystemsOrder: not present" + } +} + +# --- 10. SubsystemsOrder duplicates --- +if (-not $script:stopped) { + if ($subNames.Count -gt 0) { + $dupes = $subNames | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Warn "10. SubsystemsOrder: duplicates: $dupeNames" + } else { + Report-OK "10. SubsystemsOrder: no duplicates" + } + } else { + Report-OK "10. SubsystemsOrder: no duplicates (empty)" + } +} + +# --- 11. GroupsOrder entries --- +if (-not $script:stopped) { + $grpSection = $root.SelectSingleNode("ci:GroupsOrder", $ns) + $grpNames = @() + if ($grpSection) { + $grpOk = $true + $grpCount = 0 + foreach ($grp in $grpSection.ChildNodes) { + if ($grp.NodeType -ne 'Element') { continue } + $grpCount++ + $text = $grp.InnerText.Trim() + $grpNames += $text + if (-not $text) { + Report-Error "11. GroupsOrder: empty element" + $grpOk = $false + } + } + if ($grpOk) { Report-OK "11. GroupsOrder: $grpCount entries, all valid" } + } else { + Report-OK "11. GroupsOrder: not present" + } +} + +# --- 12. GroupsOrder duplicates --- +if (-not $script:stopped) { + if ($grpNames.Count -gt 0) { + $dupes = $grpNames | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Warn "12. GroupsOrder: duplicates: $dupeNames" + } else { + Report-OK "12. GroupsOrder: no duplicates" + } + } else { + Report-OK "12. GroupsOrder: no duplicates (empty)" + } +} + +# --- 13. Command reference format --- +if (-not $script:stopped) { + if ($script:allCommandNames.Count -gt 0) { + $badRefs = @() + foreach ($ref in $script:allCommandNames) { + if ($ref -match $stdCmdPattern) { continue } + if ($ref -match $customCmdPattern) { continue } + if ($ref -match $commonCmdPattern) { continue } + if ($ref -match $uuidCmdPattern) { continue } + $badRefs += $ref + } + if ($badRefs.Count -eq 0) { + Report-OK "13. Command reference format: all $($script:allCommandNames.Count) valid" + } else { + $shown = $badRefs[0..([Math]::Min(4, $badRefs.Count - 1))] + Report-Warn "13. Command reference format: $($badRefs.Count) unrecognized: $($shown -join ', ')$(if($badRefs.Count -gt 5){' ...'})" + } + } +} + +# --- Finalize --- +$checks = $script:okCount + $script:errors + $script:warnings + +if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: CommandInterface ($contextName) ($checks checks) ===" +} else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() +} + +Write-Host $result + +if ($OutFile) { + if (-not [System.IO.Path]::IsPathRooted($OutFile)) { + $OutFile = Join-Path (Get-Location).Path $OutFile + } + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" +} + +if ($script:errors -gt 0) { exit 1 } else { exit 0 } diff --git a/.claude/skills/meta-compile/SKILL.md b/.claude/skills/meta-compile/SKILL.md index 573c549f..a50f3cae 100644 --- a/.claude/skills/meta-compile/SKILL.md +++ b/.claude/skills/meta-compile/SKILL.md @@ -1,119 +1,119 @@ ---- -name: meta-compile -description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др. -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /meta-compile — генерация объектов метаданных из JSON DSL - -Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml. - -## Порядок работы - -1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл -2. Запусти скрипт meta-compile -3. Если нужно изменить созданный объект — `/meta-edit` -4. Если нужно проверить — `/meta-validate` - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "" -OutputDir "" -``` - -| Параметр | Описание | -|----------|----------| -| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) | -| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) | - -## JSON DSL - -### Общая структура - -```json -{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... } -``` - -`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`. - -### Shorthand реквизитов - -Используется в `attributes`, `dimensions`, `resources`, `tabularSections`: - -``` -"ИмяРеквизита" → String(10) по умолчанию -"ИмяРеквизита: Тип" → с типом -"ИмяРеквизита: Тип | req, index" → с флагами -``` - -Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные. - -Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`. - -Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`. - -### Свойства по типам - -Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл: - -- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor -- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes -- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal -- `reference/types-web.md` — HTTPService, WebService - -Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций. - -## Примеры паттернов DSL - -### Минимальный объект - -```json -{ "type": "Catalog", "name": "Валюты" } -``` - -### С реквизитами - -```json -{ - "type": "Catalog", "name": "Организации", - "descriptionLength": 100, - "attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] -} -``` - -### С табличной частью - -```json -{ - "type": "Document", "name": "ПриходнаяНакладная", - "registerRecords": ["AccumulationRegister.ОстаткиТоваров"], - "attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"], - "tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } -} -``` - -### Регистровый паттерн (измерения + ресурсы) - -```json -{ - "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day", - "dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"], - "resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] -} -``` - -### Batch — несколько объектов в одном файле - -```json -[ - { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] }, - { "type": "Catalog", "name": "Валюты" }, - { "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } -] -``` - +--- +name: meta-compile +description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др. +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /meta-compile — генерация объектов метаданных из JSON DSL + +Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml. + +## Порядок работы + +1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл +2. Запусти скрипт meta-compile +3. Если нужно изменить созданный объект — `/meta-edit` +4. Если нужно проверить — `/meta-validate` + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "" -OutputDir "" +``` + +| Параметр | Описание | +|----------|----------| +| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) | +| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) | + +## JSON DSL + +### Общая структура + +```json +{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... } +``` + +`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`. + +### Shorthand реквизитов + +Используется в `attributes`, `dimensions`, `resources`, `tabularSections`: + +``` +"ИмяРеквизита" → String(10) по умолчанию +"ИмяРеквизита: Тип" → с типом +"ИмяРеквизита: Тип | req, index" → с флагами +``` + +Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные. + +Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`. + +Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`. + +### Свойства по типам + +Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл: + +- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor +- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes +- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal +- `reference/types-web.md` — HTTPService, WebService + +Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций. + +## Примеры паттернов DSL + +### Минимальный объект + +```json +{ "type": "Catalog", "name": "Валюты" } +``` + +### С реквизитами + +```json +{ + "type": "Catalog", "name": "Организации", + "descriptionLength": 100, + "attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] +} +``` + +### С табличной частью + +```json +{ + "type": "Document", "name": "ПриходнаяНакладная", + "registerRecords": ["AccumulationRegister.ОстаткиТоваров"], + "attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"], + "tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } +} +``` + +### Регистровый паттерн (измерения + ресурсы) + +```json +{ + "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day", + "dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"], + "resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] +} +``` + +### Batch — несколько объектов в одном файле + +```json +[ + { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] }, + { "type": "Catalog", "name": "Валюты" }, + { "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } +] +``` + diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 413dc138..d4e5c0dd 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,3245 +1,3245 @@ -# meta-compile v1.14 — Compile 1C metadata object from JSON -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [string]$JsonPath, - - [Parameter(Mandatory)] - [string]$OutputDir -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- 1. Load and validate JSON --- - -if (-not (Test-Path $JsonPath)) { - Write-Error "File not found: $JsonPath" - exit 1 -} - -$json = Get-Content -Raw -Encoding UTF8 $JsonPath -$def = $json | ConvertFrom-Json - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $OutputDir 'editable' - -# --- Batch mode: JSON array of objects --- -if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) { - $batchOk = 0 - $batchFail = 0 - $idx = 0 - foreach ($item in $def) { - $idx++ - $tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json" - try { - $item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson - $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru - if ($proc.ExitCode -eq 0) { $batchOk++ } else { $batchFail++ } - } finally { - Remove-Item $tmpJson -Force -ErrorAction SilentlyContinue - } - } - Write-Host "" - Write-Host "=== Batch: $idx objects, $batchOk compiled, $batchFail failed ===" - if ($batchFail -gt 0) { exit 1 } - exit 0 -} - -# Normalize field synonyms: accept "objectType" as alias for "type" -if (-not $def.type -and $def.objectType) { - $def | Add-Member -NotePropertyName "type" -NotePropertyValue $def.objectType -} - -# Object type synonyms (Russian → English) -$script:objectTypeSynonyms = @{ - "Справочник" = "Catalog" - "Каталог" = "Catalog" - "Документ" = "Document" - "Перечисление" = "Enum" - "Константа" = "Constant" - "РегистрСведений" = "InformationRegister" - "РегистрНакопления" = "AccumulationRegister" - "РегистрБухгалтерии" = "AccountingRegister" - "РегистрРасчёта" = "CalculationRegister" - "РегистрРасчета" = "CalculationRegister" - "ПланСчетов" = "ChartOfAccounts" - "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" - "ПланВидовРасчёта" = "ChartOfCalculationTypes" - "ПланВидовРасчета" = "ChartOfCalculationTypes" - "БизнесПроцесс" = "BusinessProcess" - "Задача" = "Task" - "ПланОбмена" = "ExchangePlan" - "ЖурналДокументов" = "DocumentJournal" - "Отчёт" = "Report" - "Отчет" = "Report" - "Обработка" = "DataProcessor" - "ОбщийМодуль" = "CommonModule" - "РегламентноеЗадание" = "ScheduledJob" - "ПодпискаНаСобытие" = "EventSubscription" - "HTTPСервис" = "HTTPService" - "ВебСервис" = "WebService" - "ОпределяемыйТип" = "DefinedType" -} - -# Enum property value synonyms — model often gets these slightly wrong -$script:enumValueAliases = @{ - # RegisterType (AccumulationRegister) - "Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers" - # WriteMode (InformationRegister) - "RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate" - "ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent" - # DependenceOnCalculationTypes (ChartOfCalculationTypes) - "NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse" - "Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod" - # InformationRegisterPeriodicity - "None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month" - "Quarterly" = "Quarter"; "Yearly" = "Year" - "Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day" - "Месяц" = "Month"; "Квартал" = "Quarter"; "Год" = "Year" - "ПозицияРегистратора" = "RecorderPosition" - # DataLockControlMode - "Автоматический" = "Automatic"; "Управляемый" = "Managed" - # FullTextSearch - "Использовать" = "Use"; "НеИспользовать" = "DontUse" - # Posting - "Разрешить" = "Allow"; "Запретить" = "Deny" - # EditType - "ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays" - # DefaultPresentation - "ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode" - # FillChecking - "НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning" - # Indexing - "НеИндексировать" = "DontIndex"; "Индексировать" = "Index" - "ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder" -} - -# Valid enum values per property (from meta-validate) -$script:validEnumValues = @{ - "RegisterType" = @("Balance","Turnovers") - "WriteMode" = @("Independent","RecorderSubordinate") - "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") - "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") - "DataLockControlMode" = @("Automatic","Managed") - "FullTextSearch" = @("Use","DontUse") - "DataHistory" = @("Use","DontUse") - "DefaultPresentation" = @("AsDescription","AsCode") - "Posting" = @("Allow","Deny") - "RealTimePosting" = @("Allow","Deny") - "EditType" = @("InDialog","InList","BothWays") - "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") - "CodeType" = @("String","Number") - "CodeAllowedLength" = @("Variable","Fixed") - "NumberType" = @("String","Number") - "NumberAllowedLength" = @("Variable","Fixed") - "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") - "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") - "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") - "ReuseSessions" = @("DontUse","AutoUse") - "FillChecking" = @("DontCheck","ShowError","ShowWarning") - "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") - "SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems") - "CodeSeries" = @("WholeCatalog","WithinSubordination") - "ChoiceMode" = @("BothWays","QuickChoice","FromForm") -} - -function Normalize-EnumValue { - param([string]$propName, [string]$value) - # 1. Check alias dictionary — silent auto-correct - if ($script:enumValueAliases.ContainsKey($value)) { - return $script:enumValueAliases[$value] - } - # 2. Case-insensitive match against valid values — silent - $valid = $script:validEnumValues[$propName] - if ($valid) { - foreach ($v in $valid) { - if ($v -ieq $value) { return $v } - } - # 3. Known property, unknown value — error with hint - Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')" - exit 1 - } - # 4. Unknown property — pass-through (no validation data) - return $value -} - -# Helper: read enum property from $def with default and normalization -function Get-EnumProp { - param([string]$propName, [string]$fieldName, [string]$default) - $val = $def.$fieldName - $raw = if ($val) { "$val" } else { $default } - return (Normalize-EnumValue $propName $raw) -} - -if (-not $def.type) { - Write-Error "JSON must have 'type' field" - exit 1 -} - -# Resolve type synonym -$objType = "$($def.type)" -if ($script:objectTypeSynonyms.ContainsKey($objType)) { - $objType = $script:objectTypeSynonyms[$objType] -} - -$validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","AccumulationRegister", - "AccountingRegister","CalculationRegister","ChartOfAccounts","ChartOfCharacteristicTypes", - "ChartOfCalculationTypes","BusinessProcess","Task","ExchangePlan","DocumentJournal", - "Report","DataProcessor","CommonModule","ScheduledJob","EventSubscription", - "HTTPService","WebService","DefinedType") -if ($objType -notin $validTypes) { - Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')" - exit 1 -} - -if (-not $def.name) { - Write-Error "JSON must have 'name' field" - exit 1 -} - -$objName = "$($def.name)" - -# --- 2. XML helpers --- - -$script:xml = New-Object System.Text.StringBuilder 32768 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') -} - -function Emit-MLText { - param([string]$indent, [string]$tag, [string]$text) - if (-not $text) { - X "$indent<$tag/>" - return - } - X "$indent<$tag>" - X "$indent`t" - X "$indent`t`tru" - X "$indent`t`t$(Esc-Xml $text)" - X "$indent`t" - X "$indent" -} - -function New-Guid-String { - return [System.Guid]::NewGuid().ToString() -} - -# --- 3. CamelCase splitter --- - -function Split-CamelCase { - param([string]$name) - if (-not $name) { return $name } - # Insert space before uppercase that follows lowercase (Cyrillic + Latin) - $result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2') - $result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2') - # Lowercase all but first character of the result - if ($result.Length -gt 1) { - $result = $result.Substring(0,1) + $result.Substring(1).ToLower() - } - return $result -} - -# Auto-synonym -$synonym = if ($def.synonym) { "$($def.synonym)" } else { Split-CamelCase $objName } -$comment = if ($def.comment) { "$($def.comment)" } else { "" } - -# --- 4. Type system --- - -$script:typeSynonyms = New-Object System.Collections.Hashtable -$script:typeSynonyms["число"] = "Number" -$script:typeSynonyms["строка"] = "String" -$script:typeSynonyms["булево"] = "Boolean" -$script:typeSynonyms["дата"] = "Date" -$script:typeSynonyms["датавремя"]= "DateTime" -$script:typeSynonyms["number"] = "Number" -$script:typeSynonyms["string"] = "String" -$script:typeSynonyms["boolean"] = "Boolean" -$script:typeSynonyms["date"] = "Date" -$script:typeSynonyms["datetime"] = "DateTime" -$script:typeSynonyms["bool"] = "Boolean" -# Reference synonyms (Russian, lowercase) -$script:typeSynonyms["справочникссылка"] = "CatalogRef" -$script:typeSynonyms["документссылка"] = "DocumentRef" -$script:typeSynonyms["перечислениессылка"] = "EnumRef" -$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:typeSynonyms["задачассылка"] = "TaskRef" -$script:typeSynonyms["определяемыйтип"] = "DefinedType" -$script:typeSynonyms["definedtype"] = "DefinedType" -# English lowercase ref synonyms -$script:typeSynonyms["catalogref"] = "CatalogRef" -$script:typeSynonyms["documentref"] = "DocumentRef" -$script:typeSynonyms["enumref"] = "EnumRef" - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - - # Check for parameterized types: Number(15,2), Строка(100), etc. - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $baseName = $Matches[1].Trim() - $params = $Matches[2] - $resolved = $script:typeSynonyms[$baseName.ToLower()] - if ($resolved) { return "$resolved($params)" } - return $typeStr - } - - # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации - if ($typeStr.Contains('.')) { - $dotIdx = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $dotIdx) - $suffix = $typeStr.Substring($dotIdx) # includes the dot - $resolved = $script:typeSynonyms[$prefix.ToLower()] - if ($resolved) { return "$resolved$suffix" } - return $typeStr - } - - # Simple name lookup - $resolved = $script:typeSynonyms[$typeStr.ToLower()] - if ($resolved) { return $resolved } - - return $typeStr -} - -function Emit-TypeContent { - param([string]$indent, [string]$typeStr) - if (-not $typeStr) { return } - - # Composite type: "Type1 + Type2 + Type3" - if ($typeStr.Contains(' + ')) { - $parts = $typeStr -split '\s*\+\s*' - foreach ($part in $parts) { - Emit-TypeContent $indent $part.Trim() - } - return - } - - $typeStr = Resolve-TypeStr $typeStr - - # Boolean - if ($typeStr -eq "Boolean") { - X "$indentxs:boolean" - return - } - - # String or String(N) - if ($typeStr -match '^String(\((\d+)\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "10" } - X "$indentxs:string" - X "$indent" - X "$indent`t$len" - X "$indent`tVariable" - X "$indent" - return - } - - # Number without params → Number(10,0) - if ($typeStr -eq "Number") { - X "$indentxs:decimal" - X "$indent" - X "$indent`t10" - X "$indent`t0" - X "$indent`tAny" - X "$indent" - return - } - - # Number(D,F) or Number(D,F,nonneg) - if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1] - $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - X "$indentxs:decimal" - X "$indent" - X "$indent`t$digits" - X "$indent`t$fraction" - X "$indent`t$sign" - X "$indent" - return - } - - # Date / DateTime - if ($typeStr -eq "Date") { - X "$indentxs:dateTime" - X "$indent" - X "$indent`tDate" - X "$indent" - return - } - if ($typeStr -eq "DateTime") { - X "$indentxs:dateTime" - X "$indent" - X "$indent`tDateTime" - X "$indent" - return - } - - # DefinedType - if ($typeStr -match '^DefinedType\.(.+)$') { - $dtName = $Matches[1] - X "$indentcfg:DefinedType.$dtName" - return - } - - # ValueStorage - if ($typeStr -eq "ValueStorage") { - X "$indentxs:base64Binary" - return - } - - # Reference types — use local xmlns declaration for 1C compatibility - if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { - X "$indentd5p1:$typeStr" - return - } - - # Fallback — emit as-is - X "$indent$typeStr" -} - -function Emit-ValueType { - param([string]$indent, [string]$typeStr) - X "$indent" - Emit-TypeContent "$indent`t" $typeStr - X "$indent" -} - -function Emit-FillValue { - param([string]$indent, [string]$typeStr) - if (-not $typeStr) { - X "$indent" - return - } - - $typeStr = Resolve-TypeStr $typeStr - - if ($typeStr -eq "Boolean") { - X "$indentfalse" - return - } - if ($typeStr -match '^String') { - X "$indent" - return - } - if ($typeStr -match '^Number') { - X "$indent0" - return - } - if ($typeStr -match '^(Date|DateTime)$') { - X "$indent" - return - } - # References and others - X "$indent" -} - -# --- 5. Attribute shorthand parser --- - -function Build-TypeStr { - param($obj) - $t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" } - if ($t -and -not $t.Contains('(')) { - if ($t -eq "String" -and $obj.length) { - $t = "String($($obj.length))" - } elseif ($t -eq "Number" -and $obj.length) { - $p = if ($obj.precision) { $obj.precision } else { 0 } - $nn = if ($obj.nonneg -or $obj.nonnegative) { ",nonneg" } else { "" } - $t = "Number($($obj.length),$p$nn)" - } - } - return $t -} - -function Parse-AttributeShorthand { - param($val) - - if ($val -is [string]) { - $str = "$val" - $parsed = @{ - name = "" - type = "" - synonym = "" - comment = "" - flags = @() - } - - # Split by | for flags - $parts = $str -split '\|', 2 - $mainPart = $parts[0].Trim() - if ($parts.Count -gt 1) { - $flagStr = $parts[1].Trim() - $parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) - } - - # Split by : for name and type - $colonParts = $mainPart -split ':', 2 - $parsed.name = $colonParts[0].Trim() - if ($colonParts.Count -gt 1) { - $parsed.type = $colonParts[1].Trim() - } - - $parsed.synonym = Split-CamelCase $parsed.name - return $parsed - } - - # Object form - $name = "$($val.name)" - return @{ - name = $name - type = Build-TypeStr $val - synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } - comment = if ($val.comment) { "$($val.comment)" } else { "" } - flags = @(if ($val.flags) { $val.flags } else { @() }) - fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } - indexing = if ($val.indexing) { "$($val.indexing)" } else { "" } - multiLine = if ($val.multiLine -eq $true) { $true } else { $false } - choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" } - } -} - -function Parse-EnumValueShorthand { - param($val) - - if ($val -is [string]) { - $name = "$val" - return @{ - name = $name - synonym = Split-CamelCase $name - comment = "" - } - } - - $name = "$($val.name)" - return @{ - name = $name - synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } - comment = if ($val.comment) { "$($val.comment)" } else { "" } - } -} - -# --- 6. GeneratedType categories --- - -$script:generatedTypes = @{ - "Catalog" = @( - @{ prefix = "CatalogObject"; category = "Object" } - @{ prefix = "CatalogRef"; category = "Ref" } - @{ prefix = "CatalogSelection"; category = "Selection" } - @{ prefix = "CatalogList"; category = "List" } - @{ prefix = "CatalogManager"; category = "Manager" } - ) - "Document" = @( - @{ prefix = "DocumentObject"; category = "Object" } - @{ prefix = "DocumentRef"; category = "Ref" } - @{ prefix = "DocumentSelection"; category = "Selection" } - @{ prefix = "DocumentList"; category = "List" } - @{ prefix = "DocumentManager"; category = "Manager" } - ) - "Enum" = @( - @{ prefix = "EnumRef"; category = "Ref" } - @{ prefix = "EnumManager"; category = "Manager" } - @{ prefix = "EnumList"; category = "List" } - ) - "Constant" = @( - @{ prefix = "ConstantManager"; category = "Manager" } - @{ prefix = "ConstantValueManager"; category = "ValueManager" } - @{ prefix = "ConstantValueKey"; category = "ValueKey" } - ) - "InformationRegister" = @( - @{ prefix = "InformationRegisterRecord"; category = "Record" } - @{ prefix = "InformationRegisterManager"; category = "Manager" } - @{ prefix = "InformationRegisterSelection"; category = "Selection" } - @{ prefix = "InformationRegisterList"; category = "List" } - @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } - @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } - ) - "AccumulationRegister" = @( - @{ prefix = "AccumulationRegisterRecord"; category = "Record" } - @{ prefix = "AccumulationRegisterManager"; category = "Manager" } - @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } - @{ prefix = "AccumulationRegisterList"; category = "List" } - @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } - ) - "AccountingRegister" = @( - @{ prefix = "AccountingRegisterRecord"; category = "Record" } - @{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" } - @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } - @{ prefix = "AccountingRegisterSelection"; category = "Selection" } - @{ prefix = "AccountingRegisterList"; category = "List" } - @{ prefix = "AccountingRegisterManager"; category = "Manager" } - ) - "CalculationRegister" = @( - @{ prefix = "CalculationRegisterRecord"; category = "Record" } - @{ prefix = "CalculationRegisterManager"; category = "Manager" } - @{ prefix = "CalculationRegisterSelection"; category = "Selection" } - @{ prefix = "CalculationRegisterList"; category = "List" } - @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } - @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } - @{ prefix = "RecalculationsManager"; category = "Recalcs" } - ) - "ChartOfAccounts" = @( - @{ prefix = "ChartOfAccountsObject"; category = "Object" } - @{ prefix = "ChartOfAccountsRef"; category = "Ref" } - @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } - @{ prefix = "ChartOfAccountsList"; category = "List" } - @{ prefix = "ChartOfAccountsManager"; category = "Manager" } - @{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" } - @{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" } - ) - "ChartOfCharacteristicTypes" = @( - @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } - @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } - @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } - @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } - @{ prefix = "ChartOfCharacteristicTypesCharacteristic"; category = "Characteristic" } - @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } - ) - "ChartOfCalculationTypes" = @( - @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } - @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } - @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } - @{ prefix = "ChartOfCalculationTypesList"; category = "List" } - @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } - @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } - @{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" } - @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } - @{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" } - @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } - @{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" } - ) - "BusinessProcess" = @( - @{ prefix = "BusinessProcessObject"; category = "Object" } - @{ prefix = "BusinessProcessRef"; category = "Ref" } - @{ prefix = "BusinessProcessSelection"; category = "Selection" } - @{ prefix = "BusinessProcessList"; category = "List" } - @{ prefix = "BusinessProcessManager"; category = "Manager" } - @{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" } - ) - "Task" = @( - @{ prefix = "TaskObject"; category = "Object" } - @{ prefix = "TaskRef"; category = "Ref" } - @{ prefix = "TaskSelection"; category = "Selection" } - @{ prefix = "TaskList"; category = "List" } - @{ prefix = "TaskManager"; category = "Manager" } - ) - "ExchangePlan" = @( - @{ prefix = "ExchangePlanObject"; category = "Object" } - @{ prefix = "ExchangePlanRef"; category = "Ref" } - @{ prefix = "ExchangePlanSelection"; category = "Selection" } - @{ prefix = "ExchangePlanList"; category = "List" } - @{ prefix = "ExchangePlanManager"; category = "Manager" } - ) - "DefinedType" = @( - @{ prefix = "DefinedType"; category = "DefinedType" } - ) - "DocumentJournal" = @( - @{ prefix = "DocumentJournalSelection"; category = "Selection" } - @{ prefix = "DocumentJournalList"; category = "List" } - @{ prefix = "DocumentJournalManager"; category = "Manager" } - ) - "Report" = @( - @{ prefix = "ReportObject"; category = "Object" } - @{ prefix = "ReportManager"; category = "Manager" } - ) - "DataProcessor" = @( - @{ prefix = "DataProcessorObject"; category = "Object" } - @{ prefix = "DataProcessorManager"; category = "Manager" } - ) -} - -function Emit-InternalInfo { - param([string]$indent, [string]$objectType, [string]$objectName) - $types = $script:generatedTypes[$objectType] - if (-not $types) { return } - - X "$indent" - # ExchangePlan: ThisNode UUID before GeneratedTypes - if ($objectType -eq "ExchangePlan") { - X "$indent`t$(New-Guid-String)" - } - foreach ($gt in $types) { - $fullName = "$($gt.prefix).$objectName" - X "$indent`t" - X "$indent`t`t$(New-Guid-String)" - X "$indent`t`t$(New-Guid-String)" - X "$indent`t" - } - X "$indent" -} - -# --- 7. StandardAttributes --- - -$script:standardAttributesByType = @{ - "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") - "Document" = @("Posted","Ref","DeletionMark","Date","Number") - "Enum" = @("Order","Ref") - "InformationRegister" = @("Active","LineNumber","Recorder","Period") - "AccumulationRegister" = @("Active","LineNumber","Recorder","Period") - "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") - "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") - "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") - "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType") - "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") - "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") - "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") - "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") - "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") -} - -function Emit-StandardAttribute { - param([string]$indent, [string]$attrName) - X "$indent" - X "$indent`t" - X "$indent`tDontCheck" - X "$indent`tfalse" - X "$indent`tfalse" - X "$indent`tAuto" - X "$indent`t" - X "$indent`t" - X "$indent`tfalse" - X "$indent`t" - X "$indent`t" - X "$indent`tAuto" - X "$indent`tAuto" - X "$indent`t" - X "$indent`tfalse" - X "$indent`tUse" - X "$indent`tfalse" - X "$indent`t" - X "$indent`t" - X "$indent`t" - X "$indent`tUse" - X "$indent`t" - X "$indent`t" - X "$indent`t" - X "$indent`t" - X "$indent" -} - -function Emit-StandardAttributes { - param([string]$indent, [string]$objectType) - $attrs = $script:standardAttributesByType[$objectType] - if (-not $attrs) { return } - X "$indent" - foreach ($a in $attrs) { - Emit-StandardAttribute "$indent`t" $a - } - X "$indent" -} - -# TabularSection standard attributes (just LineNumber) -function Emit-TabularStandardAttributes { - param([string]$indent) - X "$indent" - Emit-StandardAttribute "$indent`t" "LineNumber" - X "$indent" -} - -# --- 8. Attribute emitter --- - -$script:reservedAttrNames = @{ - "Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование" - "Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец" - "IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных" - "Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность" - "Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый" - "Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача" - "Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс" - "ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого" - "CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись" - "Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый" -} - -function Emit-Attribute { - param([string]$indent, $parsed, [string]$context) - # $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register" - $attrName = $parsed.name - if ($context -notin @("tabular", "processor-tabular") -and - ($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) { - Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." - } - $uuid = New-Guid-String - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $parsed.name)" - Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym - X "$indent`t`t" - - # Type - $typeStr = $parsed.type - if ($typeStr) { - Emit-ValueType "$indent`t`t" $typeStr - } else { - # Default: unqualified string - X "$indent`t`t" - X "$indent`t`t`txs:string" - X "$indent`t`t" - } - - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } - X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - - # FillFromFillingValue — not for tabular/processor/chart/register-other - # (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these) - if ($context -notin @("tabular", "processor", "chart", "register-other")) { - X "$indent`t`tfalse" - } - - # FillValue — same restriction - if ($context -notin @("tabular", "processor", "chart", "register-other")) { - Emit-FillValue "$indent`t`t" $typeStr - } - - # FillChecking - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking } - X "$indent`t`t$fillChecking" - - X "$indent`t`tItems" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t`tAuto" - X "$indent`t`t" - X "$indent`t`t" - $chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" } - X "$indent`t`t$chi" - - # Use — only for catalog top-level attributes - if ($context -eq "catalog") { - X "$indent`t`tForItem" - } - - # Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular) - if ($context -notin @("processor", "processor-tabular")) { - $indexing = "DontIndex" - if ($parsed.flags -contains "index") { $indexing = "Index" } - if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } - if ($parsed.indexing) { $indexing = $parsed.indexing } - X "$indent`t`t$indexing" - - X "$indent`t`tUse" - # DataHistory — not for Chart* types and non-InformationRegister register family - if ($context -notin @("chart", "register-other")) { - X "$indent`t`tUse" - } - } - - X "$indent`t" - X "$indent" -} - -# --- 9. TabularSection emitter --- - -function Emit-TabularSection { - param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName) - $uuid = New-Guid-String - X "$indent" - - # InternalInfo for TabularSection - $typePrefix = "${objectType}TabularSection" - $rowPrefix = "${objectType}TabularSectionRow" - - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`t$(New-Guid-String)" - X "$indent`t`t`t$(New-Guid-String)" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t`t$(New-Guid-String)" - X "$indent`t`t`t$(New-Guid-String)" - X "$indent`t`t" - X "$indent`t" - - $tsSynonym = Split-CamelCase $tsName - - X "$indent`t" - X "$indent`t`t$(Esc-Xml $tsName)" - Emit-MLText "$indent`t`t" "Synonym" $tsSynonym - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tDontCheck" - Emit-TabularStandardAttributes "$indent`t`t" - # Use=ForItem only for Catalog tabular sections (Document does not have Use) - if ($objectType -eq "Catalog") { - X "$indent`t`tForItem" - } - X "$indent`t" - - $tsContext = if ($objectType -in @("DataProcessor","Report")) { "processor-tabular" } else { "tabular" } - X "$indent`t" - foreach ($col in $columns) { - $parsed = Parse-AttributeShorthand $col - Emit-Attribute "$indent`t`t" $parsed $tsContext - } - X "$indent`t" - - X "$indent" -} - -# --- 10. EnumValue emitter --- - -function Emit-EnumValue { - param([string]$indent, $parsed) - $uuid = New-Guid-String - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $parsed.name)" - Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym - X "$indent`t`t" - X "$indent`t" - X "$indent" -} - -# --- 11. Dimension emitter --- - -function Emit-Dimension { - param([string]$indent, $parsed, [string]$registerType) - # $registerType: "InformationRegister" or "AccumulationRegister" - $uuid = New-Guid-String - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $parsed.name)" - Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym - X "$indent`t`t" - - $typeStr = $parsed.type - if ($typeStr) { - Emit-ValueType "$indent`t`t" $typeStr - } else { - X "$indent`t`t" - X "$indent`t`t`txs:string" - X "$indent`t`t" - } - - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } - X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - - # InformationRegister dimensions have FillFromFillingValue - if ($registerType -eq "InformationRegister") { - $fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" } - X "$indent`t`t$fillFrom" - X "$indent`t`t" - } - - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - X "$indent`t`t$fillChecking" - - X "$indent`t`tItems" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t`tAuto" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - - # InformationRegister dimensions: Master, MainFilter, DenyIncompleteValues - if ($registerType -eq "InformationRegister") { - $master = if ($parsed.flags -contains "master") { "true" } else { "false" } - $mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" } - $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } - X "$indent`t`t$master" - X "$indent`t`t$mainFilter" - X "$indent`t`t$denyIncomplete" - } - - # AccumulationRegister dimensions: DenyIncompleteValues - if ($registerType -eq "AccumulationRegister") { - $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } - X "$indent`t`t$denyIncomplete" - } - - $indexing = "DontIndex" - if ($parsed.flags -contains "index") { $indexing = "Index" } - X "$indent`t`t$indexing" - - X "$indent`t`tUse" - - # AccumulationRegister dimensions: UseInTotals - if ($registerType -eq "AccumulationRegister") { - $useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" } - X "$indent`t`t$useInTotals" - } - - # InformationRegister dimensions: DataHistory - if ($registerType -eq "InformationRegister") { - X "$indent`t`tUse" - } - - X "$indent`t" - X "$indent" -} - -# --- 12. Resource emitter --- - -function Emit-Resource { - param([string]$indent, $parsed, [string]$registerType) - $uuid = New-Guid-String - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $parsed.name)" - Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym - X "$indent`t`t" - - $typeStr = $parsed.type - if ($typeStr) { - Emit-ValueType "$indent`t`t" $typeStr - } else { - X "$indent`t`t" - X "$indent`t`t`txs:decimal" - X "$indent`t`t`t" - X "$indent`t`t`t`t15" - X "$indent`t`t`t`t2" - X "$indent`t`t`t`tAny" - X "$indent`t`t`t" - X "$indent`t`t" - } - - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } - X "$indent`t`t$multiLine" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - - # InformationRegister resources have FillFromFillingValue, FillValue - if ($registerType -eq "InformationRegister") { - X "$indent`t`tfalse" - X "$indent`t`t" - } - - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - X "$indent`t`t$fillChecking" - - X "$indent`t`tItems" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t`tAuto" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - - # InformationRegister resources: Indexing, FullTextSearch, DataHistory - if ($registerType -eq "InformationRegister") { - X "$indent`t`tDontIndex" - X "$indent`t`tUse" - X "$indent`t`tUse" - } - - # AccumulationRegister resources: FullTextSearch (no Indexing, no DataHistory) - if ($registerType -eq "AccumulationRegister") { - X "$indent`t`tUse" - } - - X "$indent`t" - X "$indent" -} - -# --- 13. Property emitters per type --- - -function Emit-CatalogProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - $hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" } - $hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems" - X "$i$hierarchical" - X "$i$hierarchyType" - $limitLevelCount = if ($def.limitLevelCount -eq $true) { "true" } else { "false" } - $levelCount = if ($null -ne $def.levelCount) { "$($def.levelCount)" } else { "2" } - $foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" } - X "$i$limitLevelCount" - X "$i$levelCount" - X "$i$foldersOnTop" - X "$itrue" - if ($def.owners -and $def.owners.Count -gt 0) { - X "$i" - foreach ($ownerRef in $def.owners) { - $fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" } - X "$i`t$fullRef" - } - X "$i" - } else { - X "$i" - } - $subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems" - X "$i$subordinationUse" - - $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } - $codeType = Get-EnumProp "CodeType" "codeType" "String" - $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" - $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } - $checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" } - - X "$i$codeLength" - X "$i$descriptionLength" - X "$i$codeType" - X "$i$codeAllowedLength" - $codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog" - X "$i$codeSeries" - X "$i$checkUnique" - X "$i$autonumbering" - - $defaultPresentation = Get-EnumProp "DefaultPresentation" "defaultPresentation" "AsDescription" - X "$i$defaultPresentation" - - Emit-StandardAttributes $i "Catalog" - X "$i" - X "$iAuto" - X "$iInDialog" - $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } - $choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays" - X "$i$quickChoice" - X "$i$choiceMode" - X "$i" - X "$i`tCatalog.$objName.StandardAttribute.Description" - X "$i`tCatalog.$objName.StandardAttribute.Code" - X "$i" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-DocumentProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - X "$i" - - $numberType = Get-EnumProp "NumberType" "numberType" "String" - $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" } - $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" - $numberPeriodicity = if ($def.numberPeriodicity) { "$($def.numberPeriodicity)" } else { "Year" } - $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } - $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } - - X "$i$numberType" - X "$i$numberLength" - X "$i$numberAllowedLength" - X "$i$numberPeriodicity" - X "$i$checkUnique" - X "$i$autonumbering" - - Emit-StandardAttributes $i "Document" - X "$i" - - X "$i" - X "$i" - X "$i`tDocument.$objName.StandardAttribute.Number" - X "$i" - X "$iDontUse" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - - $posting = Get-EnumProp "Posting" "posting" "Allow" - $realTimePosting = Get-EnumProp "RealTimePosting" "realTimePosting" "Deny" - $registerRecordsDeletion = Get-EnumProp "RegisterRecordsDeletion" "registerRecordsDeletion" "AutoDelete" - $registerRecordsWritingOnPost = Get-EnumProp "RegisterRecordsWritingOnPost" "registerRecordsWritingOnPost" "WriteModified" - $sequenceFilling = if ($def.sequenceFilling) { "$($def.sequenceFilling)" } else { "AutoFill" } - $postInPrivilegedMode = if ($def.postInPrivilegedMode -eq $false) { "false" } else { "true" } - $unpostInPrivilegedMode = if ($def.unpostInPrivilegedMode -eq $false) { "false" } else { "true" } - - X "$i$posting" - X "$i$realTimePosting" - X "$i$registerRecordsDeletion" - X "$i$registerRecordsWritingOnPost" - X "$i$sequenceFilling" - - # RegisterRecords - $regRecords = @() - if ($def.registerRecords) { - foreach ($rr in $def.registerRecords) { - $rrStr = "$rr" - # Resolve Russian synonyms in register records - if ($rrStr.Contains('.')) { - $dotIdx = $rrStr.IndexOf('.') - $rrPrefix = $rrStr.Substring(0, $dotIdx) - $rrSuffix = $rrStr.Substring($dotIdx + 1) - if ($script:objectTypeSynonyms.ContainsKey($rrPrefix)) { - $rrPrefix = $script:objectTypeSynonyms[$rrPrefix] - } - $regRecords += "$rrPrefix.$rrSuffix" - } else { - $regRecords += $rrStr - } - } - } - - if ($regRecords.Count -gt 0) { - X "$i" - foreach ($rr in $regRecords) { - X "$i`t$rr" - } - X "$i" - } else { - X "$i" - } - - X "$i$postInPrivilegedMode" - X "$i$unpostInPrivilegedMode" - X "$ifalse" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-EnumProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$ifalse" - - Emit-StandardAttributes $i "Enum" - X "$i" - - $quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" } - X "$i$quickChoice" - X "$iBothWays" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iAuto" -} - -function Emit-ConstantProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - # Type - $valueType = Build-TypeStr $def - if (-not $valueType) { $valueType = "String" } - Emit-ValueType $i $valueType - - X "$itrue" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$ifalse" - X "$ifalse" - X "$i" - X "$i" - X "$iDontCheck" - X "$iItems" - X "$i" - X "$i" - X "$iAuto" - X "$i" - X "$i" - X "$iAuto" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-InformationRegisterProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - X "$iInDialog" - X "$i" - X "$i" - X "$i" - X "$i" - - Emit-StandardAttributes $i "InformationRegister" - - $periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Nonperiodical" - $writeMode = Get-EnumProp "WriteMode" "writeMode" "Independent" - - # MainFilterOnPeriod: auto based on periodicity unless explicitly set - $mainFilterOnPeriod = "false" - if ($null -ne $def.mainFilterOnPeriod) { - $mainFilterOnPeriod = if ($def.mainFilterOnPeriod -eq $true) { "true" } else { "false" } - } elseif ($periodicity -ne "Nonperiodical") { - $mainFilterOnPeriod = "true" - } - - X "$i$periodicity" - X "$i$writeMode" - X "$i$mainFilterOnPeriod" - X "$ifalse" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$ifalse" - X "$ifalse" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-AccumulationRegisterProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - X "$i" - X "$i" - - $registerType = Get-EnumProp "RegisterType" "registerType" "Balance" - X "$i$registerType" - - X "$ifalse" - - Emit-StandardAttributes $i "AccumulationRegister" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - $enableTotalsSplitting = if ($def.enableTotalsSplitting -eq $false) { "false" } else { "true" } - X "$i$enableTotalsSplitting" - - X "$i" - X "$i" - X "$i" -} - -# --- 13a. Wave 1: DefinedType, CommonModule, ScheduledJob, EventSubscription --- - -function Emit-DefinedTypeProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - # Type — composite type with multiple v8:Type entries (accept both valueType and valueTypes) - $valueTypes = @() - if ($def.valueTypes) { - $valueTypes = @($def.valueTypes) - } elseif ($def.valueType) { - $valueTypes = @($def.valueType) - } - if ($valueTypes.Count -gt 0) { - X "$i" - foreach ($vt in $valueTypes) { - $resolved = Resolve-TypeStr "$vt" - if ($resolved -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.') { - X "$i`td5p1:$resolved" - } elseif ($resolved -eq "Boolean") { - X "$i`txs:boolean" - } elseif ($resolved -match '^String') { - X "$i`txs:string" - X "$i`t" - X "$i`t`t0" - X "$i`t`tVariable" - X "$i`t" - } else { - X "$i`tcfg:$resolved" - } - } - X "$i" - } else { - X "$i" - } -} - -function Emit-CommonModuleProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - # Context shortcuts - $context = if ($def.context) { "$($def.context)" } else { "" } - - $global = if ($def.global -eq $true) { "true" } else { "false" } - $server = "false"; $serverCall = "false"; $clientManaged = "false" - $clientOrdinary = "false"; $externalConnection = "false"; $privileged = "false" - - switch ($context) { - "server" { $server = "true"; $serverCall = "true" } - "serverCall" { $server = "true"; $serverCall = "true" } - "client" { $clientManaged = "true" } - "serverClient" { $server = "true"; $clientManaged = "true" } - default { - if ($def.server -eq $true) { $server = "true" } - if ($def.serverCall -eq $true) { $serverCall = "true" } - if ($def.clientManagedApplication -eq $true) { $clientManaged = "true" } - if ($def.clientOrdinaryApplication -eq $true) { $clientOrdinary = "true" } - if ($def.externalConnection -eq $true) { $externalConnection = "true" } - if ($def.privileged -eq $true) { $privileged = "true" } - } - } - - X "$i$global" - X "$i$clientManaged" - X "$i$server" - X "$i$externalConnection" - X "$i$clientOrdinary" - X "$i$serverCall" - X "$i$privileged" - - $returnValuesReuse = Get-EnumProp "ReturnValuesReuse" "returnValuesReuse" "DontUse" - X "$i$returnValuesReuse" -} - -function Emit-ScheduledJobProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - $methodName = if ($def.methodName) { "$($def.methodName)" } else { "" } - # Ensure CommonModule. prefix - if ($methodName -and -not $methodName.StartsWith("CommonModule.")) { - $methodName = "CommonModule.$methodName" - } - X "$i$(Esc-Xml $methodName)" - - $description = if ($def.description) { "$($def.description)" } else { $synonym } - X "$i$(Esc-Xml $description)" - - $key = if ($def.key) { "$($def.key)" } else { "" } - X "$i$(Esc-Xml $key)" - - $use = if ($def.use -eq $true) { "true" } else { "false" } - X "$i$use" - - $predefined = if ($def.predefined -eq $true) { "true" } else { "false" } - X "$i$predefined" - - $restartCount = if ($null -ne $def.restartCountOnFailure) { "$($def.restartCountOnFailure)" } else { "3" } - $restartInterval = if ($null -ne $def.restartIntervalOnFailure) { "$($def.restartIntervalOnFailure)" } else { "10" } - X "$i$restartCount" - X "$i$restartInterval" -} - -function Emit-EventSubscriptionProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - # Source — array of v8:Type - $sources = @() - if ($def.source) { $sources = @($def.source) } - if ($sources.Count -gt 0) { - X "$i" - foreach ($src in $sources) { - $resolved = Resolve-TypeStr "$src" - X "$i`td5p1:$resolved" - } - X "$i" - } else { - X "$i" - } - - $event = if ($def.event) { "$($def.event)" } else { "BeforeWrite" } - X "$i$event" - - $handler = if ($def.handler) { "$($def.handler)" } else { "" } - # Ensure CommonModule. prefix - if ($handler -and -not $handler.StartsWith("CommonModule.")) { - $handler = "CommonModule.$handler" - } - X "$i$(Esc-Xml $handler)" -} - -# --- 13b. Wave 2: Report, DataProcessor --- - -function Emit-ReportProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } - if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } - - $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } - if ($auxForm) { X "$i$auxForm" } else { X "$i" } - - $mainDCS = if ($def.mainDataCompositionSchema) { "$($def.mainDataCompositionSchema)" } else { "" } - if ($mainDCS) { X "$i$mainDCS" } else { X "$i" } - - $defSettings = if ($def.defaultSettingsForm) { "$($def.defaultSettingsForm)" } else { "" } - if ($defSettings) { X "$i$defSettings" } else { X "$i" } - - $auxSettings = if ($def.auxiliarySettingsForm) { "$($def.auxiliarySettingsForm)" } else { "" } - if ($auxSettings) { X "$i$auxSettings" } else { X "$i" } - - $defVariant = if ($def.defaultVariantForm) { "$($def.defaultVariantForm)" } else { "" } - if ($defVariant) { X "$i$defVariant" } else { X "$i" } - - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" -} - -function Emit-DataProcessorProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$ifalse" - - $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } - if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } - - $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } - if ($auxForm) { X "$i$auxForm" } else { X "$i" } - - X "$ifalse" - X "$i" - X "$i" -} - -# --- 13c. Wave 3: ExchangePlan, ChartOfCharacteristicTypes, DocumentJournal --- - -function Emit-ExchangePlanProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" } - $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" - - X "$i$codeLength" - X "$i$codeAllowedLength" - X "$i$descriptionLength" - X "$iAsDescription" - X "$iInDialog" - - Emit-StandardAttributes $i "ExchangePlan" - - $distributed = if ($def.distributedInfoBase -eq $true) { "true" } else { "false" } - $includeExt = if ($def.includeConfigurationExtensions -eq $true) { "true" } else { "false" } - X "$i$distributed" - X "$i$includeExt" - - X "$i" - $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } - X "$i$quickChoice" - X "$iBothWays" - X "$i" - X "$i`tExchangePlan.$objName.StandardAttribute.Description" - X "$i`tExchangePlan.$objName.StandardAttribute.Code" - X "$i" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-ChartOfCharacteristicTypesProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } - $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" - $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } - $checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" } - - X "$i$codeLength" - X "$i$codeAllowedLength" - X "$i$descriptionLength" - X "$i$checkUnique" - X "$i$autonumbering" - X "$iAsDescription" - - # CharacteristicExtValues - $charExtValues = if ($def.characteristicExtValues) { "$($def.characteristicExtValues)" } else { "" } - if ($charExtValues) { X "$i$charExtValues" } - else { X "$i" } - - # Type — composite type of allowed characteristic value types - $valueTypes = @() - if ($def.valueTypes) { $valueTypes = @($def.valueTypes) } - if ($valueTypes.Count -gt 0) { - X "$i" - foreach ($vt in $valueTypes) { - Emit-TypeContent "$i`t" "$vt" - } - X "$i" - } else { - X "$i" - X "$i`txs:boolean" - X "$i`txs:string" - X "$i`t" - X "$i`t`t100" - X "$i`t`tVariable" - X "$i`t" - X "$i`txs:decimal" - X "$i`t" - X "$i`t`t15" - X "$i`t`t2" - X "$i`t`tAny" - X "$i`t" - X "$i`txs:dateTime" - X "$i`t" - X "$i`t`tDateTime" - X "$i`t" - X "$i" - } - - $hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" } - X "$i$hierarchical" - X "$itrue" - - Emit-StandardAttributes $i "ChartOfCharacteristicTypes" - X "$i" - X "$iAuto" - X "$iInDialog" - $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } - X "$i$quickChoice" - X "$iBothWays" - X "$i" - X "$i`tChartOfCharacteristicTypes.$objName.StandardAttribute.Description" - X "$i`tChartOfCharacteristicTypes.$objName.StandardAttribute.Code" - X "$i" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-DocumentJournalProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } - if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } - - $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } - if ($auxForm) { X "$i$auxForm" } else { X "$i" } - - X "$itrue" - - # RegisteredDocuments - $regDocs = @() - if ($def.registeredDocuments) { $regDocs = @($def.registeredDocuments) } - if ($regDocs.Count -gt 0) { - X "$i" - foreach ($rd in $regDocs) { - $rdStr = "$rd" - # Resolve Russian synonyms: Документ.Xxx → Document.Xxx - if ($rdStr.Contains('.')) { - $dotIdx = $rdStr.IndexOf('.') - $rdPrefix = $rdStr.Substring(0, $dotIdx) - $rdSuffix = $rdStr.Substring($dotIdx + 1) - if ($script:objectTypeSynonyms.ContainsKey($rdPrefix)) { - $rdPrefix = $script:objectTypeSynonyms[$rdPrefix] - } - $rdStr = "$rdPrefix.$rdSuffix" - } - X "$i`t$rdStr" - } - X "$i" - } else { - X "$i" - } - - Emit-StandardAttributes $i "DocumentJournal" - - X "$i" - X "$i" - X "$i" -} - -# --- 13d. Wave 4: ChartOfAccounts, AccountingRegister, ChartOfCalculationTypes, CalculationRegister --- - -function Emit-ChartOfAccountsProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - # ExtDimensionTypes - $extDimTypes = if ($def.extDimensionTypes) { "$($def.extDimensionTypes)" } else { "" } - if ($extDimTypes) { X "$i$extDimTypes" } - else { X "$i" } - - $maxExtDim = if ($null -ne $def.maxExtDimensionCount) { "$($def.maxExtDimensionCount)" } else { "3" } - X "$i$maxExtDim" - - $codeMask = if ($def.codeMask) { "$($def.codeMask)" } else { "" } - if ($codeMask) { X "$i$codeMask" } else { X "$i" } - - $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "8" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "120" } - $codeSeries = if ($def.codeSeries) { "$($def.codeSeries)" } else { "WholeChartOfAccounts" } - $autoOrder = if ($def.autoOrderByCode -eq $false) { "false" } else { "true" } - $orderLength = if ($null -ne $def.orderLength) { "$($def.orderLength)" } else { "5" } - - X "$i$codeLength" - X "$i$descriptionLength" - X "$i$codeSeries" - X "$ifalse" - X "$iAsDescription" - X "$i$autoOrder" - X "$i$orderLength" - - X "$iInDialog" - - Emit-StandardAttributes $i "ChartOfAccounts" - - # StandardTabularSections — ExtDimensionTypes - X "$i" - X "$i`t" - X "$i`t`t" - foreach ($stAttr in @("TurnoversOnly","Predefined","ExtDimensionType","LineNumber")) { - Emit-StandardAttribute "$i`t`t`t" $stAttr - } - X "$i`t`t" - X "$i`t" - X "$i" - - X "$i" - X "$iAuto" - $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } - X "$i$quickChoice" - X "$iBothWays" - X "$i" - X "$i`tChartOfAccounts.$objName.StandardAttribute.Description" - X "$i`tChartOfAccounts.$objName.StandardAttribute.Code" - X "$i" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-AccountingRegisterProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - X "$i" - X "$i" - - $chartOfAccounts = if ($def.chartOfAccounts) { "$($def.chartOfAccounts)" } else { "" } - if ($chartOfAccounts) { X "$i$chartOfAccounts" } - else { X "$i" } - - $correspondence = if ($def.correspondence -eq $true) { "true" } else { "false" } - X "$i$correspondence" - - $periodAdjLen = if ($null -ne $def.periodAdjustmentLength) { "$($def.periodAdjustmentLength)" } else { "0" } - X "$i$periodAdjLen" - - X "$ifalse" - - Emit-StandardAttributes $i "AccountingRegister" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" -} - -function Emit-ChartOfCalculationTypesProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } - $codeType = Get-EnumProp "CodeType" "codeType" "String" - $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" - - X "$i$codeLength" - X "$i$codeType" - X "$i$codeAllowedLength" - X "$i$descriptionLength" - X "$iAsDescription" - - $dependence = Get-EnumProp "DependenceOnCalculationTypes" "dependenceOnCalculationTypes" "DontUse" - X "$i$dependence" - - # BaseCalculationTypes - $baseTypes = @() - if ($def.baseCalculationTypes) { $baseTypes = @($def.baseCalculationTypes) } - if ($baseTypes.Count -gt 0) { - X "$i" - foreach ($bt in $baseTypes) { - X "$i`t$bt" - } - X "$i" - } else { - X "$i" - } - - $actionPeriodUse = if ($def.actionPeriodUse -eq $true) { "true" } else { "false" } - X "$i$actionPeriodUse" - - Emit-StandardAttributes $i "ChartOfCalculationTypes" - X "$i" - X "$iAuto" - X "$iInDialog" - $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } - X "$i$quickChoice" - X "$iBothWays" - X "$i" - X "$i`tChartOfCalculationTypes.$objName.StandardAttribute.Description" - X "$i`tChartOfCalculationTypes.$objName.StandardAttribute.Code" - X "$i" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iDontUse" - X "$iAuto" -} - -function Emit-CalculationRegisterProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - X "$i" - X "$i" - - $chartOfCalcTypes = if ($def.chartOfCalculationTypes) { "$($def.chartOfCalculationTypes)" } else { "" } - if ($chartOfCalcTypes) { X "$i$chartOfCalcTypes" } - else { X "$i" } - - $periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Month" - X "$i$periodicity" - - $actionPeriod = if ($def.actionPeriod -eq $true) { "true" } else { "false" } - X "$i$actionPeriod" - - $basePeriod = if ($def.basePeriod -eq $true) { "true" } else { "false" } - X "$i$basePeriod" - - $schedule = if ($def.schedule) { "$($def.schedule)" } else { "" } - if ($schedule) { X "$i$schedule" } else { X "$i" } - - $scheduleValue = if ($def.scheduleValue) { "$($def.scheduleValue)" } else { "" } - if ($scheduleValue) { X "$i$scheduleValue" } else { X "$i" } - - $scheduleDate = if ($def.scheduleDate) { "$($def.scheduleDate)" } else { "" } - if ($scheduleDate) { X "$i$scheduleDate" } else { X "$i" } - - X "$ifalse" - - Emit-StandardAttributes $i "CalculationRegister" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" -} - -# --- 13e. Wave 5: BusinessProcess, Task --- - -function Emit-BusinessProcessProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $editType = Get-EnumProp "EditType" "editType" "InDialog" - X "$i$editType" - - $numberType = Get-EnumProp "NumberType" "numberType" "String" - $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" } - $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" - $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } - $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } - - X "$i$numberType" - X "$i$numberLength" - X "$i$numberAllowedLength" - X "$i$checkUnique" - X "$i$autonumbering" - - Emit-StandardAttributes $i "BusinessProcess" - X "$i" - - $task = if ($def.task) { "$($def.task)" } else { "" } - if ($task) { - X "$i$task" - } else { - X "$i" - } - - X "$i" - X "$i" - X "$i`tBusinessProcess.$objName.StandardAttribute.Number" - X "$i" - X "$iDontUse" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -function Emit-TaskProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - X "$itrue" - - $numberType = Get-EnumProp "NumberType" "numberType" "String" - $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "14" } - $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" - $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } - $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } - - $taskNumberAutoPrefix = if ($def.taskNumberAutoPrefix) { "$($def.taskNumberAutoPrefix)" } else { "BusinessProcessNumber" } - $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "150" } - - X "$i$numberType" - X "$i$numberLength" - X "$i$numberAllowedLength" - X "$i$checkUnique" - X "$i$autonumbering" - X "$i$taskNumberAutoPrefix" - X "$i$descriptionLength" - - # Addressing - $addressing = if ($def.addressing) { "$($def.addressing)" } else { "" } - if ($addressing) { X "$i$addressing" } else { X "$i" } - - $mainAddressing = if ($def.mainAddressingAttribute) { "$($def.mainAddressingAttribute)" } else { "" } - if ($mainAddressing) { X "$i$mainAddressing" } else { X "$i" } - - $currentPerformer = if ($def.currentPerformer) { "$($def.currentPerformer)" } else { "" } - if ($currentPerformer) { X "$i$currentPerformer" } else { X "$i" } - - Emit-StandardAttributes $i "Task" - X "$i" - - X "$i" - X "$i" - X "$i`tTask.$objName.StandardAttribute.Number" - X "$i" - X "$iDontUse" - X "$iBegin" - X "$iDontUse" - X "$iDirectly" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$ifalse" - X "$i" - - $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" - X "$i$dataLockControlMode" - - $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" - X "$i$fullTextSearch" - - X "$i" - X "$i" - X "$i" - X "$i" - X "$i" - X "$iAuto" - X "$iDontUse" - X "$ifalse" - X "$ifalse" -} - -# --- 13f. Wave 6: HTTPService, WebService --- - -function Emit-HTTPServiceProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - $rootURL = if ($def.rootURL) { "$($def.rootURL)" } else { $objName.ToLower() } - X "$i$(Esc-Xml $rootURL)" - - $reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse" - X "$i$reuseSessions" - - $sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" } - X "$i$sessionMaxAge" -} - -function Emit-WebServiceProperties { - param([string]$indent) - $i = $indent - - X "$i$(Esc-Xml $objName)" - Emit-MLText $i "Synonym" $synonym - X "$i" - - $namespace = if ($def.namespace) { "$($def.namespace)" } else { "" } - X "$i$(Esc-Xml $namespace)" - - $xdtoPackages = if ($def.xdtoPackages) { "$($def.xdtoPackages)" } else { "" } - if ($xdtoPackages) { X "$i$xdtoPackages" } else { X "$i" } - - $reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse" - X "$i$reuseSessions" - - $sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" } - X "$i$sessionMaxAge" -} - -# --- 13g. ChildObjects emitters for new types --- - -function Emit-Column { - param([string]$indent, $colDef) - $uuid = New-Guid-String - - $name = "" - $synonym = "" - $indexing = "DontIndex" - $references = @() - - if ($colDef -is [string]) { - $name = "$colDef" - $synonym = Split-CamelCase $name - } else { - $name = "$($colDef.name)" - $synonym = if ($colDef.synonym) { "$($colDef.synonym)" } else { Split-CamelCase $name } - if ($colDef.indexing) { $indexing = "$($colDef.indexing)" } - if ($colDef.references) { $references = @($colDef.references) } - } - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $name)" - Emit-MLText "$indent`t`t" "Synonym" $synonym - X "$indent`t`t" - X "$indent`t`t$indexing" - if ($references.Count -gt 0) { - X "$indent`t`t" - foreach ($ref in $references) { - X "$indent`t`t`t$ref" - } - X "$indent`t`t" - } else { - X "$indent`t`t" - } - X "$indent`t" - X "$indent" -} - -function Emit-AccountingFlag { - param([string]$indent, [string]$flagName) - $uuid = New-Guid-String - $flagSynonym = Split-CamelCase $flagName - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $flagName)" - Emit-MLText "$indent`t`t" "Synonym" $flagSynonym - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t`txs:boolean" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tDontCheck" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t" - X "$indent" -} - -function Emit-ExtDimensionAccountingFlag { - param([string]$indent, [string]$flagName) - $uuid = New-Guid-String - $flagSynonym = Split-CamelCase $flagName - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $flagName)" - Emit-MLText "$indent`t`t" "Synonym" $flagSynonym - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t`txs:boolean" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`tfalse" - X "$indent`t`tfalse" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tDontCheck" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t`t" - X "$indent`t`t" - X "$indent`t`tAuto" - X "$indent`t" - X "$indent" -} - -function Emit-URLTemplate { - param([string]$indent, [string]$tmplName, $tmplDef) - $uuid = New-Guid-String - $tmplSynonym = Split-CamelCase $tmplName - - $template = "" - $methods = @{} - - if ($tmplDef -is [string]) { - $template = "$tmplDef" - } else { - $template = if ($tmplDef.template) { "$($tmplDef.template)" } else { "/$($tmplName.ToLower())" } - if ($tmplDef.methods) { - $tmplDef.methods.PSObject.Properties | ForEach-Object { - $methods[$_.Name] = "$($_.Value)" - } - } - } - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $tmplName)" - Emit-MLText "$indent`t`t" "Synonym" $tmplSynonym - X "$indent`t`t" - X "$indent`t" - - if ($methods.Count -gt 0) { - X "$indent`t" - foreach ($methodName in $methods.Keys) { - $methodUuid = New-Guid-String - $httpMethod = $methods[$methodName] - $methodSynonym = Split-CamelCase $methodName - $handler = "${tmplName}${methodName}" - - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`t$(Esc-Xml $methodName)" - Emit-MLText "$indent`t`t`t`t" "Synonym" $methodSynonym - X "$indent`t`t`t`t$httpMethod" - X "$indent`t`t`t`t$(Esc-Xml $handler)" - X "$indent`t`t`t" - X "$indent`t`t" - } - X "$indent`t" - } else { - X "$indent`t" - } - - X "$indent" -} - -function Emit-Operation { - param([string]$indent, [string]$opName, $opDef) - $uuid = New-Guid-String - $opSynonym = Split-CamelCase $opName - - $returnType = "xs:string" - $nillable = "false" - $transactioned = "false" - $handler = $opName - $params = @{} - - if ($opDef -is [string]) { - $returnType = "$opDef" - } else { - if ($opDef.returnType) { $returnType = "$($opDef.returnType)" } - if ($opDef.nillable -eq $true) { $nillable = "true" } - if ($opDef.transactioned -eq $true) { $transactioned = "true" } - if ($opDef.handler) { $handler = "$($opDef.handler)" } - if ($opDef.parameters) { - $opDef.parameters.PSObject.Properties | ForEach-Object { - $params[$_.Name] = $_.Value - } - } - } - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $opName)" - Emit-MLText "$indent`t`t" "Synonym" $opSynonym - X "$indent`t`t" - X "$indent`t`t$returnType" - X "$indent`t`t$nillable" - X "$indent`t`t$transactioned" - X "$indent`t`t$(Esc-Xml $handler)" - X "$indent`t" - - if ($params.Count -gt 0) { - X "$indent`t" - foreach ($paramName in $params.Keys) { - $paramUuid = New-Guid-String - $paramDef = $params[$paramName] - $paramSynonym = Split-CamelCase $paramName - $paramType = "xs:string" - $paramNillable = "true" - $paramDir = "In" - - if ($paramDef -is [string]) { - $paramType = "$paramDef" - } else { - if ($paramDef.type) { $paramType = "$($paramDef.type)" } - if ($paramDef.nillable -eq $false) { $paramNillable = "false" } - if ($paramDef.direction) { $paramDir = "$($paramDef.direction)" } - } - - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`t$(Esc-Xml $paramName)" - Emit-MLText "$indent`t`t`t`t" "Synonym" $paramSynonym - X "$indent`t`t`t`t$paramType" - X "$indent`t`t`t`t$paramNillable" - X "$indent`t`t`t`t$paramDir" - X "$indent`t`t`t" - X "$indent`t`t" - } - X "$indent`t" - } else { - X "$indent`t" - } - - X "$indent" -} - -function Emit-AddressingAttribute { - param([string]$indent, $addrDef) - $uuid = New-Guid-String - - $name = "" - $attrSynonym = "" - $typeStr = "" - $addressingDimension = "" - $indexing = "Index" - - $parsed = Parse-AttributeShorthand $addrDef - $name = $parsed.name - $attrSynonym = $parsed.synonym - $typeStr = $parsed.type - if ($addrDef -isnot [string]) { - if ($addrDef.addressingDimension) { $addressingDimension = "$($addrDef.addressingDimension)" } - if ($addrDef.indexing) { $indexing = "$($addrDef.indexing)" } - } - - X "$indent" - X "$indent`t" - X "$indent`t`t$(Esc-Xml $name)" - Emit-MLText "$indent`t`t" "Synonym" $attrSynonym - X "$indent`t`t" - - if ($typeStr) { - Emit-ValueType "$indent`t`t" $typeStr - } else { - X "$indent`t`t" - X "$indent`t`t`txs:string" - X "$indent`t`t" - } - - if ($addressingDimension) { - X "$indent`t`t$addressingDimension" - } else { - X "$indent`t`t" - } - - X "$indent`t`t$indexing" - X "$indent`t`tUse" - X "$indent`t`tUse" - X "$indent`t" - X "$indent" -} - -# --- 14. Namespaces --- - -$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' - -# --- 14a. Detect format version from existing Configuration.xml --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$script:formatVersion = Detect-FormatVersion $OutputDir - -# --- 15. Main assembler --- - -$uuid = New-Guid-String - -# XML declaration -X '' -X "" -X "`t<$objType uuid=`"$uuid`">" - -# InternalInfo -Emit-InternalInfo "`t`t" $objType $objName - -# Properties -X "`t`t" - -switch ($objType) { - "Catalog" { Emit-CatalogProperties "`t`t`t" } - "Document" { Emit-DocumentProperties "`t`t`t" } - "Enum" { Emit-EnumProperties "`t`t`t" } - "Constant" { Emit-ConstantProperties "`t`t`t" } - "InformationRegister" { Emit-InformationRegisterProperties "`t`t`t" } - "AccumulationRegister" { Emit-AccumulationRegisterProperties "`t`t`t" } - "DefinedType" { Emit-DefinedTypeProperties "`t`t`t" } - "CommonModule" { Emit-CommonModuleProperties "`t`t`t" } - "ScheduledJob" { Emit-ScheduledJobProperties "`t`t`t" } - "EventSubscription" { Emit-EventSubscriptionProperties "`t`t`t" } - "Report" { Emit-ReportProperties "`t`t`t" } - "DataProcessor" { Emit-DataProcessorProperties "`t`t`t" } - "ExchangePlan" { Emit-ExchangePlanProperties "`t`t`t" } - "ChartOfCharacteristicTypes" { Emit-ChartOfCharacteristicTypesProperties "`t`t`t" } - "DocumentJournal" { Emit-DocumentJournalProperties "`t`t`t" } - "ChartOfAccounts" { Emit-ChartOfAccountsProperties "`t`t`t" } - "AccountingRegister" { Emit-AccountingRegisterProperties "`t`t`t" } - "ChartOfCalculationTypes" { Emit-ChartOfCalculationTypesProperties "`t`t`t" } - "CalculationRegister" { Emit-CalculationRegisterProperties "`t`t`t" } - "BusinessProcess" { Emit-BusinessProcessProperties "`t`t`t" } - "Task" { Emit-TaskProperties "`t`t`t" } - "HTTPService" { Emit-HTTPServiceProperties "`t`t`t" } - "WebService" { Emit-WebServiceProperties "`t`t`t" } -} - -X "`t`t" - -# ChildObjects -$hasChildren = $false - -# --- Types with Attributes + TabularSections --- -$typesWithAttrTS = @("Catalog","Document","Report","DataProcessor","ExchangePlan", - "ChartOfCharacteristicTypes","ChartOfAccounts","ChartOfCalculationTypes", - "BusinessProcess","Task") - -if ($objType -in $typesWithAttrTS) { - $attrs = @() - if ($def.attributes) { - foreach ($a in $def.attributes) { - $attrs += Parse-AttributeShorthand $a - } - } - $tsSections = [ordered]@{} - if ($def.tabularSections) { - # Normalize array format: [{name:"X", attributes:[...]}, ...] → {"X": [...]} - if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") { - foreach ($ts in $def.tabularSections) { - $tsName = $ts.name - $tsCols = if ($ts.attributes) { @($ts.attributes) } else { @() } - $tsSections[$tsName] = $tsCols - } - } else { - $def.tabularSections.PSObject.Properties | ForEach-Object { - $tsSections[$_.Name] = @($_.Value) - } - } - } - - # ChartOfAccounts: AccountingFlags + ExtDimensionAccountingFlags - $acctFlags = @() - $extDimFlags = @() - if ($objType -eq "ChartOfAccounts") { - if ($def.accountingFlags) { $acctFlags = @($def.accountingFlags) } - if ($def.extDimensionAccountingFlags) { $extDimFlags = @($def.extDimensionAccountingFlags) } - } - - # Task: AddressingAttributes - $addrAttrs = @() - if ($objType -eq "Task" -and $def.addressingAttributes) { - $addrAttrs = @($def.addressingAttributes) - } - - $childCount = $attrs.Count + $tsSections.Count + $acctFlags.Count + $extDimFlags.Count + $addrAttrs.Count - if ($childCount -gt 0) { - $hasChildren = $true - X "`t`t" - $context = switch ($objType) { - "Catalog" { "catalog" } - "Document" { "document" } - { $_ -in @("DataProcessor","Report") } { "processor" } - { $_ -in @("ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes") } { "chart" } - default { "object" } - } - foreach ($a in $attrs) { - Emit-Attribute "`t`t`t" $a $context - } - foreach ($tsName in $tsSections.Keys) { - $columns = $tsSections[$tsName] - Emit-TabularSection "`t`t`t" $tsName $columns $objType $objName - } - foreach ($af in $acctFlags) { - $afName = if ($af.name) { $af.name } else { "$af" } - Emit-AccountingFlag "`t`t`t" $afName - } - foreach ($edf in $extDimFlags) { - $edfName = if ($edf.name) { $edf.name } else { "$edf" } - Emit-ExtDimensionAccountingFlag "`t`t`t" $edfName - } - foreach ($aa in $addrAttrs) { - Emit-AddressingAttribute "`t`t`t" $aa - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- Enum: enum values --- -if ($objType -eq "Enum") { - $values = @() - if ($def.values) { - foreach ($v in $def.values) { - $values += Parse-EnumValueShorthand $v - } - } - if ($values.Count -gt 0) { - $hasChildren = $true - X "`t`t" - foreach ($v in $values) { - Emit-EnumValue "`t`t`t" $v - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- Constant, DefinedType, ScheduledJob, EventSubscription: no ChildObjects --- - -# --- Registers: dimensions + resources + attributes --- -if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister")) { - $dims = @() - $resources = @() - $regAttrs = @() - if ($def.dimensions) { - foreach ($d in $def.dimensions) { - $dims += Parse-AttributeShorthand $d - } - } - if ($def.resources) { - foreach ($r in $def.resources) { - $resources += Parse-AttributeShorthand $r - } - } - if ($def.attributes) { - foreach ($a in $def.attributes) { - $regAttrs += Parse-AttributeShorthand $a - } - } - - if ($dims.Count -gt 0 -or $resources.Count -gt 0 -or $regAttrs.Count -gt 0) { - $hasChildren = $true - X "`t`t" - foreach ($r in $resources) { - Emit-Resource "`t`t`t" $r $objType - } - foreach ($d in $dims) { - Emit-Dimension "`t`t`t" $d $objType - } - # InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory; - # AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT. - $regCtx = if ($objType -eq "InformationRegister") { "register-info" } else { "register-other" } - foreach ($a in $regAttrs) { - Emit-Attribute "`t`t`t" $a $regCtx - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- DocumentJournal: columns --- -if ($objType -eq "DocumentJournal") { - $columns = @() - if ($def.columns) { $columns = @($def.columns) } - if ($columns.Count -gt 0) { - $hasChildren = $true - X "`t`t" - foreach ($col in $columns) { - Emit-Column "`t`t`t" $col - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- HTTPService: URLTemplates --- -if ($objType -eq "HTTPService") { - $urlTemplates = @{} - if ($def.urlTemplates) { - $def.urlTemplates.PSObject.Properties | ForEach-Object { - $urlTemplates[$_.Name] = $_.Value - } - } - if ($urlTemplates.Count -gt 0) { - $hasChildren = $true - X "`t`t" - foreach ($tmplName in $urlTemplates.Keys) { - Emit-URLTemplate "`t`t`t" $tmplName $urlTemplates[$tmplName] - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- WebService: Operations --- -if ($objType -eq "WebService") { - $operations = @{} - if ($def.operations) { - $def.operations.PSObject.Properties | ForEach-Object { - $operations[$_.Name] = $_.Value - } - } - if ($operations.Count -gt 0) { - $hasChildren = $true - X "`t`t" - foreach ($opName in $operations.Keys) { - Emit-Operation "`t`t`t" $opName $operations[$opName] - } - X "`t`t" - } else { - X "`t`t" - } -} - -# --- CommonModule: no ChildObjects --- - -X "`t" -X "" - -$metadataXml = $script:xml.ToString() - -# --- 16. Write files --- - -# Type → plural directory mapping -$script:typePluralMap = @{ - "Catalog" = "Catalogs" - "Document" = "Documents" - "Enum" = "Enums" - "Constant" = "Constants" - "InformationRegister" = "InformationRegisters" - "AccumulationRegister" = "AccumulationRegisters" - "AccountingRegister" = "AccountingRegisters" - "CalculationRegister" = "CalculationRegisters" - "ChartOfAccounts" = "ChartsOfAccounts" - "ChartOfCharacteristicTypes"= "ChartsOfCharacteristicTypes" - "ChartOfCalculationTypes" = "ChartsOfCalculationTypes" - "BusinessProcess" = "BusinessProcesses" - "Task" = "Tasks" - "ExchangePlan" = "ExchangePlans" - "DocumentJournal" = "DocumentJournals" - "Report" = "Reports" - "DataProcessor" = "DataProcessors" - "CommonModule" = "CommonModules" - "ScheduledJob" = "ScheduledJobs" - "EventSubscription" = "EventSubscriptions" - "HTTPService" = "HTTPServices" - "WebService" = "WebServices" - "DefinedType" = "DefinedTypes" -} - -$typePlural = $script:typePluralMap[$objType] -$typeDir = Join-Path $OutputDir $typePlural - -# Main XML file: {OutputDir}/{TypePlural}/{Name}.xml -$mainXmlPath = Join-Path $typeDir "$objName.xml" - -# Types that don't have subdirectory structure (no Ext/, no modules) -$typesNoSubDir = @("DefinedType","ScheduledJob","EventSubscription") - -# Object subdirectory: {OutputDir}/{TypePlural}/{Name}/Ext/ -$objSubDir = Join-Path $typeDir $objName -$extDir = Join-Path $objSubDir "Ext" - -if (-not (Test-Path $typeDir)) { - New-Item -ItemType Directory -Path $typeDir -Force | Out-Null -} -if ($objType -notin $typesNoSubDir) { - if (-not (Test-Path $objSubDir)) { - New-Item -ItemType Directory -Path $objSubDir -Force | Out-Null - } -} - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($mainXmlPath, $metadataXml, $enc) - -# Module files -$modulesCreated = @() - -# Helper: create Ext/ only when needed (avoids empty Ext/ for Constant, Enum, etc.) -function Ensure-ExtDir { - if (-not (Test-Path $extDir)) { - New-Item -ItemType Directory -Path $extDir -Force | Out-Null - } -} - -# Types with ObjectModule.bsl -$typesWithObjectModule = @("Catalog","Document","Report","DataProcessor","ExchangePlan", - "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", - "BusinessProcess","Task") -# Types with RecordSetModule.bsl -$typesWithRecordSetModule = @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") -# Types with ManagerModule.bsl -$typesWithManagerModule = @("Report","DataProcessor","Constant","Enum") -# Types with ValueManagerModule.bsl -$typesWithValueManagerModule = @("Constant") -# Types with Module.bsl (general) -$typesWithModule = @("CommonModule","HTTPService","WebService") - -if ($objType -in $typesWithObjectModule) { - $modulePath = Join-Path $extDir "ObjectModule.bsl" - if (-not (Test-Path $modulePath)) { - Ensure-ExtDir - [System.IO.File]::WriteAllText($modulePath, "", $enc) - $modulesCreated += $modulePath - } -} -if ($objType -in $typesWithManagerModule) { - $modulePath = Join-Path $extDir "ManagerModule.bsl" - if (-not (Test-Path $modulePath)) { - Ensure-ExtDir - [System.IO.File]::WriteAllText($modulePath, "", $enc) - $modulesCreated += $modulePath - } -} -if ($objType -in $typesWithValueManagerModule) { - $modulePath = Join-Path $extDir "ValueManagerModule.bsl" - if (-not (Test-Path $modulePath)) { - Ensure-ExtDir - [System.IO.File]::WriteAllText($modulePath, "", $enc) - $modulesCreated += $modulePath - } -} -if ($objType -in $typesWithRecordSetModule) { - $modulePath = Join-Path $extDir "RecordSetModule.bsl" - if (-not (Test-Path $modulePath)) { - Ensure-ExtDir - [System.IO.File]::WriteAllText($modulePath, "", $enc) - $modulesCreated += $modulePath - } -} -if ($objType -in $typesWithModule) { - $modulePath = Join-Path $extDir "Module.bsl" - if (-not (Test-Path $modulePath)) { - Ensure-ExtDir - [System.IO.File]::WriteAllText($modulePath, "", $enc) - $modulesCreated += $modulePath - } -} - -# Special files -if ($objType -eq "ExchangePlan") { - $contentPath = Join-Path $extDir "Content.xml" - if (-not (Test-Path $contentPath)) { - Ensure-ExtDir - $contentXml = "`r`n`r`n" - [System.IO.File]::WriteAllText($contentPath, $contentXml, $enc) - $modulesCreated += $contentPath - } -} -if ($objType -eq "BusinessProcess") { - $flowchartPath = Join-Path $extDir "Flowchart.xml" - if (-not (Test-Path $flowchartPath)) { - Ensure-ExtDir - $flowchartXml = "`r`n`r`n" - [System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc) - $modulesCreated += $flowchartPath - } -} - -# --- 17. Register in Configuration.xml --- - -$configXmlPath = Join-Path $OutputDir "Configuration.xml" -$regResult = $null - -# XML tag name for Configuration.xml ChildObjects -$childTag = $objType - -if (Test-Path $configXmlPath) { - $configDoc = New-Object System.Xml.XmlDocument - $configDoc.PreserveWhitespace = $true - $configDoc.Load($configXmlPath) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable) - $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr) - if ($childObjects) { - $existing = $childObjects.SelectNodes("md:$childTag", $nsMgr) - $alreadyExists = $false - foreach ($e in $existing) { - if ($e.InnerText -eq $objName) { - $alreadyExists = $true - break - } - } - - if ($alreadyExists) { - $regResult = "already" - } else { - $newElem = $configDoc.CreateElement($childTag, "http://v8.1c.ru/8.3/MDClasses") - $newElem.InnerText = $objName - - if ($existing.Count -gt 0) { - # Insert after last existing element of same type - $lastElem = $existing[$existing.Count - 1] - $newWs = $configDoc.CreateWhitespace("`n`t`t`t") - $childObjects.InsertAfter($newWs, $lastElem) | Out-Null - $childObjects.InsertAfter($newElem, $newWs) | Out-Null - } else { - # No existing elements of this type — insert before closing whitespace - $lastChild = $childObjects.LastChild - if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { - $newWs = $configDoc.CreateWhitespace("`n`t`t`t") - $childObjects.InsertBefore($newWs, $lastChild) | Out-Null - $childObjects.InsertBefore($newElem, $lastChild) | Out-Null - } else { - $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null - $childObjects.AppendChild($newElem) | Out-Null - $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null - } - } - - # Save - $cfgSettings = New-Object System.Xml.XmlWriterSettings - $cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true) - $cfgSettings.Indent = $false - $stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create) - $writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings) - $configDoc.Save($writer) - $writer.Close() - $stream.Close() - - $regResult = "added" - } - } else { - $regResult = "no-childobj" - } -} else { - $regResult = "no-config" -} - -# --- 18. Summary --- - -$attrCount = 0 -$tsCount = 0 -$dimCount = 0 -$resCount = 0 -$valCount = 0 -$colCount = 0 - -if ($def.attributes) { $attrCount = @($def.attributes).Count } -if ($def.tabularSections) { - if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") { - $tsCount = @($def.tabularSections).Count - } else { - $tsCount = @($def.tabularSections.PSObject.Properties).Count - } -} -if ($def.dimensions) { $dimCount = @($def.dimensions).Count } -if ($def.resources) { $resCount = @($def.resources).Count } -if ($def.values) { $valCount = @($def.values).Count } -if ($def.columns) { $colCount = @($def.columns).Count } - -Write-Host "[OK] $objType '$objName' compiled" -Write-Host " UUID: $uuid" -Write-Host " File: $mainXmlPath" - -$details = @() -if ($attrCount -gt 0) { $details += "Attributes: $attrCount" } -if ($tsCount -gt 0) { $details += "TabularSections: $tsCount" } -if ($dimCount -gt 0) { $details += "Dimensions: $dimCount" } -if ($resCount -gt 0) { $details += "Resources: $resCount" } -if ($valCount -gt 0) { $details += "Values: $valCount" } -if ($colCount -gt 0) { $details += "Columns: $colCount" } - -if ($details.Count -gt 0) { - Write-Host " $($details -join ', ')" -} - -foreach ($mc in $modulesCreated) { - Write-Host " Module: $mc" -} - -switch ($regResult) { - "added" { Write-Host " Configuration.xml: <$childTag>$objName added to ChildObjects" } - "already" { Write-Host " Configuration.xml: <$childTag>$objName already registered" } - "no-childobj" { Write-Warning "Configuration.xml found but not found" } - "no-config" { Write-Host " Configuration.xml: not found at $configXmlPath (register manually)" } -} - -# Cross-reference hints -if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) { - Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:" - Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" -} -if ($objType -eq "CalculationRegister" -and -not $def.chartOfCalculationTypes) { - Write-Host "[HINT] CalculationRegister requires ChartOfCalculationTypes reference:" - Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`"" -} -if ($objType -eq "BusinessProcess" -and -not $def.task) { - Write-Host "[HINT] BusinessProcess requires Task reference:" - Write-Host " /meta-edit -Operation modify-property -Value `"Task=Task.XXX`"" -} -if ($objType -eq "ChartOfAccounts") { - $maxExtDim = if ($null -ne $def.maxExtDimensionCount) { [int]$def.maxExtDimensionCount } else { 0 } - if ($maxExtDim -gt 0 -and -not $def.extDimensionTypes) { - Write-Host "[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:" - Write-Host " /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`"" - } -} +# meta-compile v1.14 — Compile 1C metadata object from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputDir +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Load and validate JSON --- + +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$def = $json | ConvertFrom-Json + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $OutputDir 'editable' + +# --- Batch mode: JSON array of objects --- +if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) { + $batchOk = 0 + $batchFail = 0 + $idx = 0 + foreach ($item in $def) { + $idx++ + $tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json" + try { + $item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson + $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru + if ($proc.ExitCode -eq 0) { $batchOk++ } else { $batchFail++ } + } finally { + Remove-Item $tmpJson -Force -ErrorAction SilentlyContinue + } + } + Write-Host "" + Write-Host "=== Batch: $idx objects, $batchOk compiled, $batchFail failed ===" + if ($batchFail -gt 0) { exit 1 } + exit 0 +} + +# Normalize field synonyms: accept "objectType" as alias for "type" +if (-not $def.type -and $def.objectType) { + $def | Add-Member -NotePropertyName "type" -NotePropertyValue $def.objectType +} + +# Object type synonyms (Russian → English) +$script:objectTypeSynonyms = @{ + "Справочник" = "Catalog" + "Каталог" = "Catalog" + "Документ" = "Document" + "Перечисление" = "Enum" + "Константа" = "Constant" + "РегистрСведений" = "InformationRegister" + "РегистрНакопления" = "AccumulationRegister" + "РегистрБухгалтерии" = "AccountingRegister" + "РегистрРасчёта" = "CalculationRegister" + "РегистрРасчета" = "CalculationRegister" + "ПланСчетов" = "ChartOfAccounts" + "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" + "ПланВидовРасчёта" = "ChartOfCalculationTypes" + "ПланВидовРасчета" = "ChartOfCalculationTypes" + "БизнесПроцесс" = "BusinessProcess" + "Задача" = "Task" + "ПланОбмена" = "ExchangePlan" + "ЖурналДокументов" = "DocumentJournal" + "Отчёт" = "Report" + "Отчет" = "Report" + "Обработка" = "DataProcessor" + "ОбщийМодуль" = "CommonModule" + "РегламентноеЗадание" = "ScheduledJob" + "ПодпискаНаСобытие" = "EventSubscription" + "HTTPСервис" = "HTTPService" + "ВебСервис" = "WebService" + "ОпределяемыйТип" = "DefinedType" +} + +# Enum property value synonyms — model often gets these slightly wrong +$script:enumValueAliases = @{ + # RegisterType (AccumulationRegister) + "Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers" + # WriteMode (InformationRegister) + "RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate" + "ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent" + # DependenceOnCalculationTypes (ChartOfCalculationTypes) + "NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse" + "Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod" + # InformationRegisterPeriodicity + "None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month" + "Quarterly" = "Quarter"; "Yearly" = "Year" + "Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day" + "Месяц" = "Month"; "Квартал" = "Quarter"; "Год" = "Year" + "ПозицияРегистратора" = "RecorderPosition" + # DataLockControlMode + "Автоматический" = "Automatic"; "Управляемый" = "Managed" + # FullTextSearch + "Использовать" = "Use"; "НеИспользовать" = "DontUse" + # Posting + "Разрешить" = "Allow"; "Запретить" = "Deny" + # EditType + "ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays" + # DefaultPresentation + "ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode" + # FillChecking + "НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning" + # Indexing + "НеИндексировать" = "DontIndex"; "Индексировать" = "Index" + "ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder" +} + +# Valid enum values per property (from meta-validate) +$script:validEnumValues = @{ + "RegisterType" = @("Balance","Turnovers") + "WriteMode" = @("Independent","RecorderSubordinate") + "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") + "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") + "DataLockControlMode" = @("Automatic","Managed") + "FullTextSearch" = @("Use","DontUse") + "DataHistory" = @("Use","DontUse") + "DefaultPresentation" = @("AsDescription","AsCode") + "Posting" = @("Allow","Deny") + "RealTimePosting" = @("Allow","Deny") + "EditType" = @("InDialog","InList","BothWays") + "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") + "CodeType" = @("String","Number") + "CodeAllowedLength" = @("Variable","Fixed") + "NumberType" = @("String","Number") + "NumberAllowedLength" = @("Variable","Fixed") + "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") + "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") + "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") + "ReuseSessions" = @("DontUse","AutoUse") + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") + "SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems") + "CodeSeries" = @("WholeCatalog","WithinSubordination") + "ChoiceMode" = @("BothWays","QuickChoice","FromForm") +} + +function Normalize-EnumValue { + param([string]$propName, [string]$value) + # 1. Check alias dictionary — silent auto-correct + if ($script:enumValueAliases.ContainsKey($value)) { + return $script:enumValueAliases[$value] + } + # 2. Case-insensitive match against valid values — silent + $valid = $script:validEnumValues[$propName] + if ($valid) { + foreach ($v in $valid) { + if ($v -ieq $value) { return $v } + } + # 3. Known property, unknown value — error with hint + Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')" + exit 1 + } + # 4. Unknown property — pass-through (no validation data) + return $value +} + +# Helper: read enum property from $def with default and normalization +function Get-EnumProp { + param([string]$propName, [string]$fieldName, [string]$default) + $val = $def.$fieldName + $raw = if ($val) { "$val" } else { $default } + return (Normalize-EnumValue $propName $raw) +} + +if (-not $def.type) { + Write-Error "JSON must have 'type' field" + exit 1 +} + +# Resolve type synonym +$objType = "$($def.type)" +if ($script:objectTypeSynonyms.ContainsKey($objType)) { + $objType = $script:objectTypeSynonyms[$objType] +} + +$validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","AccumulationRegister", + "AccountingRegister","CalculationRegister","ChartOfAccounts","ChartOfCharacteristicTypes", + "ChartOfCalculationTypes","BusinessProcess","Task","ExchangePlan","DocumentJournal", + "Report","DataProcessor","CommonModule","ScheduledJob","EventSubscription", + "HTTPService","WebService","DefinedType") +if ($objType -notin $validTypes) { + Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')" + exit 1 +} + +if (-not $def.name) { + Write-Error "JSON must have 'name' field" + exit 1 +} + +$objName = "$($def.name)" + +# --- 2. XML helpers --- + +$script:xml = New-Object System.Text.StringBuilder 32768 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function Emit-MLText { + param([string]$indent, [string]$tag, [string]$text) + if (-not $text) { + X "$indent<$tag/>" + return + } + X "$indent<$tag>" + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml $text)" + X "$indent`t" + X "$indent" +} + +function New-Guid-String { + return [System.Guid]::NewGuid().ToString() +} + +# --- 3. CamelCase splitter --- + +function Split-CamelCase { + param([string]$name) + if (-not $name) { return $name } + # Insert space before uppercase that follows lowercase (Cyrillic + Latin) + $result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2') + $result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2') + # Lowercase all but first character of the result + if ($result.Length -gt 1) { + $result = $result.Substring(0,1) + $result.Substring(1).ToLower() + } + return $result +} + +# Auto-synonym +$synonym = if ($def.synonym) { "$($def.synonym)" } else { Split-CamelCase $objName } +$comment = if ($def.comment) { "$($def.comment)" } else { "" } + +# --- 4. Type system --- + +$script:typeSynonyms = New-Object System.Collections.Hashtable +$script:typeSynonyms["число"] = "Number" +$script:typeSynonyms["строка"] = "String" +$script:typeSynonyms["булево"] = "Boolean" +$script:typeSynonyms["дата"] = "Date" +$script:typeSynonyms["датавремя"]= "DateTime" +$script:typeSynonyms["number"] = "Number" +$script:typeSynonyms["string"] = "String" +$script:typeSynonyms["boolean"] = "Boolean" +$script:typeSynonyms["date"] = "Date" +$script:typeSynonyms["datetime"] = "DateTime" +$script:typeSynonyms["bool"] = "Boolean" +# Reference synonyms (Russian, lowercase) +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:typeSynonyms["задачассылка"] = "TaskRef" +$script:typeSynonyms["определяемыйтип"] = "DefinedType" +$script:typeSynonyms["definedtype"] = "DefinedType" +# English lowercase ref synonyms +$script:typeSynonyms["catalogref"] = "CatalogRef" +$script:typeSynonyms["documentref"] = "DocumentRef" +$script:typeSynonyms["enumref"] = "EnumRef" + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + # Check for parameterized types: Number(15,2), Строка(100), etc. + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) # includes the dot + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + # Simple name lookup + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + + return $typeStr +} + +function Emit-TypeContent { + param([string]$indent, [string]$typeStr) + if (-not $typeStr) { return } + + # Composite type: "Type1 + Type2 + Type3" + if ($typeStr.Contains(' + ')) { + $parts = $typeStr -split '\s*\+\s*' + foreach ($part in $parts) { + Emit-TypeContent $indent $part.Trim() + } + return + } + + $typeStr = Resolve-TypeStr $typeStr + + # Boolean + if ($typeStr -eq "Boolean") { + X "$indentxs:boolean" + return + } + + # String or String(N) + if ($typeStr -match '^String(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "10" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent" + return + } + + # Number without params → Number(10,0) + if ($typeStr -eq "Number") { + X "$indentxs:decimal" + X "$indent" + X "$indent`t10" + X "$indent`t0" + X "$indent`tAny" + X "$indent" + return + } + + # Number(D,F) or Number(D,F,nonneg) + if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent" + return + } + + # Date / DateTime + if ($typeStr -eq "Date") { + X "$indentxs:dateTime" + X "$indent" + X "$indent`tDate" + X "$indent" + return + } + if ($typeStr -eq "DateTime") { + X "$indentxs:dateTime" + X "$indent" + X "$indent`tDateTime" + X "$indent" + return + } + + # DefinedType + if ($typeStr -match '^DefinedType\.(.+)$') { + $dtName = $Matches[1] + X "$indentcfg:DefinedType.$dtName" + return + } + + # ValueStorage + if ($typeStr -eq "ValueStorage") { + X "$indentxs:base64Binary" + return + } + + # Reference types — use local xmlns declaration for 1C compatibility + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { + X "$indentd5p1:$typeStr" + return + } + + # Fallback — emit as-is + X "$indent$typeStr" +} + +function Emit-ValueType { + param([string]$indent, [string]$typeStr) + X "$indent" + Emit-TypeContent "$indent`t" $typeStr + X "$indent" +} + +function Emit-FillValue { + param([string]$indent, [string]$typeStr) + if (-not $typeStr) { + X "$indent" + return + } + + $typeStr = Resolve-TypeStr $typeStr + + if ($typeStr -eq "Boolean") { + X "$indentfalse" + return + } + if ($typeStr -match '^String') { + X "$indent" + return + } + if ($typeStr -match '^Number') { + X "$indent0" + return + } + if ($typeStr -match '^(Date|DateTime)$') { + X "$indent" + return + } + # References and others + X "$indent" +} + +# --- 5. Attribute shorthand parser --- + +function Build-TypeStr { + param($obj) + $t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" } + if ($t -and -not $t.Contains('(')) { + if ($t -eq "String" -and $obj.length) { + $t = "String($($obj.length))" + } elseif ($t -eq "Number" -and $obj.length) { + $p = if ($obj.precision) { $obj.precision } else { 0 } + $nn = if ($obj.nonneg -or $obj.nonnegative) { ",nonneg" } else { "" } + $t = "Number($($obj.length),$p$nn)" + } + } + return $t +} + +function Parse-AttributeShorthand { + param($val) + + if ($val -is [string]) { + $str = "$val" + $parsed = @{ + name = "" + type = "" + synonym = "" + comment = "" + flags = @() + } + + # Split by | for flags + $parts = $str -split '\|', 2 + $mainPart = $parts[0].Trim() + if ($parts.Count -gt 1) { + $flagStr = $parts[1].Trim() + $parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) + } + + # Split by : for name and type + $colonParts = $mainPart -split ':', 2 + $parsed.name = $colonParts[0].Trim() + if ($colonParts.Count -gt 1) { + $parsed.type = $colonParts[1].Trim() + } + + $parsed.synonym = Split-CamelCase $parsed.name + return $parsed + } + + # Object form + $name = "$($val.name)" + return @{ + name = $name + type = Build-TypeStr $val + synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } + comment = if ($val.comment) { "$($val.comment)" } else { "" } + flags = @(if ($val.flags) { $val.flags } else { @() }) + fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } + indexing = if ($val.indexing) { "$($val.indexing)" } else { "" } + multiLine = if ($val.multiLine -eq $true) { $true } else { $false } + choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" } + } +} + +function Parse-EnumValueShorthand { + param($val) + + if ($val -is [string]) { + $name = "$val" + return @{ + name = $name + synonym = Split-CamelCase $name + comment = "" + } + } + + $name = "$($val.name)" + return @{ + name = $name + synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } + comment = if ($val.comment) { "$($val.comment)" } else { "" } + } +} + +# --- 6. GeneratedType categories --- + +$script:generatedTypes = @{ + "Catalog" = @( + @{ prefix = "CatalogObject"; category = "Object" } + @{ prefix = "CatalogRef"; category = "Ref" } + @{ prefix = "CatalogSelection"; category = "Selection" } + @{ prefix = "CatalogList"; category = "List" } + @{ prefix = "CatalogManager"; category = "Manager" } + ) + "Document" = @( + @{ prefix = "DocumentObject"; category = "Object" } + @{ prefix = "DocumentRef"; category = "Ref" } + @{ prefix = "DocumentSelection"; category = "Selection" } + @{ prefix = "DocumentList"; category = "List" } + @{ prefix = "DocumentManager"; category = "Manager" } + ) + "Enum" = @( + @{ prefix = "EnumRef"; category = "Ref" } + @{ prefix = "EnumManager"; category = "Manager" } + @{ prefix = "EnumList"; category = "List" } + ) + "Constant" = @( + @{ prefix = "ConstantManager"; category = "Manager" } + @{ prefix = "ConstantValueManager"; category = "ValueManager" } + @{ prefix = "ConstantValueKey"; category = "ValueKey" } + ) + "InformationRegister" = @( + @{ prefix = "InformationRegisterRecord"; category = "Record" } + @{ prefix = "InformationRegisterManager"; category = "Manager" } + @{ prefix = "InformationRegisterSelection"; category = "Selection" } + @{ prefix = "InformationRegisterList"; category = "List" } + @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } + ) + "AccumulationRegister" = @( + @{ prefix = "AccumulationRegisterRecord"; category = "Record" } + @{ prefix = "AccumulationRegisterManager"; category = "Manager" } + @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } + @{ prefix = "AccumulationRegisterList"; category = "List" } + @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } + ) + "AccountingRegister" = @( + @{ prefix = "AccountingRegisterRecord"; category = "Record" } + @{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" } + @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "AccountingRegisterSelection"; category = "Selection" } + @{ prefix = "AccountingRegisterList"; category = "List" } + @{ prefix = "AccountingRegisterManager"; category = "Manager" } + ) + "CalculationRegister" = @( + @{ prefix = "CalculationRegisterRecord"; category = "Record" } + @{ prefix = "CalculationRegisterManager"; category = "Manager" } + @{ prefix = "CalculationRegisterSelection"; category = "Selection" } + @{ prefix = "CalculationRegisterList"; category = "List" } + @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "RecalculationsManager"; category = "Recalcs" } + ) + "ChartOfAccounts" = @( + @{ prefix = "ChartOfAccountsObject"; category = "Object" } + @{ prefix = "ChartOfAccountsRef"; category = "Ref" } + @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } + @{ prefix = "ChartOfAccountsList"; category = "List" } + @{ prefix = "ChartOfAccountsManager"; category = "Manager" } + @{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" } + @{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" } + ) + "ChartOfCharacteristicTypes" = @( + @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } + @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } + @{ prefix = "ChartOfCharacteristicTypesCharacteristic"; category = "Characteristic" } + @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } + ) + "ChartOfCalculationTypes" = @( + @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } + @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCalculationTypesList"; category = "List" } + @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } + @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } + @{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" } + @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } + @{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" } + @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } + @{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" } + ) + "BusinessProcess" = @( + @{ prefix = "BusinessProcessObject"; category = "Object" } + @{ prefix = "BusinessProcessRef"; category = "Ref" } + @{ prefix = "BusinessProcessSelection"; category = "Selection" } + @{ prefix = "BusinessProcessList"; category = "List" } + @{ prefix = "BusinessProcessManager"; category = "Manager" } + @{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" } + ) + "Task" = @( + @{ prefix = "TaskObject"; category = "Object" } + @{ prefix = "TaskRef"; category = "Ref" } + @{ prefix = "TaskSelection"; category = "Selection" } + @{ prefix = "TaskList"; category = "List" } + @{ prefix = "TaskManager"; category = "Manager" } + ) + "ExchangePlan" = @( + @{ prefix = "ExchangePlanObject"; category = "Object" } + @{ prefix = "ExchangePlanRef"; category = "Ref" } + @{ prefix = "ExchangePlanSelection"; category = "Selection" } + @{ prefix = "ExchangePlanList"; category = "List" } + @{ prefix = "ExchangePlanManager"; category = "Manager" } + ) + "DefinedType" = @( + @{ prefix = "DefinedType"; category = "DefinedType" } + ) + "DocumentJournal" = @( + @{ prefix = "DocumentJournalSelection"; category = "Selection" } + @{ prefix = "DocumentJournalList"; category = "List" } + @{ prefix = "DocumentJournalManager"; category = "Manager" } + ) + "Report" = @( + @{ prefix = "ReportObject"; category = "Object" } + @{ prefix = "ReportManager"; category = "Manager" } + ) + "DataProcessor" = @( + @{ prefix = "DataProcessorObject"; category = "Object" } + @{ prefix = "DataProcessorManager"; category = "Manager" } + ) +} + +function Emit-InternalInfo { + param([string]$indent, [string]$objectType, [string]$objectName) + $types = $script:generatedTypes[$objectType] + if (-not $types) { return } + + X "$indent" + # ExchangePlan: ThisNode UUID before GeneratedTypes + if ($objectType -eq "ExchangePlan") { + X "$indent`t$(New-Guid-String)" + } + foreach ($gt in $types) { + $fullName = "$($gt.prefix).$objectName" + X "$indent`t" + X "$indent`t`t$(New-Guid-String)" + X "$indent`t`t$(New-Guid-String)" + X "$indent`t" + } + X "$indent" +} + +# --- 7. StandardAttributes --- + +$script:standardAttributesByType = @{ + "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") + "Document" = @("Posted","Ref","DeletionMark","Date","Number") + "Enum" = @("Order","Ref") + "InformationRegister" = @("Active","LineNumber","Recorder","Period") + "AccumulationRegister" = @("Active","LineNumber","Recorder","Period") + "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") + "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") + "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") + "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType") + "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") + "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") + "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") + "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") + "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") +} + +function Emit-StandardAttribute { + param([string]$indent, [string]$attrName) + X "$indent" + X "$indent`t" + X "$indent`tDontCheck" + X "$indent`tfalse" + X "$indent`tfalse" + X "$indent`tAuto" + X "$indent`t" + X "$indent`t" + X "$indent`tfalse" + X "$indent`t" + X "$indent`t" + X "$indent`tAuto" + X "$indent`tAuto" + X "$indent`t" + X "$indent`tfalse" + X "$indent`tUse" + X "$indent`tfalse" + X "$indent`t" + X "$indent`t" + X "$indent`t" + X "$indent`tUse" + X "$indent`t" + X "$indent`t" + X "$indent`t" + X "$indent`t" + X "$indent" +} + +function Emit-StandardAttributes { + param([string]$indent, [string]$objectType) + $attrs = $script:standardAttributesByType[$objectType] + if (-not $attrs) { return } + X "$indent" + foreach ($a in $attrs) { + Emit-StandardAttribute "$indent`t" $a + } + X "$indent" +} + +# TabularSection standard attributes (just LineNumber) +function Emit-TabularStandardAttributes { + param([string]$indent) + X "$indent" + Emit-StandardAttribute "$indent`t" "LineNumber" + X "$indent" +} + +# --- 8. Attribute emitter --- + +$script:reservedAttrNames = @{ + "Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование" + "Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец" + "IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных" + "Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность" + "Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый" + "Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача" + "Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс" + "ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого" + "CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись" + "Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый" +} + +function Emit-Attribute { + param([string]$indent, $parsed, [string]$context) + # $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register" + $attrName = $parsed.name + if ($context -notin @("tabular", "processor-tabular") -and + ($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) { + Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." + } + $uuid = New-Guid-String + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $parsed.name)" + Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym + X "$indent`t`t" + + # Type + $typeStr = $parsed.type + if ($typeStr) { + Emit-ValueType "$indent`t`t" $typeStr + } else { + # Default: unqualified string + X "$indent`t`t" + X "$indent`t`t`txs:string" + X "$indent`t`t" + } + + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } + X "$indent`t`t$multiLine" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + + # FillFromFillingValue — not for tabular/processor/chart/register-other + # (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these) + if ($context -notin @("tabular", "processor", "chart", "register-other")) { + X "$indent`t`tfalse" + } + + # FillValue — same restriction + if ($context -notin @("tabular", "processor", "chart", "register-other")) { + Emit-FillValue "$indent`t`t" $typeStr + } + + # FillChecking + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking } + X "$indent`t`t$fillChecking" + + X "$indent`t`tItems" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t`tAuto" + X "$indent`t`t" + X "$indent`t`t" + $chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" } + X "$indent`t`t$chi" + + # Use — only for catalog top-level attributes + if ($context -eq "catalog") { + X "$indent`t`tForItem" + } + + # Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular) + if ($context -notin @("processor", "processor-tabular")) { + $indexing = "DontIndex" + if ($parsed.flags -contains "index") { $indexing = "Index" } + if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } + if ($parsed.indexing) { $indexing = $parsed.indexing } + X "$indent`t`t$indexing" + + X "$indent`t`tUse" + # DataHistory — not for Chart* types and non-InformationRegister register family + if ($context -notin @("chart", "register-other")) { + X "$indent`t`tUse" + } + } + + X "$indent`t" + X "$indent" +} + +# --- 9. TabularSection emitter --- + +function Emit-TabularSection { + param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName) + $uuid = New-Guid-String + X "$indent" + + # InternalInfo for TabularSection + $typePrefix = "${objectType}TabularSection" + $rowPrefix = "${objectType}TabularSectionRow" + + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t`t$(New-Guid-String)" + X "$indent`t`t`t$(New-Guid-String)" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t`t$(New-Guid-String)" + X "$indent`t`t`t$(New-Guid-String)" + X "$indent`t`t" + X "$indent`t" + + $tsSynonym = Split-CamelCase $tsName + + X "$indent`t" + X "$indent`t`t$(Esc-Xml $tsName)" + Emit-MLText "$indent`t`t" "Synonym" $tsSynonym + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tDontCheck" + Emit-TabularStandardAttributes "$indent`t`t" + # Use=ForItem only for Catalog tabular sections (Document does not have Use) + if ($objectType -eq "Catalog") { + X "$indent`t`tForItem" + } + X "$indent`t" + + $tsContext = if ($objectType -in @("DataProcessor","Report")) { "processor-tabular" } else { "tabular" } + X "$indent`t" + foreach ($col in $columns) { + $parsed = Parse-AttributeShorthand $col + Emit-Attribute "$indent`t`t" $parsed $tsContext + } + X "$indent`t" + + X "$indent" +} + +# --- 10. EnumValue emitter --- + +function Emit-EnumValue { + param([string]$indent, $parsed) + $uuid = New-Guid-String + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $parsed.name)" + Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym + X "$indent`t`t" + X "$indent`t" + X "$indent" +} + +# --- 11. Dimension emitter --- + +function Emit-Dimension { + param([string]$indent, $parsed, [string]$registerType) + # $registerType: "InformationRegister" or "AccumulationRegister" + $uuid = New-Guid-String + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $parsed.name)" + Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym + X "$indent`t`t" + + $typeStr = $parsed.type + if ($typeStr) { + Emit-ValueType "$indent`t`t" $typeStr + } else { + X "$indent`t`t" + X "$indent`t`t`txs:string" + X "$indent`t`t" + } + + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } + X "$indent`t`t$multiLine" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + + # InformationRegister dimensions have FillFromFillingValue + if ($registerType -eq "InformationRegister") { + $fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" } + X "$indent`t`t$fillFrom" + X "$indent`t`t" + } + + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + X "$indent`t`t$fillChecking" + + X "$indent`t`tItems" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t`tAuto" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + + # InformationRegister dimensions: Master, MainFilter, DenyIncompleteValues + if ($registerType -eq "InformationRegister") { + $master = if ($parsed.flags -contains "master") { "true" } else { "false" } + $mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" } + $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } + X "$indent`t`t$master" + X "$indent`t`t$mainFilter" + X "$indent`t`t$denyIncomplete" + } + + # AccumulationRegister dimensions: DenyIncompleteValues + if ($registerType -eq "AccumulationRegister") { + $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } + X "$indent`t`t$denyIncomplete" + } + + $indexing = "DontIndex" + if ($parsed.flags -contains "index") { $indexing = "Index" } + X "$indent`t`t$indexing" + + X "$indent`t`tUse" + + # AccumulationRegister dimensions: UseInTotals + if ($registerType -eq "AccumulationRegister") { + $useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" } + X "$indent`t`t$useInTotals" + } + + # InformationRegister dimensions: DataHistory + if ($registerType -eq "InformationRegister") { + X "$indent`t`tUse" + } + + X "$indent`t" + X "$indent" +} + +# --- 12. Resource emitter --- + +function Emit-Resource { + param([string]$indent, $parsed, [string]$registerType) + $uuid = New-Guid-String + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $parsed.name)" + Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym + X "$indent`t`t" + + $typeStr = $parsed.type + if ($typeStr) { + Emit-ValueType "$indent`t`t" $typeStr + } else { + X "$indent`t`t" + X "$indent`t`t`txs:decimal" + X "$indent`t`t`t" + X "$indent`t`t`t`t15" + X "$indent`t`t`t`t2" + X "$indent`t`t`t`tAny" + X "$indent`t`t`t" + X "$indent`t`t" + } + + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" } + X "$indent`t`t$multiLine" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + + # InformationRegister resources have FillFromFillingValue, FillValue + if ($registerType -eq "InformationRegister") { + X "$indent`t`tfalse" + X "$indent`t`t" + } + + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + X "$indent`t`t$fillChecking" + + X "$indent`t`tItems" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t`tAuto" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + + # InformationRegister resources: Indexing, FullTextSearch, DataHistory + if ($registerType -eq "InformationRegister") { + X "$indent`t`tDontIndex" + X "$indent`t`tUse" + X "$indent`t`tUse" + } + + # AccumulationRegister resources: FullTextSearch (no Indexing, no DataHistory) + if ($registerType -eq "AccumulationRegister") { + X "$indent`t`tUse" + } + + X "$indent`t" + X "$indent" +} + +# --- 13. Property emitters per type --- + +function Emit-CatalogProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + $hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" } + $hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems" + X "$i$hierarchical" + X "$i$hierarchyType" + $limitLevelCount = if ($def.limitLevelCount -eq $true) { "true" } else { "false" } + $levelCount = if ($null -ne $def.levelCount) { "$($def.levelCount)" } else { "2" } + $foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" } + X "$i$limitLevelCount" + X "$i$levelCount" + X "$i$foldersOnTop" + X "$itrue" + if ($def.owners -and $def.owners.Count -gt 0) { + X "$i" + foreach ($ownerRef in $def.owners) { + $fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" } + X "$i`t$fullRef" + } + X "$i" + } else { + X "$i" + } + $subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems" + X "$i$subordinationUse" + + $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } + $codeType = Get-EnumProp "CodeType" "codeType" "String" + $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" + $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } + $checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" } + + X "$i$codeLength" + X "$i$descriptionLength" + X "$i$codeType" + X "$i$codeAllowedLength" + $codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog" + X "$i$codeSeries" + X "$i$checkUnique" + X "$i$autonumbering" + + $defaultPresentation = Get-EnumProp "DefaultPresentation" "defaultPresentation" "AsDescription" + X "$i$defaultPresentation" + + Emit-StandardAttributes $i "Catalog" + X "$i" + X "$iAuto" + X "$iInDialog" + $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } + $choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays" + X "$i$quickChoice" + X "$i$choiceMode" + X "$i" + X "$i`tCatalog.$objName.StandardAttribute.Description" + X "$i`tCatalog.$objName.StandardAttribute.Code" + X "$i" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-DocumentProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + X "$i" + + $numberType = Get-EnumProp "NumberType" "numberType" "String" + $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" } + $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" + $numberPeriodicity = if ($def.numberPeriodicity) { "$($def.numberPeriodicity)" } else { "Year" } + $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } + $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } + + X "$i$numberType" + X "$i$numberLength" + X "$i$numberAllowedLength" + X "$i$numberPeriodicity" + X "$i$checkUnique" + X "$i$autonumbering" + + Emit-StandardAttributes $i "Document" + X "$i" + + X "$i" + X "$i" + X "$i`tDocument.$objName.StandardAttribute.Number" + X "$i" + X "$iDontUse" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + + $posting = Get-EnumProp "Posting" "posting" "Allow" + $realTimePosting = Get-EnumProp "RealTimePosting" "realTimePosting" "Deny" + $registerRecordsDeletion = Get-EnumProp "RegisterRecordsDeletion" "registerRecordsDeletion" "AutoDelete" + $registerRecordsWritingOnPost = Get-EnumProp "RegisterRecordsWritingOnPost" "registerRecordsWritingOnPost" "WriteModified" + $sequenceFilling = if ($def.sequenceFilling) { "$($def.sequenceFilling)" } else { "AutoFill" } + $postInPrivilegedMode = if ($def.postInPrivilegedMode -eq $false) { "false" } else { "true" } + $unpostInPrivilegedMode = if ($def.unpostInPrivilegedMode -eq $false) { "false" } else { "true" } + + X "$i$posting" + X "$i$realTimePosting" + X "$i$registerRecordsDeletion" + X "$i$registerRecordsWritingOnPost" + X "$i$sequenceFilling" + + # RegisterRecords + $regRecords = @() + if ($def.registerRecords) { + foreach ($rr in $def.registerRecords) { + $rrStr = "$rr" + # Resolve Russian synonyms in register records + if ($rrStr.Contains('.')) { + $dotIdx = $rrStr.IndexOf('.') + $rrPrefix = $rrStr.Substring(0, $dotIdx) + $rrSuffix = $rrStr.Substring($dotIdx + 1) + if ($script:objectTypeSynonyms.ContainsKey($rrPrefix)) { + $rrPrefix = $script:objectTypeSynonyms[$rrPrefix] + } + $regRecords += "$rrPrefix.$rrSuffix" + } else { + $regRecords += $rrStr + } + } + } + + if ($regRecords.Count -gt 0) { + X "$i" + foreach ($rr in $regRecords) { + X "$i`t$rr" + } + X "$i" + } else { + X "$i" + } + + X "$i$postInPrivilegedMode" + X "$i$unpostInPrivilegedMode" + X "$ifalse" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-EnumProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$ifalse" + + Emit-StandardAttributes $i "Enum" + X "$i" + + $quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" } + X "$i$quickChoice" + X "$iBothWays" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iAuto" +} + +function Emit-ConstantProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + # Type + $valueType = Build-TypeStr $def + if (-not $valueType) { $valueType = "String" } + Emit-ValueType $i $valueType + + X "$itrue" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$ifalse" + X "$ifalse" + X "$i" + X "$i" + X "$iDontCheck" + X "$iItems" + X "$i" + X "$i" + X "$iAuto" + X "$i" + X "$i" + X "$iAuto" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-InformationRegisterProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + X "$iInDialog" + X "$i" + X "$i" + X "$i" + X "$i" + + Emit-StandardAttributes $i "InformationRegister" + + $periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Nonperiodical" + $writeMode = Get-EnumProp "WriteMode" "writeMode" "Independent" + + # MainFilterOnPeriod: auto based on periodicity unless explicitly set + $mainFilterOnPeriod = "false" + if ($null -ne $def.mainFilterOnPeriod) { + $mainFilterOnPeriod = if ($def.mainFilterOnPeriod -eq $true) { "true" } else { "false" } + } elseif ($periodicity -ne "Nonperiodical") { + $mainFilterOnPeriod = "true" + } + + X "$i$periodicity" + X "$i$writeMode" + X "$i$mainFilterOnPeriod" + X "$ifalse" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$ifalse" + X "$ifalse" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-AccumulationRegisterProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + X "$i" + X "$i" + + $registerType = Get-EnumProp "RegisterType" "registerType" "Balance" + X "$i$registerType" + + X "$ifalse" + + Emit-StandardAttributes $i "AccumulationRegister" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + $enableTotalsSplitting = if ($def.enableTotalsSplitting -eq $false) { "false" } else { "true" } + X "$i$enableTotalsSplitting" + + X "$i" + X "$i" + X "$i" +} + +# --- 13a. Wave 1: DefinedType, CommonModule, ScheduledJob, EventSubscription --- + +function Emit-DefinedTypeProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + # Type — composite type with multiple v8:Type entries (accept both valueType and valueTypes) + $valueTypes = @() + if ($def.valueTypes) { + $valueTypes = @($def.valueTypes) + } elseif ($def.valueType) { + $valueTypes = @($def.valueType) + } + if ($valueTypes.Count -gt 0) { + X "$i" + foreach ($vt in $valueTypes) { + $resolved = Resolve-TypeStr "$vt" + if ($resolved -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.') { + X "$i`td5p1:$resolved" + } elseif ($resolved -eq "Boolean") { + X "$i`txs:boolean" + } elseif ($resolved -match '^String') { + X "$i`txs:string" + X "$i`t" + X "$i`t`t0" + X "$i`t`tVariable" + X "$i`t" + } else { + X "$i`tcfg:$resolved" + } + } + X "$i" + } else { + X "$i" + } +} + +function Emit-CommonModuleProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + # Context shortcuts + $context = if ($def.context) { "$($def.context)" } else { "" } + + $global = if ($def.global -eq $true) { "true" } else { "false" } + $server = "false"; $serverCall = "false"; $clientManaged = "false" + $clientOrdinary = "false"; $externalConnection = "false"; $privileged = "false" + + switch ($context) { + "server" { $server = "true"; $serverCall = "true" } + "serverCall" { $server = "true"; $serverCall = "true" } + "client" { $clientManaged = "true" } + "serverClient" { $server = "true"; $clientManaged = "true" } + default { + if ($def.server -eq $true) { $server = "true" } + if ($def.serverCall -eq $true) { $serverCall = "true" } + if ($def.clientManagedApplication -eq $true) { $clientManaged = "true" } + if ($def.clientOrdinaryApplication -eq $true) { $clientOrdinary = "true" } + if ($def.externalConnection -eq $true) { $externalConnection = "true" } + if ($def.privileged -eq $true) { $privileged = "true" } + } + } + + X "$i$global" + X "$i$clientManaged" + X "$i$server" + X "$i$externalConnection" + X "$i$clientOrdinary" + X "$i$serverCall" + X "$i$privileged" + + $returnValuesReuse = Get-EnumProp "ReturnValuesReuse" "returnValuesReuse" "DontUse" + X "$i$returnValuesReuse" +} + +function Emit-ScheduledJobProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + $methodName = if ($def.methodName) { "$($def.methodName)" } else { "" } + # Ensure CommonModule. prefix + if ($methodName -and -not $methodName.StartsWith("CommonModule.")) { + $methodName = "CommonModule.$methodName" + } + X "$i$(Esc-Xml $methodName)" + + $description = if ($def.description) { "$($def.description)" } else { $synonym } + X "$i$(Esc-Xml $description)" + + $key = if ($def.key) { "$($def.key)" } else { "" } + X "$i$(Esc-Xml $key)" + + $use = if ($def.use -eq $true) { "true" } else { "false" } + X "$i$use" + + $predefined = if ($def.predefined -eq $true) { "true" } else { "false" } + X "$i$predefined" + + $restartCount = if ($null -ne $def.restartCountOnFailure) { "$($def.restartCountOnFailure)" } else { "3" } + $restartInterval = if ($null -ne $def.restartIntervalOnFailure) { "$($def.restartIntervalOnFailure)" } else { "10" } + X "$i$restartCount" + X "$i$restartInterval" +} + +function Emit-EventSubscriptionProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + # Source — array of v8:Type + $sources = @() + if ($def.source) { $sources = @($def.source) } + if ($sources.Count -gt 0) { + X "$i" + foreach ($src in $sources) { + $resolved = Resolve-TypeStr "$src" + X "$i`td5p1:$resolved" + } + X "$i" + } else { + X "$i" + } + + $event = if ($def.event) { "$($def.event)" } else { "BeforeWrite" } + X "$i$event" + + $handler = if ($def.handler) { "$($def.handler)" } else { "" } + # Ensure CommonModule. prefix + if ($handler -and -not $handler.StartsWith("CommonModule.")) { + $handler = "CommonModule.$handler" + } + X "$i$(Esc-Xml $handler)" +} + +# --- 13b. Wave 2: Report, DataProcessor --- + +function Emit-ReportProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } + if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } + + $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } + if ($auxForm) { X "$i$auxForm" } else { X "$i" } + + $mainDCS = if ($def.mainDataCompositionSchema) { "$($def.mainDataCompositionSchema)" } else { "" } + if ($mainDCS) { X "$i$mainDCS" } else { X "$i" } + + $defSettings = if ($def.defaultSettingsForm) { "$($def.defaultSettingsForm)" } else { "" } + if ($defSettings) { X "$i$defSettings" } else { X "$i" } + + $auxSettings = if ($def.auxiliarySettingsForm) { "$($def.auxiliarySettingsForm)" } else { "" } + if ($auxSettings) { X "$i$auxSettings" } else { X "$i" } + + $defVariant = if ($def.defaultVariantForm) { "$($def.defaultVariantForm)" } else { "" } + if ($defVariant) { X "$i$defVariant" } else { X "$i" } + + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" +} + +function Emit-DataProcessorProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$ifalse" + + $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } + if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } + + $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } + if ($auxForm) { X "$i$auxForm" } else { X "$i" } + + X "$ifalse" + X "$i" + X "$i" +} + +# --- 13c. Wave 3: ExchangePlan, ChartOfCharacteristicTypes, DocumentJournal --- + +function Emit-ExchangePlanProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" } + $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" + + X "$i$codeLength" + X "$i$codeAllowedLength" + X "$i$descriptionLength" + X "$iAsDescription" + X "$iInDialog" + + Emit-StandardAttributes $i "ExchangePlan" + + $distributed = if ($def.distributedInfoBase -eq $true) { "true" } else { "false" } + $includeExt = if ($def.includeConfigurationExtensions -eq $true) { "true" } else { "false" } + X "$i$distributed" + X "$i$includeExt" + + X "$i" + $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } + X "$i$quickChoice" + X "$iBothWays" + X "$i" + X "$i`tExchangePlan.$objName.StandardAttribute.Description" + X "$i`tExchangePlan.$objName.StandardAttribute.Code" + X "$i" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-ChartOfCharacteristicTypesProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } + $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" + $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } + $checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" } + + X "$i$codeLength" + X "$i$codeAllowedLength" + X "$i$descriptionLength" + X "$i$checkUnique" + X "$i$autonumbering" + X "$iAsDescription" + + # CharacteristicExtValues + $charExtValues = if ($def.characteristicExtValues) { "$($def.characteristicExtValues)" } else { "" } + if ($charExtValues) { X "$i$charExtValues" } + else { X "$i" } + + # Type — composite type of allowed characteristic value types + $valueTypes = @() + if ($def.valueTypes) { $valueTypes = @($def.valueTypes) } + if ($valueTypes.Count -gt 0) { + X "$i" + foreach ($vt in $valueTypes) { + Emit-TypeContent "$i`t" "$vt" + } + X "$i" + } else { + X "$i" + X "$i`txs:boolean" + X "$i`txs:string" + X "$i`t" + X "$i`t`t100" + X "$i`t`tVariable" + X "$i`t" + X "$i`txs:decimal" + X "$i`t" + X "$i`t`t15" + X "$i`t`t2" + X "$i`t`tAny" + X "$i`t" + X "$i`txs:dateTime" + X "$i`t" + X "$i`t`tDateTime" + X "$i`t" + X "$i" + } + + $hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" } + X "$i$hierarchical" + X "$itrue" + + Emit-StandardAttributes $i "ChartOfCharacteristicTypes" + X "$i" + X "$iAuto" + X "$iInDialog" + $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } + X "$i$quickChoice" + X "$iBothWays" + X "$i" + X "$i`tChartOfCharacteristicTypes.$objName.StandardAttribute.Description" + X "$i`tChartOfCharacteristicTypes.$objName.StandardAttribute.Code" + X "$i" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-DocumentJournalProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + $defaultForm = if ($def.defaultForm) { "$($def.defaultForm)" } else { "" } + if ($defaultForm) { X "$i$defaultForm" } else { X "$i" } + + $auxForm = if ($def.auxiliaryForm) { "$($def.auxiliaryForm)" } else { "" } + if ($auxForm) { X "$i$auxForm" } else { X "$i" } + + X "$itrue" + + # RegisteredDocuments + $regDocs = @() + if ($def.registeredDocuments) { $regDocs = @($def.registeredDocuments) } + if ($regDocs.Count -gt 0) { + X "$i" + foreach ($rd in $regDocs) { + $rdStr = "$rd" + # Resolve Russian synonyms: Документ.Xxx → Document.Xxx + if ($rdStr.Contains('.')) { + $dotIdx = $rdStr.IndexOf('.') + $rdPrefix = $rdStr.Substring(0, $dotIdx) + $rdSuffix = $rdStr.Substring($dotIdx + 1) + if ($script:objectTypeSynonyms.ContainsKey($rdPrefix)) { + $rdPrefix = $script:objectTypeSynonyms[$rdPrefix] + } + $rdStr = "$rdPrefix.$rdSuffix" + } + X "$i`t$rdStr" + } + X "$i" + } else { + X "$i" + } + + Emit-StandardAttributes $i "DocumentJournal" + + X "$i" + X "$i" + X "$i" +} + +# --- 13d. Wave 4: ChartOfAccounts, AccountingRegister, ChartOfCalculationTypes, CalculationRegister --- + +function Emit-ChartOfAccountsProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + # ExtDimensionTypes + $extDimTypes = if ($def.extDimensionTypes) { "$($def.extDimensionTypes)" } else { "" } + if ($extDimTypes) { X "$i$extDimTypes" } + else { X "$i" } + + $maxExtDim = if ($null -ne $def.maxExtDimensionCount) { "$($def.maxExtDimensionCount)" } else { "3" } + X "$i$maxExtDim" + + $codeMask = if ($def.codeMask) { "$($def.codeMask)" } else { "" } + if ($codeMask) { X "$i$codeMask" } else { X "$i" } + + $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "8" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "120" } + $codeSeries = if ($def.codeSeries) { "$($def.codeSeries)" } else { "WholeChartOfAccounts" } + $autoOrder = if ($def.autoOrderByCode -eq $false) { "false" } else { "true" } + $orderLength = if ($null -ne $def.orderLength) { "$($def.orderLength)" } else { "5" } + + X "$i$codeLength" + X "$i$descriptionLength" + X "$i$codeSeries" + X "$ifalse" + X "$iAsDescription" + X "$i$autoOrder" + X "$i$orderLength" + + X "$iInDialog" + + Emit-StandardAttributes $i "ChartOfAccounts" + + # StandardTabularSections — ExtDimensionTypes + X "$i" + X "$i`t" + X "$i`t`t" + foreach ($stAttr in @("TurnoversOnly","Predefined","ExtDimensionType","LineNumber")) { + Emit-StandardAttribute "$i`t`t`t" $stAttr + } + X "$i`t`t" + X "$i`t" + X "$i" + + X "$i" + X "$iAuto" + $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } + X "$i$quickChoice" + X "$iBothWays" + X "$i" + X "$i`tChartOfAccounts.$objName.StandardAttribute.Description" + X "$i`tChartOfAccounts.$objName.StandardAttribute.Code" + X "$i" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-AccountingRegisterProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + X "$i" + X "$i" + + $chartOfAccounts = if ($def.chartOfAccounts) { "$($def.chartOfAccounts)" } else { "" } + if ($chartOfAccounts) { X "$i$chartOfAccounts" } + else { X "$i" } + + $correspondence = if ($def.correspondence -eq $true) { "true" } else { "false" } + X "$i$correspondence" + + $periodAdjLen = if ($null -ne $def.periodAdjustmentLength) { "$($def.periodAdjustmentLength)" } else { "0" } + X "$i$periodAdjLen" + + X "$ifalse" + + Emit-StandardAttributes $i "AccountingRegister" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" +} + +function Emit-ChartOfCalculationTypesProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" } + $codeType = Get-EnumProp "CodeType" "codeType" "String" + $codeAllowedLength = Get-EnumProp "CodeAllowedLength" "codeAllowedLength" "Variable" + + X "$i$codeLength" + X "$i$codeType" + X "$i$codeAllowedLength" + X "$i$descriptionLength" + X "$iAsDescription" + + $dependence = Get-EnumProp "DependenceOnCalculationTypes" "dependenceOnCalculationTypes" "DontUse" + X "$i$dependence" + + # BaseCalculationTypes + $baseTypes = @() + if ($def.baseCalculationTypes) { $baseTypes = @($def.baseCalculationTypes) } + if ($baseTypes.Count -gt 0) { + X "$i" + foreach ($bt in $baseTypes) { + X "$i`t$bt" + } + X "$i" + } else { + X "$i" + } + + $actionPeriodUse = if ($def.actionPeriodUse -eq $true) { "true" } else { "false" } + X "$i$actionPeriodUse" + + Emit-StandardAttributes $i "ChartOfCalculationTypes" + X "$i" + X "$iAuto" + X "$iInDialog" + $quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" } + X "$i$quickChoice" + X "$iBothWays" + X "$i" + X "$i`tChartOfCalculationTypes.$objName.StandardAttribute.Description" + X "$i`tChartOfCalculationTypes.$objName.StandardAttribute.Code" + X "$i" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iDontUse" + X "$iAuto" +} + +function Emit-CalculationRegisterProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + X "$i" + X "$i" + + $chartOfCalcTypes = if ($def.chartOfCalculationTypes) { "$($def.chartOfCalculationTypes)" } else { "" } + if ($chartOfCalcTypes) { X "$i$chartOfCalcTypes" } + else { X "$i" } + + $periodicity = Get-EnumProp "InformationRegisterPeriodicity" "periodicity" "Month" + X "$i$periodicity" + + $actionPeriod = if ($def.actionPeriod -eq $true) { "true" } else { "false" } + X "$i$actionPeriod" + + $basePeriod = if ($def.basePeriod -eq $true) { "true" } else { "false" } + X "$i$basePeriod" + + $schedule = if ($def.schedule) { "$($def.schedule)" } else { "" } + if ($schedule) { X "$i$schedule" } else { X "$i" } + + $scheduleValue = if ($def.scheduleValue) { "$($def.scheduleValue)" } else { "" } + if ($scheduleValue) { X "$i$scheduleValue" } else { X "$i" } + + $scheduleDate = if ($def.scheduleDate) { "$($def.scheduleDate)" } else { "" } + if ($scheduleDate) { X "$i$scheduleDate" } else { X "$i" } + + X "$ifalse" + + Emit-StandardAttributes $i "CalculationRegister" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" +} + +# --- 13e. Wave 5: BusinessProcess, Task --- + +function Emit-BusinessProcessProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $editType = Get-EnumProp "EditType" "editType" "InDialog" + X "$i$editType" + + $numberType = Get-EnumProp "NumberType" "numberType" "String" + $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "11" } + $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" + $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } + $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } + + X "$i$numberType" + X "$i$numberLength" + X "$i$numberAllowedLength" + X "$i$checkUnique" + X "$i$autonumbering" + + Emit-StandardAttributes $i "BusinessProcess" + X "$i" + + $task = if ($def.task) { "$($def.task)" } else { "" } + if ($task) { + X "$i$task" + } else { + X "$i" + } + + X "$i" + X "$i" + X "$i`tBusinessProcess.$objName.StandardAttribute.Number" + X "$i" + X "$iDontUse" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +function Emit-TaskProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + X "$itrue" + + $numberType = Get-EnumProp "NumberType" "numberType" "String" + $numberLength = if ($null -ne $def.numberLength) { "$($def.numberLength)" } else { "14" } + $numberAllowedLength = Get-EnumProp "NumberAllowedLength" "numberAllowedLength" "Variable" + $checkUnique = if ($def.checkUnique -eq $false) { "false" } else { "true" } + $autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" } + + $taskNumberAutoPrefix = if ($def.taskNumberAutoPrefix) { "$($def.taskNumberAutoPrefix)" } else { "BusinessProcessNumber" } + $descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "150" } + + X "$i$numberType" + X "$i$numberLength" + X "$i$numberAllowedLength" + X "$i$checkUnique" + X "$i$autonumbering" + X "$i$taskNumberAutoPrefix" + X "$i$descriptionLength" + + # Addressing + $addressing = if ($def.addressing) { "$($def.addressing)" } else { "" } + if ($addressing) { X "$i$addressing" } else { X "$i" } + + $mainAddressing = if ($def.mainAddressingAttribute) { "$($def.mainAddressingAttribute)" } else { "" } + if ($mainAddressing) { X "$i$mainAddressing" } else { X "$i" } + + $currentPerformer = if ($def.currentPerformer) { "$($def.currentPerformer)" } else { "" } + if ($currentPerformer) { X "$i$currentPerformer" } else { X "$i" } + + Emit-StandardAttributes $i "Task" + X "$i" + + X "$i" + X "$i" + X "$i`tTask.$objName.StandardAttribute.Number" + X "$i" + X "$iDontUse" + X "$iBegin" + X "$iDontUse" + X "$iDirectly" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$ifalse" + X "$i" + + $dataLockControlMode = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dataLockControlMode" + + $fullTextSearch = Get-EnumProp "FullTextSearch" "fullTextSearch" "Use" + X "$i$fullTextSearch" + + X "$i" + X "$i" + X "$i" + X "$i" + X "$i" + X "$iAuto" + X "$iDontUse" + X "$ifalse" + X "$ifalse" +} + +# --- 13f. Wave 6: HTTPService, WebService --- + +function Emit-HTTPServiceProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + $rootURL = if ($def.rootURL) { "$($def.rootURL)" } else { $objName.ToLower() } + X "$i$(Esc-Xml $rootURL)" + + $reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse" + X "$i$reuseSessions" + + $sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" } + X "$i$sessionMaxAge" +} + +function Emit-WebServiceProperties { + param([string]$indent) + $i = $indent + + X "$i$(Esc-Xml $objName)" + Emit-MLText $i "Synonym" $synonym + X "$i" + + $namespace = if ($def.namespace) { "$($def.namespace)" } else { "" } + X "$i$(Esc-Xml $namespace)" + + $xdtoPackages = if ($def.xdtoPackages) { "$($def.xdtoPackages)" } else { "" } + if ($xdtoPackages) { X "$i$xdtoPackages" } else { X "$i" } + + $reuseSessions = Get-EnumProp "ReuseSessions" "reuseSessions" "DontUse" + X "$i$reuseSessions" + + $sessionMaxAge = if ($null -ne $def.sessionMaxAge) { "$($def.sessionMaxAge)" } else { "20" } + X "$i$sessionMaxAge" +} + +# --- 13g. ChildObjects emitters for new types --- + +function Emit-Column { + param([string]$indent, $colDef) + $uuid = New-Guid-String + + $name = "" + $synonym = "" + $indexing = "DontIndex" + $references = @() + + if ($colDef -is [string]) { + $name = "$colDef" + $synonym = Split-CamelCase $name + } else { + $name = "$($colDef.name)" + $synonym = if ($colDef.synonym) { "$($colDef.synonym)" } else { Split-CamelCase $name } + if ($colDef.indexing) { $indexing = "$($colDef.indexing)" } + if ($colDef.references) { $references = @($colDef.references) } + } + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $name)" + Emit-MLText "$indent`t`t" "Synonym" $synonym + X "$indent`t`t" + X "$indent`t`t$indexing" + if ($references.Count -gt 0) { + X "$indent`t`t" + foreach ($ref in $references) { + X "$indent`t`t`t$ref" + } + X "$indent`t`t" + } else { + X "$indent`t`t" + } + X "$indent`t" + X "$indent" +} + +function Emit-AccountingFlag { + param([string]$indent, [string]$flagName) + $uuid = New-Guid-String + $flagSynonym = Split-CamelCase $flagName + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $flagName)" + Emit-MLText "$indent`t`t" "Synonym" $flagSynonym + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t`txs:boolean" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tDontCheck" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t" + X "$indent" +} + +function Emit-ExtDimensionAccountingFlag { + param([string]$indent, [string]$flagName) + $uuid = New-Guid-String + $flagSynonym = Split-CamelCase $flagName + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $flagName)" + Emit-MLText "$indent`t`t" "Synonym" $flagSynonym + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t`txs:boolean" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`tfalse" + X "$indent`t`tfalse" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tDontCheck" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t`t" + X "$indent`t`t" + X "$indent`t`tAuto" + X "$indent`t" + X "$indent" +} + +function Emit-URLTemplate { + param([string]$indent, [string]$tmplName, $tmplDef) + $uuid = New-Guid-String + $tmplSynonym = Split-CamelCase $tmplName + + $template = "" + $methods = @{} + + if ($tmplDef -is [string]) { + $template = "$tmplDef" + } else { + $template = if ($tmplDef.template) { "$($tmplDef.template)" } else { "/$($tmplName.ToLower())" } + if ($tmplDef.methods) { + $tmplDef.methods.PSObject.Properties | ForEach-Object { + $methods[$_.Name] = "$($_.Value)" + } + } + } + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $tmplName)" + Emit-MLText "$indent`t`t" "Synonym" $tmplSynonym + X "$indent`t`t" + X "$indent`t" + + if ($methods.Count -gt 0) { + X "$indent`t" + foreach ($methodName in $methods.Keys) { + $methodUuid = New-Guid-String + $httpMethod = $methods[$methodName] + $methodSynonym = Split-CamelCase $methodName + $handler = "${tmplName}${methodName}" + + X "$indent`t`t" + X "$indent`t`t`t" + X "$indent`t`t`t`t$(Esc-Xml $methodName)" + Emit-MLText "$indent`t`t`t`t" "Synonym" $methodSynonym + X "$indent`t`t`t`t$httpMethod" + X "$indent`t`t`t`t$(Esc-Xml $handler)" + X "$indent`t`t`t" + X "$indent`t`t" + } + X "$indent`t" + } else { + X "$indent`t" + } + + X "$indent" +} + +function Emit-Operation { + param([string]$indent, [string]$opName, $opDef) + $uuid = New-Guid-String + $opSynonym = Split-CamelCase $opName + + $returnType = "xs:string" + $nillable = "false" + $transactioned = "false" + $handler = $opName + $params = @{} + + if ($opDef -is [string]) { + $returnType = "$opDef" + } else { + if ($opDef.returnType) { $returnType = "$($opDef.returnType)" } + if ($opDef.nillable -eq $true) { $nillable = "true" } + if ($opDef.transactioned -eq $true) { $transactioned = "true" } + if ($opDef.handler) { $handler = "$($opDef.handler)" } + if ($opDef.parameters) { + $opDef.parameters.PSObject.Properties | ForEach-Object { + $params[$_.Name] = $_.Value + } + } + } + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $opName)" + Emit-MLText "$indent`t`t" "Synonym" $opSynonym + X "$indent`t`t" + X "$indent`t`t$returnType" + X "$indent`t`t$nillable" + X "$indent`t`t$transactioned" + X "$indent`t`t$(Esc-Xml $handler)" + X "$indent`t" + + if ($params.Count -gt 0) { + X "$indent`t" + foreach ($paramName in $params.Keys) { + $paramUuid = New-Guid-String + $paramDef = $params[$paramName] + $paramSynonym = Split-CamelCase $paramName + $paramType = "xs:string" + $paramNillable = "true" + $paramDir = "In" + + if ($paramDef -is [string]) { + $paramType = "$paramDef" + } else { + if ($paramDef.type) { $paramType = "$($paramDef.type)" } + if ($paramDef.nillable -eq $false) { $paramNillable = "false" } + if ($paramDef.direction) { $paramDir = "$($paramDef.direction)" } + } + + X "$indent`t`t" + X "$indent`t`t`t" + X "$indent`t`t`t`t$(Esc-Xml $paramName)" + Emit-MLText "$indent`t`t`t`t" "Synonym" $paramSynonym + X "$indent`t`t`t`t$paramType" + X "$indent`t`t`t`t$paramNillable" + X "$indent`t`t`t`t$paramDir" + X "$indent`t`t`t" + X "$indent`t`t" + } + X "$indent`t" + } else { + X "$indent`t" + } + + X "$indent" +} + +function Emit-AddressingAttribute { + param([string]$indent, $addrDef) + $uuid = New-Guid-String + + $name = "" + $attrSynonym = "" + $typeStr = "" + $addressingDimension = "" + $indexing = "Index" + + $parsed = Parse-AttributeShorthand $addrDef + $name = $parsed.name + $attrSynonym = $parsed.synonym + $typeStr = $parsed.type + if ($addrDef -isnot [string]) { + if ($addrDef.addressingDimension) { $addressingDimension = "$($addrDef.addressingDimension)" } + if ($addrDef.indexing) { $indexing = "$($addrDef.indexing)" } + } + + X "$indent" + X "$indent`t" + X "$indent`t`t$(Esc-Xml $name)" + Emit-MLText "$indent`t`t" "Synonym" $attrSynonym + X "$indent`t`t" + + if ($typeStr) { + Emit-ValueType "$indent`t`t" $typeStr + } else { + X "$indent`t`t" + X "$indent`t`t`txs:string" + X "$indent`t`t" + } + + if ($addressingDimension) { + X "$indent`t`t$addressingDimension" + } else { + X "$indent`t`t" + } + + X "$indent`t`t$indexing" + X "$indent`t`tUse" + X "$indent`t`tUse" + X "$indent`t" + X "$indent" +} + +# --- 14. Namespaces --- + +$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' + +# --- 14a. Detect format version from existing Configuration.xml --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$script:formatVersion = Detect-FormatVersion $OutputDir + +# --- 15. Main assembler --- + +$uuid = New-Guid-String + +# XML declaration +X '' +X "" +X "`t<$objType uuid=`"$uuid`">" + +# InternalInfo +Emit-InternalInfo "`t`t" $objType $objName + +# Properties +X "`t`t" + +switch ($objType) { + "Catalog" { Emit-CatalogProperties "`t`t`t" } + "Document" { Emit-DocumentProperties "`t`t`t" } + "Enum" { Emit-EnumProperties "`t`t`t" } + "Constant" { Emit-ConstantProperties "`t`t`t" } + "InformationRegister" { Emit-InformationRegisterProperties "`t`t`t" } + "AccumulationRegister" { Emit-AccumulationRegisterProperties "`t`t`t" } + "DefinedType" { Emit-DefinedTypeProperties "`t`t`t" } + "CommonModule" { Emit-CommonModuleProperties "`t`t`t" } + "ScheduledJob" { Emit-ScheduledJobProperties "`t`t`t" } + "EventSubscription" { Emit-EventSubscriptionProperties "`t`t`t" } + "Report" { Emit-ReportProperties "`t`t`t" } + "DataProcessor" { Emit-DataProcessorProperties "`t`t`t" } + "ExchangePlan" { Emit-ExchangePlanProperties "`t`t`t" } + "ChartOfCharacteristicTypes" { Emit-ChartOfCharacteristicTypesProperties "`t`t`t" } + "DocumentJournal" { Emit-DocumentJournalProperties "`t`t`t" } + "ChartOfAccounts" { Emit-ChartOfAccountsProperties "`t`t`t" } + "AccountingRegister" { Emit-AccountingRegisterProperties "`t`t`t" } + "ChartOfCalculationTypes" { Emit-ChartOfCalculationTypesProperties "`t`t`t" } + "CalculationRegister" { Emit-CalculationRegisterProperties "`t`t`t" } + "BusinessProcess" { Emit-BusinessProcessProperties "`t`t`t" } + "Task" { Emit-TaskProperties "`t`t`t" } + "HTTPService" { Emit-HTTPServiceProperties "`t`t`t" } + "WebService" { Emit-WebServiceProperties "`t`t`t" } +} + +X "`t`t" + +# ChildObjects +$hasChildren = $false + +# --- Types with Attributes + TabularSections --- +$typesWithAttrTS = @("Catalog","Document","Report","DataProcessor","ExchangePlan", + "ChartOfCharacteristicTypes","ChartOfAccounts","ChartOfCalculationTypes", + "BusinessProcess","Task") + +if ($objType -in $typesWithAttrTS) { + $attrs = @() + if ($def.attributes) { + foreach ($a in $def.attributes) { + $attrs += Parse-AttributeShorthand $a + } + } + $tsSections = [ordered]@{} + if ($def.tabularSections) { + # Normalize array format: [{name:"X", attributes:[...]}, ...] → {"X": [...]} + if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") { + foreach ($ts in $def.tabularSections) { + $tsName = $ts.name + $tsCols = if ($ts.attributes) { @($ts.attributes) } else { @() } + $tsSections[$tsName] = $tsCols + } + } else { + $def.tabularSections.PSObject.Properties | ForEach-Object { + $tsSections[$_.Name] = @($_.Value) + } + } + } + + # ChartOfAccounts: AccountingFlags + ExtDimensionAccountingFlags + $acctFlags = @() + $extDimFlags = @() + if ($objType -eq "ChartOfAccounts") { + if ($def.accountingFlags) { $acctFlags = @($def.accountingFlags) } + if ($def.extDimensionAccountingFlags) { $extDimFlags = @($def.extDimensionAccountingFlags) } + } + + # Task: AddressingAttributes + $addrAttrs = @() + if ($objType -eq "Task" -and $def.addressingAttributes) { + $addrAttrs = @($def.addressingAttributes) + } + + $childCount = $attrs.Count + $tsSections.Count + $acctFlags.Count + $extDimFlags.Count + $addrAttrs.Count + if ($childCount -gt 0) { + $hasChildren = $true + X "`t`t" + $context = switch ($objType) { + "Catalog" { "catalog" } + "Document" { "document" } + { $_ -in @("DataProcessor","Report") } { "processor" } + { $_ -in @("ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes") } { "chart" } + default { "object" } + } + foreach ($a in $attrs) { + Emit-Attribute "`t`t`t" $a $context + } + foreach ($tsName in $tsSections.Keys) { + $columns = $tsSections[$tsName] + Emit-TabularSection "`t`t`t" $tsName $columns $objType $objName + } + foreach ($af in $acctFlags) { + $afName = if ($af.name) { $af.name } else { "$af" } + Emit-AccountingFlag "`t`t`t" $afName + } + foreach ($edf in $extDimFlags) { + $edfName = if ($edf.name) { $edf.name } else { "$edf" } + Emit-ExtDimensionAccountingFlag "`t`t`t" $edfName + } + foreach ($aa in $addrAttrs) { + Emit-AddressingAttribute "`t`t`t" $aa + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- Enum: enum values --- +if ($objType -eq "Enum") { + $values = @() + if ($def.values) { + foreach ($v in $def.values) { + $values += Parse-EnumValueShorthand $v + } + } + if ($values.Count -gt 0) { + $hasChildren = $true + X "`t`t" + foreach ($v in $values) { + Emit-EnumValue "`t`t`t" $v + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- Constant, DefinedType, ScheduledJob, EventSubscription: no ChildObjects --- + +# --- Registers: dimensions + resources + attributes --- +if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister")) { + $dims = @() + $resources = @() + $regAttrs = @() + if ($def.dimensions) { + foreach ($d in $def.dimensions) { + $dims += Parse-AttributeShorthand $d + } + } + if ($def.resources) { + foreach ($r in $def.resources) { + $resources += Parse-AttributeShorthand $r + } + } + if ($def.attributes) { + foreach ($a in $def.attributes) { + $regAttrs += Parse-AttributeShorthand $a + } + } + + if ($dims.Count -gt 0 -or $resources.Count -gt 0 -or $regAttrs.Count -gt 0) { + $hasChildren = $true + X "`t`t" + foreach ($r in $resources) { + Emit-Resource "`t`t`t" $r $objType + } + foreach ($d in $dims) { + Emit-Dimension "`t`t`t" $d $objType + } + # InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory; + # AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT. + $regCtx = if ($objType -eq "InformationRegister") { "register-info" } else { "register-other" } + foreach ($a in $regAttrs) { + Emit-Attribute "`t`t`t" $a $regCtx + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- DocumentJournal: columns --- +if ($objType -eq "DocumentJournal") { + $columns = @() + if ($def.columns) { $columns = @($def.columns) } + if ($columns.Count -gt 0) { + $hasChildren = $true + X "`t`t" + foreach ($col in $columns) { + Emit-Column "`t`t`t" $col + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- HTTPService: URLTemplates --- +if ($objType -eq "HTTPService") { + $urlTemplates = @{} + if ($def.urlTemplates) { + $def.urlTemplates.PSObject.Properties | ForEach-Object { + $urlTemplates[$_.Name] = $_.Value + } + } + if ($urlTemplates.Count -gt 0) { + $hasChildren = $true + X "`t`t" + foreach ($tmplName in $urlTemplates.Keys) { + Emit-URLTemplate "`t`t`t" $tmplName $urlTemplates[$tmplName] + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- WebService: Operations --- +if ($objType -eq "WebService") { + $operations = @{} + if ($def.operations) { + $def.operations.PSObject.Properties | ForEach-Object { + $operations[$_.Name] = $_.Value + } + } + if ($operations.Count -gt 0) { + $hasChildren = $true + X "`t`t" + foreach ($opName in $operations.Keys) { + Emit-Operation "`t`t`t" $opName $operations[$opName] + } + X "`t`t" + } else { + X "`t`t" + } +} + +# --- CommonModule: no ChildObjects --- + +X "`t" +X "" + +$metadataXml = $script:xml.ToString() + +# --- 16. Write files --- + +# Type → plural directory mapping +$script:typePluralMap = @{ + "Catalog" = "Catalogs" + "Document" = "Documents" + "Enum" = "Enums" + "Constant" = "Constants" + "InformationRegister" = "InformationRegisters" + "AccumulationRegister" = "AccumulationRegisters" + "AccountingRegister" = "AccountingRegisters" + "CalculationRegister" = "CalculationRegisters" + "ChartOfAccounts" = "ChartsOfAccounts" + "ChartOfCharacteristicTypes"= "ChartsOfCharacteristicTypes" + "ChartOfCalculationTypes" = "ChartsOfCalculationTypes" + "BusinessProcess" = "BusinessProcesses" + "Task" = "Tasks" + "ExchangePlan" = "ExchangePlans" + "DocumentJournal" = "DocumentJournals" + "Report" = "Reports" + "DataProcessor" = "DataProcessors" + "CommonModule" = "CommonModules" + "ScheduledJob" = "ScheduledJobs" + "EventSubscription" = "EventSubscriptions" + "HTTPService" = "HTTPServices" + "WebService" = "WebServices" + "DefinedType" = "DefinedTypes" +} + +$typePlural = $script:typePluralMap[$objType] +$typeDir = Join-Path $OutputDir $typePlural + +# Main XML file: {OutputDir}/{TypePlural}/{Name}.xml +$mainXmlPath = Join-Path $typeDir "$objName.xml" + +# Types that don't have subdirectory structure (no Ext/, no modules) +$typesNoSubDir = @("DefinedType","ScheduledJob","EventSubscription") + +# Object subdirectory: {OutputDir}/{TypePlural}/{Name}/Ext/ +$objSubDir = Join-Path $typeDir $objName +$extDir = Join-Path $objSubDir "Ext" + +if (-not (Test-Path $typeDir)) { + New-Item -ItemType Directory -Path $typeDir -Force | Out-Null +} +if ($objType -notin $typesNoSubDir) { + if (-not (Test-Path $objSubDir)) { + New-Item -ItemType Directory -Path $objSubDir -Force | Out-Null + } +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($mainXmlPath, $metadataXml, $enc) + +# Module files +$modulesCreated = @() + +# Helper: create Ext/ only when needed (avoids empty Ext/ for Constant, Enum, etc.) +function Ensure-ExtDir { + if (-not (Test-Path $extDir)) { + New-Item -ItemType Directory -Path $extDir -Force | Out-Null + } +} + +# Types with ObjectModule.bsl +$typesWithObjectModule = @("Catalog","Document","Report","DataProcessor","ExchangePlan", + "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task") +# Types with RecordSetModule.bsl +$typesWithRecordSetModule = @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") +# Types with ManagerModule.bsl +$typesWithManagerModule = @("Report","DataProcessor","Constant","Enum") +# Types with ValueManagerModule.bsl +$typesWithValueManagerModule = @("Constant") +# Types with Module.bsl (general) +$typesWithModule = @("CommonModule","HTTPService","WebService") + +if ($objType -in $typesWithObjectModule) { + $modulePath = Join-Path $extDir "ObjectModule.bsl" + if (-not (Test-Path $modulePath)) { + Ensure-ExtDir + [System.IO.File]::WriteAllText($modulePath, "", $enc) + $modulesCreated += $modulePath + } +} +if ($objType -in $typesWithManagerModule) { + $modulePath = Join-Path $extDir "ManagerModule.bsl" + if (-not (Test-Path $modulePath)) { + Ensure-ExtDir + [System.IO.File]::WriteAllText($modulePath, "", $enc) + $modulesCreated += $modulePath + } +} +if ($objType -in $typesWithValueManagerModule) { + $modulePath = Join-Path $extDir "ValueManagerModule.bsl" + if (-not (Test-Path $modulePath)) { + Ensure-ExtDir + [System.IO.File]::WriteAllText($modulePath, "", $enc) + $modulesCreated += $modulePath + } +} +if ($objType -in $typesWithRecordSetModule) { + $modulePath = Join-Path $extDir "RecordSetModule.bsl" + if (-not (Test-Path $modulePath)) { + Ensure-ExtDir + [System.IO.File]::WriteAllText($modulePath, "", $enc) + $modulesCreated += $modulePath + } +} +if ($objType -in $typesWithModule) { + $modulePath = Join-Path $extDir "Module.bsl" + if (-not (Test-Path $modulePath)) { + Ensure-ExtDir + [System.IO.File]::WriteAllText($modulePath, "", $enc) + $modulesCreated += $modulePath + } +} + +# Special files +if ($objType -eq "ExchangePlan") { + $contentPath = Join-Path $extDir "Content.xml" + if (-not (Test-Path $contentPath)) { + Ensure-ExtDir + $contentXml = "`r`n`r`n" + [System.IO.File]::WriteAllText($contentPath, $contentXml, $enc) + $modulesCreated += $contentPath + } +} +if ($objType -eq "BusinessProcess") { + $flowchartPath = Join-Path $extDir "Flowchart.xml" + if (-not (Test-Path $flowchartPath)) { + Ensure-ExtDir + $flowchartXml = "`r`n`r`n" + [System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc) + $modulesCreated += $flowchartPath + } +} + +# --- 17. Register in Configuration.xml --- + +$configXmlPath = Join-Path $OutputDir "Configuration.xml" +$regResult = $null + +# XML tag name for Configuration.xml ChildObjects +$childTag = $objType + +if (Test-Path $configXmlPath) { + $configDoc = New-Object System.Xml.XmlDocument + $configDoc.PreserveWhitespace = $true + $configDoc.Load($configXmlPath) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr) + if ($childObjects) { + $existing = $childObjects.SelectNodes("md:$childTag", $nsMgr) + $alreadyExists = $false + foreach ($e in $existing) { + if ($e.InnerText -eq $objName) { + $alreadyExists = $true + break + } + } + + if ($alreadyExists) { + $regResult = "already" + } else { + $newElem = $configDoc.CreateElement($childTag, "http://v8.1c.ru/8.3/MDClasses") + $newElem.InnerText = $objName + + if ($existing.Count -gt 0) { + # Insert after last existing element of same type + $lastElem = $existing[$existing.Count - 1] + $newWs = $configDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertAfter($newWs, $lastElem) | Out-Null + $childObjects.InsertAfter($newElem, $newWs) | Out-Null + } else { + # No existing elements of this type — insert before closing whitespace + $lastChild = $childObjects.LastChild + if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $newWs = $configDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertBefore($newWs, $lastChild) | Out-Null + $childObjects.InsertBefore($newElem, $lastChild) | Out-Null + } else { + $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null + $childObjects.AppendChild($newElem) | Out-Null + $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null + } + } + + # Save + $cfgSettings = New-Object System.Xml.XmlWriterSettings + $cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true) + $cfgSettings.Indent = $false + $stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create) + $writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings) + $configDoc.Save($writer) + $writer.Close() + $stream.Close() + + $regResult = "added" + } + } else { + $regResult = "no-childobj" + } +} else { + $regResult = "no-config" +} + +# --- 18. Summary --- + +$attrCount = 0 +$tsCount = 0 +$dimCount = 0 +$resCount = 0 +$valCount = 0 +$colCount = 0 + +if ($def.attributes) { $attrCount = @($def.attributes).Count } +if ($def.tabularSections) { + if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") { + $tsCount = @($def.tabularSections).Count + } else { + $tsCount = @($def.tabularSections.PSObject.Properties).Count + } +} +if ($def.dimensions) { $dimCount = @($def.dimensions).Count } +if ($def.resources) { $resCount = @($def.resources).Count } +if ($def.values) { $valCount = @($def.values).Count } +if ($def.columns) { $colCount = @($def.columns).Count } + +Write-Host "[OK] $objType '$objName' compiled" +Write-Host " UUID: $uuid" +Write-Host " File: $mainXmlPath" + +$details = @() +if ($attrCount -gt 0) { $details += "Attributes: $attrCount" } +if ($tsCount -gt 0) { $details += "TabularSections: $tsCount" } +if ($dimCount -gt 0) { $details += "Dimensions: $dimCount" } +if ($resCount -gt 0) { $details += "Resources: $resCount" } +if ($valCount -gt 0) { $details += "Values: $valCount" } +if ($colCount -gt 0) { $details += "Columns: $colCount" } + +if ($details.Count -gt 0) { + Write-Host " $($details -join ', ')" +} + +foreach ($mc in $modulesCreated) { + Write-Host " Module: $mc" +} + +switch ($regResult) { + "added" { Write-Host " Configuration.xml: <$childTag>$objName added to ChildObjects" } + "already" { Write-Host " Configuration.xml: <$childTag>$objName already registered" } + "no-childobj" { Write-Warning "Configuration.xml found but not found" } + "no-config" { Write-Host " Configuration.xml: not found at $configXmlPath (register manually)" } +} + +# Cross-reference hints +if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) { + Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:" + Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" +} +if ($objType -eq "CalculationRegister" -and -not $def.chartOfCalculationTypes) { + Write-Host "[HINT] CalculationRegister requires ChartOfCalculationTypes reference:" + Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`"" +} +if ($objType -eq "BusinessProcess" -and -not $def.task) { + Write-Host "[HINT] BusinessProcess requires Task reference:" + Write-Host " /meta-edit -Operation modify-property -Value `"Task=Task.XXX`"" +} +if ($objType -eq "ChartOfAccounts") { + $maxExtDim = if ($null -ne $def.maxExtDimensionCount) { [int]$def.maxExtDimensionCount } else { 0 } + if ($maxExtDim -gt 0 -and -not $def.extDimensionTypes) { + Write-Host "[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:" + Write-Host " /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`"" + } +} diff --git a/.claude/skills/meta-edit/SKILL.md b/.claude/skills/meta-edit/SKILL.md index 64e76df7..ed8c11f1 100644 --- a/.claude/skills/meta-edit/SKILL.md +++ b/.claude/skills/meta-edit/SKILL.md @@ -1,108 +1,108 @@ ---- -name: meta-edit -description: Точечное редактирование объекта метаданных 1С. Используй когда нужно добавить, удалить или изменить реквизиты, табличные части, измерения, ресурсы или свойства существующего объекта конфигурации -argument-hint: -Operation -Value "" | -DefinitionFile [-NoValidate] -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /meta-edit — точечное редактирование метаданных 1С - -Атомарные операции модификации существующих XML объектов метаданных. - -## Команда - -### Inline mode (простые операции) - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -ObjectPath "" -Operation -Value "" -``` - -### JSON mode (сложные/комбинированные) - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -DefinitionFile "" -ObjectPath "" -``` - -| Параметр | Описание | -|----------|----------| -| ObjectPath | XML-файл или директория объекта (обязательный, авторезолв `.xml`) | -| Operation | Inline-операция (альтернатива DefinitionFile) | -| Value | Значение для inline-операции | -| DefinitionFile | JSON-файл с операциями (альтернатива Operation) | -| NoValidate | Не запускать meta-validate после правки | - -## Операции — сводная таблица - -Batch через `;;` во всех операциях. Подробный синтаксис — в файлах по ссылкам. - -### Дочерние элементы — [child-operations.md](child-operations.md) - -| Операция | Формат Value | Пример | -|----------|-------------|--------| -| `add-attribute` | `Имя: Тип \| флаги` | `"Сумма: Число(15,2) \| req, index"` | -| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` | -| `add-dimension` | `Имя: Тип \| флаги` | `"Организация: CatalogRef.Организации \| master"` | -| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` | -| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` | -| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` | -| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` | -| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` | -| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` | -| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` | -| `modify-attribute` | `Имя: ключ=значение` | `"СтароеИмя: name=НовоеИмя, type=Строка(500)"` | -| `modify-ts-attribute` | `ТЧ.Имя: ключ=значение` | `"Товары.Рекв: name=НовоеИмя"` | -| `modify-ts` | `ТЧ: ключ=значение` | `"Товары: synonym=Товарный состав"` | - -Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`. - -### Свойства объекта — [properties-reference.md](properties-reference.md) - -| Операция | Формат Value | Пример | -|----------|-------------|--------| -| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` | -| `add-owner` | `MetaType.Name` | `"Catalog.Контрагенты ;; Catalog.Организации"` | -| `add-registerRecord` | `MetaType.Name` | `"AccumulationRegister.ОстаткиТоваров"` | -| `add-basedOn` | `MetaType.Name` | `"Document.ЗаказКлиента"` | -| `add-inputByString` | `Путь поля` | `"StandardAttribute.Description"` | -| `set-owners` / `set-registerRecords` / `set-basedOn` / `set-inputByString` | Замена всего списка | `"Catalog.Орг ;; Catalog.Контр"` | -| `remove-owner` / `remove-registerRecord` / ... | Удаление из списка | `"Catalog.Контрагенты"` | - -### JSON DSL — [json-dsl.md](json-dsl.md) - -Для комбинированных операций (add + remove + modify в одном файле), синонимы ключей/типов, таблица поддерживаемых объектов. - -## Быстрые примеры - -```powershell -# Добавить реквизиты --Operation add-attribute -Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index" - -# Составной тип (несколько типов через +) --Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" - -# Добавить ТЧ с реквизитами --Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)" - -# Удалить реквизит --Operation remove-attribute -Value "УстаревшийРеквизит" - -# Переименовать + сменить тип --Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" - -# Изменить свойства объекта --Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" - -# Владельцы справочника --Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации" -``` - -## Верификация - -``` -/meta-validate — валидация после редактирования -/meta-info — визуальная сводка -``` +--- +name: meta-edit +description: Точечное редактирование объекта метаданных 1С. Используй когда нужно добавить, удалить или изменить реквизиты, табличные части, измерения, ресурсы или свойства существующего объекта конфигурации +argument-hint: -Operation -Value "" | -DefinitionFile [-NoValidate] +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /meta-edit — точечное редактирование метаданных 1С + +Атомарные операции модификации существующих XML объектов метаданных. + +## Команда + +### Inline mode (простые операции) + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -ObjectPath "" -Operation -Value "" +``` + +### JSON mode (сложные/комбинированные) + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -DefinitionFile "" -ObjectPath "" +``` + +| Параметр | Описание | +|----------|----------| +| ObjectPath | XML-файл или директория объекта (обязательный, авторезолв `.xml`) | +| Operation | Inline-операция (альтернатива DefinitionFile) | +| Value | Значение для inline-операции | +| DefinitionFile | JSON-файл с операциями (альтернатива Operation) | +| NoValidate | Не запускать meta-validate после правки | + +## Операции — сводная таблица + +Batch через `;;` во всех операциях. Подробный синтаксис — в файлах по ссылкам. + +### Дочерние элементы — [child-operations.md](child-operations.md) + +| Операция | Формат Value | Пример | +|----------|-------------|--------| +| `add-attribute` | `Имя: Тип \| флаги` | `"Сумма: Число(15,2) \| req, index"` | +| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` | +| `add-dimension` | `Имя: Тип \| флаги` | `"Организация: CatalogRef.Организации \| master"` | +| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` | +| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` | +| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` | +| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` | +| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` | +| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` | +| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` | +| `modify-attribute` | `Имя: ключ=значение` | `"СтароеИмя: name=НовоеИмя, type=Строка(500)"` | +| `modify-ts-attribute` | `ТЧ.Имя: ключ=значение` | `"Товары.Рекв: name=НовоеИмя"` | +| `modify-ts` | `ТЧ: ключ=значение` | `"Товары: synonym=Товарный состав"` | + +Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`. + +### Свойства объекта — [properties-reference.md](properties-reference.md) + +| Операция | Формат Value | Пример | +|----------|-------------|--------| +| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` | +| `add-owner` | `MetaType.Name` | `"Catalog.Контрагенты ;; Catalog.Организации"` | +| `add-registerRecord` | `MetaType.Name` | `"AccumulationRegister.ОстаткиТоваров"` | +| `add-basedOn` | `MetaType.Name` | `"Document.ЗаказКлиента"` | +| `add-inputByString` | `Путь поля` | `"StandardAttribute.Description"` | +| `set-owners` / `set-registerRecords` / `set-basedOn` / `set-inputByString` | Замена всего списка | `"Catalog.Орг ;; Catalog.Контр"` | +| `remove-owner` / `remove-registerRecord` / ... | Удаление из списка | `"Catalog.Контрагенты"` | + +### JSON DSL — [json-dsl.md](json-dsl.md) + +Для комбинированных операций (add + remove + modify в одном файле), синонимы ключей/типов, таблица поддерживаемых объектов. + +## Быстрые примеры + +```powershell +# Добавить реквизиты +-Operation add-attribute -Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index" + +# Составной тип (несколько типов через +) +-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" + +# Добавить ТЧ с реквизитами +-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)" + +# Удалить реквизит +-Operation remove-attribute -Value "УстаревшийРеквизит" + +# Переименовать + сменить тип +-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" + +# Изменить свойства объекта +-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" + +# Владельцы справочника +-Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации" +``` + +## Верификация + +``` +/meta-validate — валидация после редактирования +/meta-info — визуальная сводка +``` diff --git a/.claude/skills/meta-edit/child-operations.md b/.claude/skills/meta-edit/child-operations.md index dc9a2dc1..f6cf6581 100644 --- a/.claude/skills/meta-edit/child-operations.md +++ b/.claude/skills/meta-edit/child-operations.md @@ -1,116 +1,116 @@ -# Inline-операции над дочерними элементами - -Подробный справочник операций `add-*` / `remove-*` / `modify-*` для дочерних элементов объекта метаданных. - -## Общие правила - -**Batch-режим** — несколько элементов через `;;`: -``` --Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index" -``` - -**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги` - -Флаги: `req` (FillChecking=ShowError), `index` (Indexing=Index), `master` (Master=true, только dimensions), `mainFilter` (MainFilterOperand, только dimensions). - -**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`: -```powershell --Operation add-attribute -Value "Склад: CatalogRef.Склады >> after Организация" -``` - -## Составные типы - -Для реквизитов с несколькими допустимыми типами — разделитель `+`: -```powershell --Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" --Operation add-attribute -Value "Значение: Строка + Число(15,2) | req" --Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата" -``` - -В JSON DSL — массив в `type`: -```json -{ "name": "Значение", "type": ["Строка", "Число(15,2)", "Дата", "CatalogRef.Контрагенты"] } -``` - -## add-attribute / add-dimension / add-resource / add-column - -```powershell --Operation add-attribute -Value "Комментарий: Строка(200)" --Operation add-attribute -Value "Сумма: Число(15,2) | req, index" --Operation add-attribute -Value "Ном: CatalogRef.Номенклатура | req ;; Кол: Число(15,3)" --Operation add-dimension -Value "Организация: CatalogRef.Организации | master, mainFilter" --Operation add-resource -Value "Сумма: Число(15,2)" --Operation add-column -Value "Тип: EnumRef.ТипыДокументов" -``` - -## add-ts - -Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...` - -```powershell --Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)" -``` - -## add-ts-attribute / remove-ts-attribute / modify-ts-attribute - -Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация). - -```powershell -# Добавить реквизит в ТЧ --Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС" --Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)" - -# Позиционная вставка в ТЧ --Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена" - -# Удалить реквизит из ТЧ --Operation remove-ts-attribute -Value "Товары.УстаревшийРекв" --Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2" - -# Изменить реквизит в ТЧ (rename, type change и т.д.) --Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)" -``` - -Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`. - -## modify-ts - -Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.): - -```powershell --Operation modify-ts -Value "Товары: synonym=Товарный состав" --Operation modify-ts -Value "Товары: fillChecking=ShowError" -``` - -Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`. - -## add-enumValue / add-form / add-template / add-command - -Просто имена (batch через `;;`): -```powershell --Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3" --Operation add-form -Value "ФормаЭлемента ;; ФормаСписка" --Operation add-template -Value "ПечатнаяФорма" --Operation add-command -Value "Команда1" -``` - -## remove-* - -Имя элемента (или несколько через `;;`): -```powershell --Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин" --Operation remove-ts -Value "УстаревшаяТЧ" --Operation remove-enumValue -Value "НеиспользуемоеЗначение" -``` - -## modify-attribute / modify-dimension / modify-resource / modify-enumValue / modify-column - -Формат: `ИмяЭлемента: ключ=значение, ключ=значение` - -Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др. - -```powershell --Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" --Operation modify-attribute -Value "Комментарий: indexing=Index" --Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение" -``` +# Inline-операции над дочерними элементами + +Подробный справочник операций `add-*` / `remove-*` / `modify-*` для дочерних элементов объекта метаданных. + +## Общие правила + +**Batch-режим** — несколько элементов через `;;`: +``` +-Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index" +``` + +**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги` + +Флаги: `req` (FillChecking=ShowError), `index` (Indexing=Index), `master` (Master=true, только dimensions), `mainFilter` (MainFilterOperand, только dimensions). + +**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`: +```powershell +-Operation add-attribute -Value "Склад: CatalogRef.Склады >> after Организация" +``` + +## Составные типы + +Для реквизитов с несколькими допустимыми типами — разделитель `+`: +```powershell +-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" +-Operation add-attribute -Value "Значение: Строка + Число(15,2) | req" +-Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата" +``` + +В JSON DSL — массив в `type`: +```json +{ "name": "Значение", "type": ["Строка", "Число(15,2)", "Дата", "CatalogRef.Контрагенты"] } +``` + +## add-attribute / add-dimension / add-resource / add-column + +```powershell +-Operation add-attribute -Value "Комментарий: Строка(200)" +-Operation add-attribute -Value "Сумма: Число(15,2) | req, index" +-Operation add-attribute -Value "Ном: CatalogRef.Номенклатура | req ;; Кол: Число(15,3)" +-Operation add-dimension -Value "Организация: CatalogRef.Организации | master, mainFilter" +-Operation add-resource -Value "Сумма: Число(15,2)" +-Operation add-column -Value "Тип: EnumRef.ТипыДокументов" +``` + +## add-ts + +Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...` + +```powershell +-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)" +``` + +## add-ts-attribute / remove-ts-attribute / modify-ts-attribute + +Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация). + +```powershell +# Добавить реквизит в ТЧ +-Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС" +-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)" + +# Позиционная вставка в ТЧ +-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена" + +# Удалить реквизит из ТЧ +-Operation remove-ts-attribute -Value "Товары.УстаревшийРекв" +-Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2" + +# Изменить реквизит в ТЧ (rename, type change и т.д.) +-Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)" +``` + +Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`. + +## modify-ts + +Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.): + +```powershell +-Operation modify-ts -Value "Товары: synonym=Товарный состав" +-Operation modify-ts -Value "Товары: fillChecking=ShowError" +``` + +Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`. + +## add-enumValue / add-form / add-template / add-command + +Просто имена (batch через `;;`): +```powershell +-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3" +-Operation add-form -Value "ФормаЭлемента ;; ФормаСписка" +-Operation add-template -Value "ПечатнаяФорма" +-Operation add-command -Value "Команда1" +``` + +## remove-* + +Имя элемента (или несколько через `;;`): +```powershell +-Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин" +-Operation remove-ts -Value "УстаревшаяТЧ" +-Operation remove-enumValue -Value "НеиспользуемоеЗначение" +``` + +## modify-attribute / modify-dimension / modify-resource / modify-enumValue / modify-column + +Формат: `ИмяЭлемента: ключ=значение, ключ=значение` + +Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др. + +```powershell +-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" +-Operation modify-attribute -Value "Комментарий: indexing=Index" +-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение" +``` diff --git a/.claude/skills/meta-edit/json-dsl.md b/.claude/skills/meta-edit/json-dsl.md index 4e3a116d..245197c1 100644 --- a/.claude/skills/meta-edit/json-dsl.md +++ b/.claude/skills/meta-edit/json-dsl.md @@ -1,148 +1,148 @@ -# JSON DSL — режим определений - -Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима. - -```powershell -powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "" -ObjectPath "" -``` - -## add — добавить элементы - -```json -{ - "add": { - "attributes": [ - { "name": "Комментарий", "type": "Строка(200)" }, - { "name": "Сумма", "type": "Число(15,2)", "indexing": "Index" } - ], - "tabularSections": [{ - "name": "Товары", - "attrs": [ - { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, - { "name": "Количество", "type": "Число(15,3)" } - ] - }], - "forms": ["ФормаЭлемента"], - "templates": ["ПечатнаяФорма"] - } -} -``` - -Реквизиты можно задавать shorthand-строками: `"Сумма: Число(15,2) | req, index"`. - -## remove — удалить элементы - -```json -{ - "remove": { - "attributes": ["СтарыйРеквизит"], - "tabularSections": ["УстаревшаяТЧ"] - } -} -``` - -## modify — изменить существующие - -```json -{ - "modify": { - "properties": { - "CodeLength": 11, - "Hierarchical": true, - "Owners": ["Catalog.Контрагенты", "Catalog.Организации"], - "RegisterRecords": ["AccumulationRegister.Продажи"], - "InputByString": ["StandardAttribute.Description"] - }, - "attributes": { - "Комментарий": { "type": "Строка(500)" }, - "СтароеИмя": { "name": "НовоеИмя" } - } - } -} -``` - -## modify — реквизиты внутри ТЧ - -```json -{ - "modify": { - "tabularSections": { - "Товары": { - "add": ["СтавкаНДС: EnumRef.СтавкиНДС", "Скидка: Число(15,2)"], - "remove": ["УстаревшийРекв"], - "modify": { - "СтароеИмя": { "name": "НовоеИмя", "type": "Строка(500)" } - } - } - } - } -} -``` - -## Комбинирование - -Все три операции (`add`, `remove`, `modify`) можно указать в одном JSON-файле: - -```json -{ - "add": { "tabularSections": [{ "name": "НоваяТЧ", "attrs": ["Имя: Строка(100)"] }] }, - "modify": { - "tabularSections": { - "СуществующаяТЧ": { - "add": ["НовыйРекв: Число(15,2)"], - "remove": ["СтарыйРекв"] - } - } - } -} -``` - -## Позиционная вставка - -```json -{ "name": "Склад", "type": "CatalogRef.Склады", "after": "Организация" } -``` - -## Синонимы ключей (case-insensitive) - -**Операции:** `add`/`добавить`, `remove`/`удалить`, `modify`/`изменить` - -| Каноническое | Синонимы | -|-------------|----------| -| attributes | реквизиты, attrs | -| tabularSections | табличныеЧасти, тч, ts | -| dimensions | измерения, dims | -| resources | ресурсы, res | -| enumValues | значения, values | -| columns | графы, колонки | -| forms | формы | -| templates | макеты | -| commands | команды | -| properties | свойства | - -## Составные типы - -Для полей с несколькими допустимыми типами — массив в `type`: - -```json -{ "name": "Значение", "type": ["Строка", "Число(15,2)", "Дата", "CatalogRef.Контрагенты"] } -``` - -В inline-формате — через `+`: -``` -"Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" -``` - -## Синонимы типов - -`Строка(200)`, `Число(15,2)`, `Булево`, `Дата`, `ДатаВремя`, `ХранилищеЗначения`, `СправочникСсылка.XXX`, `ДокументСсылка.XXX`, `ПеречислениеСсылка.XXX`, `ОпределяемыйТип.XXX`. - -## Поддерживаемые типы объектов - -| Тип объекта | Допустимые add-типы | -|-------------|-------------------| -| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, forms, templates, commands | -| Enum | enumValues, forms, templates, commands | -| *Register (4 типа) | dimensions, resources, attributes, forms, templates, commands | -| DocumentJournal | columns, forms, templates, commands | -| Constant | forms | +# JSON DSL — режим определений + +Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима. + +```powershell +powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "" -ObjectPath "" +``` + +## add — добавить элементы + +```json +{ + "add": { + "attributes": [ + { "name": "Комментарий", "type": "Строка(200)" }, + { "name": "Сумма", "type": "Число(15,2)", "indexing": "Index" } + ], + "tabularSections": [{ + "name": "Товары", + "attrs": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, + { "name": "Количество", "type": "Число(15,3)" } + ] + }], + "forms": ["ФормаЭлемента"], + "templates": ["ПечатнаяФорма"] + } +} +``` + +Реквизиты можно задавать shorthand-строками: `"Сумма: Число(15,2) | req, index"`. + +## remove — удалить элементы + +```json +{ + "remove": { + "attributes": ["СтарыйРеквизит"], + "tabularSections": ["УстаревшаяТЧ"] + } +} +``` + +## modify — изменить существующие + +```json +{ + "modify": { + "properties": { + "CodeLength": 11, + "Hierarchical": true, + "Owners": ["Catalog.Контрагенты", "Catalog.Организации"], + "RegisterRecords": ["AccumulationRegister.Продажи"], + "InputByString": ["StandardAttribute.Description"] + }, + "attributes": { + "Комментарий": { "type": "Строка(500)" }, + "СтароеИмя": { "name": "НовоеИмя" } + } + } +} +``` + +## modify — реквизиты внутри ТЧ + +```json +{ + "modify": { + "tabularSections": { + "Товары": { + "add": ["СтавкаНДС: EnumRef.СтавкиНДС", "Скидка: Число(15,2)"], + "remove": ["УстаревшийРекв"], + "modify": { + "СтароеИмя": { "name": "НовоеИмя", "type": "Строка(500)" } + } + } + } + } +} +``` + +## Комбинирование + +Все три операции (`add`, `remove`, `modify`) можно указать в одном JSON-файле: + +```json +{ + "add": { "tabularSections": [{ "name": "НоваяТЧ", "attrs": ["Имя: Строка(100)"] }] }, + "modify": { + "tabularSections": { + "СуществующаяТЧ": { + "add": ["НовыйРекв: Число(15,2)"], + "remove": ["СтарыйРекв"] + } + } + } +} +``` + +## Позиционная вставка + +```json +{ "name": "Склад", "type": "CatalogRef.Склады", "after": "Организация" } +``` + +## Синонимы ключей (case-insensitive) + +**Операции:** `add`/`добавить`, `remove`/`удалить`, `modify`/`изменить` + +| Каноническое | Синонимы | +|-------------|----------| +| attributes | реквизиты, attrs | +| tabularSections | табличныеЧасти, тч, ts | +| dimensions | измерения, dims | +| resources | ресурсы, res | +| enumValues | значения, values | +| columns | графы, колонки | +| forms | формы | +| templates | макеты | +| commands | команды | +| properties | свойства | + +## Составные типы + +Для полей с несколькими допустимыми типами — массив в `type`: + +```json +{ "name": "Значение", "type": ["Строка", "Число(15,2)", "Дата", "CatalogRef.Контрагенты"] } +``` + +В inline-формате — через `+`: +``` +"Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты" +``` + +## Синонимы типов + +`Строка(200)`, `Число(15,2)`, `Булево`, `Дата`, `ДатаВремя`, `ХранилищеЗначения`, `СправочникСсылка.XXX`, `ДокументСсылка.XXX`, `ПеречислениеСсылка.XXX`, `ОпределяемыйТип.XXX`. + +## Поддерживаемые типы объектов + +| Тип объекта | Допустимые add-типы | +|-------------|-------------------| +| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, forms, templates, commands | +| Enum | enumValues, forms, templates, commands | +| *Register (4 типа) | dimensions, resources, attributes, forms, templates, commands | +| DocumentJournal | columns, forms, templates, commands | +| Constant | forms | diff --git a/.claude/skills/meta-edit/properties-reference.md b/.claude/skills/meta-edit/properties-reference.md index 84f7d114..74a7eb49 100644 --- a/.claude/skills/meta-edit/properties-reference.md +++ b/.claude/skills/meta-edit/properties-reference.md @@ -1,54 +1,54 @@ -# Свойства объекта и complex properties - -Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString). - -## modify-property - -Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`): -```powershell --Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" --Operation modify-property -Value "Hierarchical=true" -``` - -## Complex properties - -Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`. - -| Свойство | Объекты | Inline-значение | -|----------|---------|-----------------| -| Owners | Catalog, ChartOfCharacteristicTypes | `Catalog.XXX` | -| RegisterRecords | Document | `AccumulationRegister.XXX` | -| BasedOn | Document, Catalog, BP, Task | `Document.XXX` | -| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` | - -### add-owner / add-registerRecord / add-basedOn - -Полное имя метаданных `MetaType.Name`: -```powershell --Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации" --Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров" --Operation add-basedOn -Value "Document.ЗаказКлиента" -``` - -### add-inputByString - -Пути полей (префикс `MetaType.Name.` добавляется автоматически): -```powershell --Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" -``` - -### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString - -```powershell --Operation remove-owner -Value "Catalog.Контрагенты" --Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code" -``` - -### set-owners / set-registerRecords / set-basedOn / set-inputByString - -Заменяют **весь список** (в отличие от add/remove): -```powershell --Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты" --Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров" --Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" -``` +# Свойства объекта и complex properties + +Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString). + +## modify-property + +Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`): +```powershell +-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" +-Operation modify-property -Value "Hierarchical=true" +``` + +## Complex properties + +Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`. + +| Свойство | Объекты | Inline-значение | +|----------|---------|-----------------| +| Owners | Catalog, ChartOfCharacteristicTypes | `Catalog.XXX` | +| RegisterRecords | Document | `AccumulationRegister.XXX` | +| BasedOn | Document, Catalog, BP, Task | `Document.XXX` | +| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` | + +### add-owner / add-registerRecord / add-basedOn + +Полное имя метаданных `MetaType.Name`: +```powershell +-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации" +-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров" +-Operation add-basedOn -Value "Document.ЗаказКлиента" +``` + +### add-inputByString + +Пути полей (префикс `MetaType.Name.` добавляется автоматически): +```powershell +-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" +``` + +### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString + +```powershell +-Operation remove-owner -Value "Catalog.Контрагенты" +-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code" +``` + +### set-owners / set-registerRecords / set-basedOn / set-inputByString + +Заменяют **весь список** (в отличие от add/remove): +```powershell +-Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты" +-Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров" +-Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" +``` diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index bf984dc5..1ae0d636 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,2538 +1,2538 @@ -# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [string]$DefinitionFile, - - [Parameter(Mandatory)] - [Alias('Path')] - [string]$ObjectPath, - - # Inline mode (alternative to DefinitionFile) - [ValidateSet( - "add-attribute", "add-ts", "add-dimension", "add-resource", - "add-enumValue", "add-column", "add-form", "add-template", "add-command", - "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", - "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", - "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", - "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", - "add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts", - "modify-attribute", "modify-dimension", "modify-resource", - "modify-enumValue", "modify-column", - "modify-property", - "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString" - )] - [string]$Operation, - [string]$Value, - - [switch]$NoValidate -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# ============================================================ -# Section 1: Parameters + loading -# ============================================================ - -# --- Mode validation --- -if ($DefinitionFile -and $Operation) { - Write-Error "Cannot use both -DefinitionFile and -Operation" - exit 1 -} -if (-not $DefinitionFile -and -not $Operation) { - Write-Error "Either -DefinitionFile or -Operation is required" - exit 1 -} - -# --- Enum value normalization (same as meta-compile) --- -$script:enumValueAliases = @{ - "Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers" - "RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate" - "ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent" - "NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse" - "Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod" - "None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month" - "Quarterly" = "Quarter"; "Yearly" = "Year" - "Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"; "Месяц" = "Month" - "Квартал" = "Quarter"; "Год" = "Year" - "ПозицияРегистратора" = "RecorderPosition" - "Автоматический" = "Automatic"; "Управляемый" = "Managed" - "Использовать" = "Use"; "НеИспользовать" = "DontUse" - "Разрешить" = "Allow"; "Запретить" = "Deny" - "ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays" - "ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode" - "НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning" - "НеИндексировать" = "DontIndex"; "Индексировать" = "Index" - "ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder" -} - -$script:validEnumValues = @{ - "RegisterType" = @("Balance","Turnovers") - "WriteMode" = @("Independent","RecorderSubordinate") - "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") - "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") - "DataLockControlMode" = @("Automatic","Managed") - "FullTextSearch" = @("Use","DontUse") - "DataHistory" = @("Use","DontUse") - "DefaultPresentation" = @("AsDescription","AsCode") - "Posting" = @("Allow","Deny") - "RealTimePosting" = @("Allow","Deny") - "EditType" = @("InDialog","InList","BothWays") - "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") - "CodeType" = @("String","Number") - "CodeAllowedLength" = @("Variable","Fixed") - "NumberType" = @("String","Number") - "NumberAllowedLength" = @("Variable","Fixed") - "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") - "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") - "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") - "ReuseSessions" = @("DontUse","AutoUse") - "FillChecking" = @("DontCheck","ShowError","ShowWarning") - "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") -} - -function Normalize-EnumValue { - param([string]$propName, [string]$value) - # 1. Check alias dictionary — silent auto-correct - if ($script:enumValueAliases.ContainsKey($value)) { - return $script:enumValueAliases[$value] - } - # 2. Case-insensitive match against valid values — silent - $valid = $script:validEnumValues[$propName] - if ($valid) { - foreach ($v in $valid) { - if ($v -ieq $value) { return $v } - } - # 3. Known property, unknown value — error with hint - Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')" - exit 1 - } - # 4. Unknown property — pass-through (no validation data) - return $value -} - -# --- Load JSON definition (DefinitionFile mode) --- -$def = $null -if ($DefinitionFile) { - if (-not (Test-Path $DefinitionFile)) { - Write-Error "Definition file not found: $DefinitionFile" - exit 1 - } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile - $def = $jsonText | ConvertFrom-Json -} - -# --- Resolve object path --- -if (Test-Path $ObjectPath -PathType Container) { - $dirName = Split-Path $ObjectPath -Leaf - $candidate = Join-Path $ObjectPath "$dirName.xml" - $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" - if (Test-Path $candidate) { - $ObjectPath = $candidate - } elseif (Test-Path $sibling) { - $ObjectPath = $sibling - } else { - Write-Error "Directory given but no $dirName.xml found inside or as sibling" - exit 1 - } -} -# File not found — check Dir/Name/Name.xml → Dir/Name.xml -if (-not (Test-Path $ObjectPath)) { - $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) - $parentDir = Split-Path $ObjectPath - $parentDirName = Split-Path $parentDir -Leaf - if ($fileName -eq $parentDirName) { - $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" - if (Test-Path $candidate) { $ObjectPath = $candidate } - } -} -if (-not (Test-Path $ObjectPath)) { - Write-Error "Object file not found: $ObjectPath" - exit 1 -} -$resolvedPath = (Resolve-Path $ObjectPath).Path - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $resolvedPath 'editable' - -# --- Load XML --- -$script:xmlDoc = New-Object System.Xml.XmlDocument -$script:xmlDoc.PreserveWhitespace = $true -$script:xmlDoc.Load($resolvedPath) - -# --- Counters --- -$script:addCount = 0 -$script:removeCount = 0 -$script:modifyCount = 0 -$script:warnCount = 0 - -function Warn($msg) { - Write-Host "[WARN] $msg" -ForegroundColor Yellow - $script:warnCount++ -} - -function Info($msg) { - Write-Host "[INFO] $msg" -ForegroundColor Cyan -} - -# ============================================================ -# Section 2: Detect object type -# ============================================================ - -$root = $script:xmlDoc.DocumentElement -if ($root.LocalName -ne "MetaDataObject") { - Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)" - exit 1 -} - -# Find the first child element — this is the object type element -$script:objElement = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $script:objElement = $child - break - } -} -if (-not $script:objElement) { - Write-Error "No object element found under MetaDataObject" - exit 1 -} - -$script:objType = $script:objElement.LocalName -$script:mdNs = $script:objElement.NamespaceURI - -# Find Properties and ChildObjects -$script:propertiesEl = $null -$script:childObjectsEl = $null -foreach ($child in $script:objElement.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -eq "Properties") { $script:propertiesEl = $child } - if ($child.LocalName -eq "ChildObjects") { $script:childObjectsEl = $child } -} - -if (-not $script:propertiesEl) { - Write-Error "No found in $($script:objType)" - exit 1 -} - -# Extract object name -$script:objName = "" -foreach ($child in $script:propertiesEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { - $script:objName = $child.InnerText.Trim() - break - } -} - -Info "Object: $($script:objType).$($script:objName)" - -# ============================================================ -# Section 3: Synonym tables -# ============================================================ - -# Operation synonyms -$script:operationSynonyms = @{ - "add" = "add"; "добавить" = "add" - "remove" = "remove"; "удалить" = "remove" - "modify" = "modify"; "изменить" = "modify" -} - -# Child type synonyms -$script:childTypeSynonyms = @{ - "attributes" = "attributes"; "реквизиты" = "attributes"; "attrs" = "attributes" - "tabularsections" = "tabularSections"; "табличныечасти" = "tabularSections"; "тч" = "tabularSections"; "ts" = "tabularSections" - "dimensions" = "dimensions"; "измерения" = "dimensions"; "dims" = "dimensions" - "resources" = "resources"; "ресурсы" = "resources"; "res" = "resources" - "enumvalues" = "enumValues"; "значения" = "enumValues"; "values" = "enumValues" - "columns" = "columns"; "графы" = "columns"; "колонки" = "columns" - "forms" = "forms"; "формы" = "forms" - "templates" = "templates"; "макеты" = "templates" - "commands" = "commands"; "команды" = "commands" - "properties" = "properties"; "свойства" = "properties" -} - -# Type synonyms (from meta-compile) -$script:typeSynonyms = New-Object System.Collections.Hashtable -$script:typeSynonyms["число"] = "Number" -$script:typeSynonyms["строка"] = "String" -$script:typeSynonyms["булево"] = "Boolean" -$script:typeSynonyms["дата"] = "Date" -$script:typeSynonyms["датавремя"]= "DateTime" -$script:typeSynonyms["хранилищезначения"] = "ValueStorage" -$script:typeSynonyms["number"] = "Number" -$script:typeSynonyms["string"] = "String" -$script:typeSynonyms["boolean"] = "Boolean" -$script:typeSynonyms["date"] = "Date" -$script:typeSynonyms["datetime"] = "DateTime" -$script:typeSynonyms["valuestorage"] = "ValueStorage" -$script:typeSynonyms["bool"] = "Boolean" -# Reference synonyms -$script:typeSynonyms["справочникссылка"] = "CatalogRef" -$script:typeSynonyms["документссылка"] = "DocumentRef" -$script:typeSynonyms["перечислениессылка"] = "EnumRef" -$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:typeSynonyms["задачассылка"] = "TaskRef" -$script:typeSynonyms["определяемыйтип"] = "DefinedType" -$script:typeSynonyms["definedtype"] = "DefinedType" -$script:typeSynonyms["catalogref"] = "CatalogRef" -$script:typeSynonyms["documentref"] = "DocumentRef" -$script:typeSynonyms["enumref"] = "EnumRef" - -# ============================================================ -# Section 4: Type system -# ============================================================ - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') -} - -function Split-CamelCase { - param([string]$name) - if (-not $name) { return $name } - $result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2') - $result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2') - if ($result.Length -gt 1) { - $result = $result.Substring(0,1) + $result.Substring(1).ToLower() - } - return $result -} - -function New-Guid-String { - return [System.Guid]::NewGuid().ToString() -} - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - - # Parameterized: Number(15,2), Строка(100) - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $baseName = $Matches[1].Trim() - $params = $Matches[2] - $resolved = $script:typeSynonyms[$baseName.ToLower()] - if ($resolved) { return "$resolved($params)" } - return $typeStr - } - - # Reference: СправочникСсылка.Организации - if ($typeStr.Contains('.')) { - $dotIdx = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $dotIdx) - $suffix = $typeStr.Substring($dotIdx) - $resolved = $script:typeSynonyms[$prefix.ToLower()] - if ($resolved) { return "$resolved$suffix" } - return $typeStr - } - - # Simple - $resolved = $script:typeSynonyms[$typeStr.ToLower()] - if ($resolved) { return $resolved } - return $typeStr -} - -function Build-TypeContentXml { - param([string]$indent, [string]$typeStr) - if (-not $typeStr) { return "" } - - # Composite type: "Type1 + Type2 + Type3" - if ($typeStr.Contains(' + ')) { - $parts = $typeStr -split '\s*\+\s*' - $sb = New-Object System.Text.StringBuilder - foreach ($part in $parts) { - $inner = Build-TypeContentXml $indent $part.Trim() - if ($inner) { $sb.AppendLine($inner) | Out-Null } - } - return $sb.ToString().TrimEnd("`r","`n") - } - - $typeStr = Resolve-TypeStr $typeStr - $sb = New-Object System.Text.StringBuilder - - # Boolean - if ($typeStr -eq "Boolean") { - $sb.AppendLine("$indentxs:boolean") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # ValueStorage - if ($typeStr -eq "ValueStorage") { - $sb.AppendLine("$indentxs:base64Binary") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # String or String(N) - if ($typeStr -match '^String(\((\d+)\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "10" } - $sb.AppendLine("$indentxs:string") | Out-Null - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t$len") | Out-Null - $sb.AppendLine("$indent`tVariable") | Out-Null - $sb.AppendLine("$indent") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # Number(D,F) or Number(D,F,nonneg) - if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1]; $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - $sb.AppendLine("$indentxs:decimal") | Out-Null - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t$digits") | Out-Null - $sb.AppendLine("$indent`t$fraction") | Out-Null - $sb.AppendLine("$indent`t$sign") | Out-Null - $sb.AppendLine("$indent") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # Number without params → Number(10,0) - if ($typeStr -eq "Number") { - $sb.AppendLine("$indentxs:decimal") | Out-Null - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t10") | Out-Null - $sb.AppendLine("$indent`t0") | Out-Null - $sb.AppendLine("$indent`tAny") | Out-Null - $sb.AppendLine("$indent") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # Date / DateTime - if ($typeStr -eq "Date") { - $sb.AppendLine("$indentxs:dateTime") | Out-Null - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`tDate") | Out-Null - $sb.AppendLine("$indent") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - if ($typeStr -eq "DateTime") { - $sb.AppendLine("$indentxs:dateTime") | Out-Null - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`tDateTime") | Out-Null - $sb.AppendLine("$indent") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # DefinedType - if ($typeStr -match '^DefinedType\.(.+)$') { - $dtName = $Matches[1] - $sb.AppendLine("$indentcfg:DefinedType.$dtName") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # Reference types — use local xmlns declaration for 1C compatibility - if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { - $sb.AppendLine("$indentd5p1:$typeStr") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") - } - - # Fallback - $sb.AppendLine("$indent$typeStr") | Out-Null - return $sb.ToString().TrimEnd("`r","`n") -} - -function Build-ValueTypeXml { - param([string]$indent, [string]$typeStr) - $inner = Build-TypeContentXml "$indent`t" $typeStr - return "$indent`r`n$inner`r`n$indent" -} - -function Build-FillValueXml { - param([string]$indent, [string]$typeStr) - if (-not $typeStr) { - return "$indent" - } - $typeStr = Resolve-TypeStr $typeStr - if ($typeStr -eq "Boolean") { - return "$indentfalse" - } - if ($typeStr -match '^String') { - return "$indent" - } - if ($typeStr -match '^Number') { - return "$indent0" - } - return "$indent" -} - -function Build-MLTextXml { - param([string]$indent, [string]$tag, [string]$text) - if (-not $text) { - return "$indent<$tag/>" - } - $lines = @( - "$indent<$tag>" - "$indent`t" - "$indent`t`tru" - "$indent`t`t$(Esc-Xml $text)" - "$indent`t" - "$indent" - ) - return $lines -join "`r`n" -} - -# ============================================================ -# Section 5: DOM helpers -# ============================================================ - -$script:metaNs = "http://v8.1c.ru/8.3/MDClasses" -$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" -$script:v8Ns = "http://v8.1c.ru/8.1/data/core" - -function Import-Fragment([string]$xmlString) { - $wrapper = @" -<_W xmlns="http://v8.1c.ru/8.3/MDClasses" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:v8="http://v8.1c.ru/8.1/data/core" - xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" - xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" - xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString -"@ - $frag = New-Object System.Xml.XmlDocument - $frag.PreserveWhitespace = $true - $frag.LoadXml($wrapper) - $nodes = @() - foreach ($child in $frag.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $script:xmlDoc.ImportNode($child, $true) - } - } - return ,$nodes -} - -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - $text = $child.Value - if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($text -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - # Fallback: count depth - $depth = 0 - $current = $container - while ($current -and $current -ne $script:xmlDoc.DocumentElement) { - $depth++ - $current = $current.ParentNode - } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Remove-NodeWithWhitespace($node) { - $parent = $node.ParentNode - $prev = $node.PreviousSibling - $next = $node.NextSibling - - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($prev) | Out-Null - } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($next) | Out-Null - } - $parent.RemoveChild($node) | Out-Null -} - -function Find-ElementByName($container, [string]$elemLocalName, [string]$nameValue) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -ne $elemLocalName) { continue } - # Look for Properties/Name or just Name child - $propsEl = $null - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { - $propsEl = $gc; break - } - } - $searchIn = if ($propsEl) { $propsEl } else { $child } - foreach ($gc in $searchIn.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name" -and $gc.InnerText.Trim() -eq $nameValue) { - return $child - } - } - } - return $null -} - -function Find-LastElementOfType($container, [string]$localName) { - $last = $null - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { - $last = $child - } - } - return $last -} - -function Find-FirstElementOfType($container, [string]$localName) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { - return $child - } - } - return $null -} - -function Ensure-ChildObjectsOpen { - if ($script:childObjectsEl) { - # Check if it's self-closing (no child elements) - $hasElements = $false - foreach ($ch in $script:childObjectsEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } - } - if (-not $hasElements) { - # It's empty — we need to add whitespace for proper formatting - $indent = Get-ChildIndent $script:objElement - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") - $script:childObjectsEl.AppendChild($closeWs) | Out-Null - } - return - } - # No ChildObjects at all — create one after Properties - $indent = Get-ChildIndent $script:objElement - $coXml = "`r`n$indent`r`n$indent" - # Insert after Properties - $refNode = $null - $foundProps = $false - foreach ($child in $script:objElement.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Properties") { - $foundProps = $true - continue - } - if ($foundProps -and $child.NodeType -eq 'Element') { - $refNode = $child - break - } - } - - $coEl = $script:xmlDoc.CreateElement("ChildObjects", $script:mdNs) - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") - $coEl.AppendChild($closeWs) | Out-Null - - $wsB = $script:xmlDoc.CreateWhitespace("`r`n$indent") - if ($refNode) { - $script:objElement.InsertBefore($wsB, $refNode) | Out-Null - $script:objElement.InsertBefore($coEl, $wsB) | Out-Null - } else { - # After last child - $trailing = $script:objElement.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $script:objElement.InsertBefore($wsB, $trailing) | Out-Null - $script:objElement.InsertBefore($coEl, $trailing) | Out-Null - } else { - $script:objElement.AppendChild($wsB) | Out-Null - $script:objElement.AppendChild($coEl) | Out-Null - } - } - $script:childObjectsEl = $coEl -} - -function Collapse-ChildObjectsIfEmpty { - if (-not $script:childObjectsEl) { return } - $hasElements = $false - foreach ($ch in $script:childObjectsEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } - } - if (-not $hasElements) { - # Remove all whitespace children - while ($script:childObjectsEl.HasChildNodes) { - $script:childObjectsEl.RemoveChild($script:childObjectsEl.FirstChild) | Out-Null - } - } -} - -# ============================================================ -# Section 6: Fragment builders -# ============================================================ - -function Parse-AttributeShorthand { - param($val) - - if ($val -is [string]) { - $str = "$val" - $parsed = @{ - name = ""; type = ""; synonym = ""; comment = "" - flags = @(); fillChecking = ""; indexing = "" - after = ""; before = "" - } - # Extract positional markers: >> after Name, << before Name - if ($str -match '\s*>>\s*after\s+(\S+)\s*$') { - $parsed.after = $Matches[1] - $str = ($str -replace '\s*>>\s*after\s+\S+\s*$', '').Trim() - } elseif ($str -match '\s*<<\s*before\s+(\S+)\s*$') { - $parsed.before = $Matches[1] - $str = ($str -replace '\s*<<\s*before\s+\S+\s*$', '').Trim() - } - # Split by | for flags - $parts = $str -split '\|', 2 - $mainPart = $parts[0].Trim() - if ($parts.Count -gt 1) { - $flagStr = $parts[1].Trim() - $parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) - } - # Split by : for name and type - $colonParts = $mainPart -split ':', 2 - $parsed.name = $colonParts[0].Trim() - if ($colonParts.Count -gt 1) { - $parsed.type = $colonParts[1].Trim() - } - $parsed.synonym = Split-CamelCase $parsed.name - return $parsed - } - - # Object form - $name = "$($val.name)" - $result = @{ - name = $name - type = if ($val.type -is [array]) { ($val.type | ForEach-Object { "$_" }) -join ' + ' } elseif ($val.type) { "$($val.type)" } else { "" } - synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } - comment = if ($val.comment) { "$($val.comment)" } else { "" } - flags = @(if ($val.flags) { $val.flags } else { @() }) - fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } - indexing = if ($val.indexing) { "$($val.indexing)" } else { "" } - after = if ($val.after) { "$($val.after)" } else { "" } - before = if ($val.before) { "$($val.before)" } else { "" } - } - # Map flags to properties - if ($result.flags -contains "req" -and -not $result.fillChecking) { - $result.fillChecking = "ShowError" - } - if ($result.flags -contains "index" -and -not $result.indexing) { - $result.indexing = "Index" - } - if ($result.flags -contains "indexadditional" -and -not $result.indexing) { - $result.indexing = "IndexWithAdditionalOrder" - } - return $result -} - -function Parse-EnumValueShorthand { - param($val) - if ($val -is [string]) { - $name = "$val" - return @{ - name = $name - synonym = Split-CamelCase $name - comment = "" - after = ""; before = "" - } - } - $name = "$($val.name)" - return @{ - name = $name - synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } - comment = if ($val.comment) { "$($val.comment)" } else { "" } - after = if ($val.after) { "$($val.after)" } else { "" } - before = if ($val.before) { "$($val.before)" } else { "" } - } -} - -# Determine attribute context from object type -function Get-AttributeContext { - switch ($script:objType) { - "Catalog" { return "catalog" } - "Document" { return "document" } - { $_ -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") } { return "register" } - { $_ -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport") } { return "processor" } - default { return "object" } - } -} - -$script:reservedAttrNames = @{ - "Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование" - "Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец" - "IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных" - "Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность" - "Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый" - "Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача" - "Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс" - "ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого" - "CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись" - "Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый" -} - -function Build-AttributeFragment { - param($parsed, [string]$context, [string]$indent) - - if (-not $context) { $context = Get-AttributeContext } - - # Check reserved attribute names - $attrName = $parsed.name - if ($script:reservedAttrNames.ContainsKey($attrName)) { - Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." - } - $ruValues = $script:reservedAttrNames.Values - if ($ruValues -contains $attrName) { - Write-Warning "Attribute '$attrName' conflicts with a standard attribute name (Russian). This may cause errors when loading into 1C." - } - - $uuid = New-Guid-String - $sb = New-Object System.Text.StringBuilder - - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - # Type - $typeStr = $parsed.type - if ($typeStr) { - $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null - } else { - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`txs:string") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - # FillFromFillingValue/FillValue — not for register, tabular (config TS), or processor (non-stored top-level) - if ($context -notin @("register", "tabular", "processor")) { - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine($(Build-FillValueXml "$indent`t`t" $typeStr)) | Out-Null - } - - # FillChecking - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - if ($parsed.fillChecking) { $fillChecking = Normalize-EnumValue "FillChecking" $parsed.fillChecking } - $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null - - $sb.AppendLine("$indent`t`tItems") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - - # Use — catalog only - if ($context -eq "catalog") { - $sb.AppendLine("$indent`t`tForItem") | Out-Null - } - - # Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular) - if ($context -notin @("processor", "processor-tabular")) { - $indexing = "DontIndex" - if ($parsed.flags -contains "index") { $indexing = "Index" } - if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } - if ($parsed.indexing) { $indexing = Normalize-EnumValue "Indexing" $parsed.indexing } - $sb.AppendLine("$indent`t`t$indexing") | Out-Null - - $sb.AppendLine("$indent`t`tUse") | Out-Null - $sb.AppendLine("$indent`t`tUse") | Out-Null - } - - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-TabularSectionFragment { - param($tsDef, [string]$indent) - - $tsName = "$($tsDef.name)" - $tsSynonym = if ($tsDef.synonym) { "$($tsDef.synonym)" } else { Split-CamelCase $tsName } - $uuid = New-Guid-String - $objType = $script:objType - $objName = $script:objName - - $typePrefix = "${objType}TabularSection" - $rowPrefix = "${objType}TabularSectionRow" - - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("$indent") | Out-Null - - # InternalInfo - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null - $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null - $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - - # Properties - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $tsName)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $tsSynonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tDontCheck") | Out-Null - - # StandardAttributes (LineNumber) - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`tDontCheck") | Out-Null - $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null - $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - # Use — catalog only - if ($objType -eq "Catalog") { - $sb.AppendLine("$indent`t`tForItem") | Out-Null - } - - $sb.AppendLine("$indent`t") | Out-Null - - # ChildObjects with attrs - $columns = @() - if ($tsDef.attrs) { $columns = @($tsDef.attrs) } - elseif ($tsDef.attributes) { $columns = @($tsDef.attributes) } - elseif ($tsDef.реквизиты) { $columns = @($tsDef.реквизиты) } - - $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } - if ($columns.Count -gt 0) { - $sb.AppendLine("$indent`t") | Out-Null - foreach ($col in $columns) { - $colParsed = Parse-AttributeShorthand $col - $sb.AppendLine($(Build-AttributeFragment $colParsed $tsAttrContext "$indent`t`t")) | Out-Null - } - $sb.AppendLine("$indent`t") | Out-Null - } else { - $sb.AppendLine("$indent`t") | Out-Null - } - - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-DimensionFragment { - param($parsed, [string]$registerType, [string]$indent) - - if (-not $registerType) { $registerType = $script:objType } - $uuid = New-Guid-String - $sb = New-Object System.Text.StringBuilder - - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - $typeStr = $parsed.type - if ($typeStr) { - $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null - } else { - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`txs:string") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - # InformationRegister: FillFromFillingValue, FillValue - if ($registerType -eq "InformationRegister") { - $fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" } - $sb.AppendLine("$indent`t`t$fillFrom") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null - - $sb.AppendLine("$indent`t`tItems") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - - # InformationRegister: Master, MainFilter, DenyIncompleteValues - if ($registerType -eq "InformationRegister") { - $master = if ($parsed.flags -contains "master") { "true" } else { "false" } - $mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" } - $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } - $sb.AppendLine("$indent`t`t$master") | Out-Null - $sb.AppendLine("$indent`t`t$mainFilter") | Out-Null - $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null - } - - # AccumulationRegister: DenyIncompleteValues - if ($registerType -eq "AccumulationRegister") { - $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } - $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null - } - - $indexing = "DontIndex" - if ($parsed.flags -contains "index") { $indexing = "Index" } - $sb.AppendLine("$indent`t`t$indexing") | Out-Null - - $sb.AppendLine("$indent`t`tUse") | Out-Null - - # AccumulationRegister: UseInTotals - if ($registerType -eq "AccumulationRegister") { - $useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" } - $sb.AppendLine("$indent`t`t$useInTotals") | Out-Null - } - - # InformationRegister: DataHistory - if ($registerType -eq "InformationRegister") { - $sb.AppendLine("$indent`t`tUse") | Out-Null - } - - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-ResourceFragment { - param($parsed, [string]$registerType, [string]$indent) - - if (-not $registerType) { $registerType = $script:objType } - $uuid = New-Guid-String - $sb = New-Object System.Text.StringBuilder - - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - $typeStr = $parsed.type - if ($typeStr) { - $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null - } else { - # Default: Number(15,2) - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`txs:decimal") | Out-Null - $sb.AppendLine("$indent`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t`t`t15") | Out-Null - $sb.AppendLine("$indent`t`t`t`t2") | Out-Null - $sb.AppendLine("$indent`t`t`t`tAny") | Out-Null - $sb.AppendLine("$indent`t`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - - # InformationRegister: FillFromFillingValue, FillValue - if ($registerType -eq "InformationRegister") { - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - - $fillChecking = "DontCheck" - if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } - $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null - - $sb.AppendLine("$indent`t`tItems") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - - # InformationRegister: Indexing, FullTextSearch, DataHistory - if ($registerType -eq "InformationRegister") { - $sb.AppendLine("$indent`t`tDontIndex") | Out-Null - $sb.AppendLine("$indent`t`tUse") | Out-Null - $sb.AppendLine("$indent`t`tUse") | Out-Null - } - - # AccumulationRegister: FullTextSearch - if ($registerType -eq "AccumulationRegister") { - $sb.AppendLine("$indent`t`tUse") | Out-Null - } - - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-EnumValueFragment { - param($parsed, [string]$indent) - - $uuid = New-Guid-String - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-ColumnFragment { - param($colDef, [string]$indent) - - $uuid = New-Guid-String - $name = "" - $synonym = "" - $indexing = "DontIndex" - $references = @() - - if ($colDef -is [string]) { - $name = "$colDef" - $synonym = Split-CamelCase $name - } else { - $name = "$($colDef.name)" - $synonym = if ($colDef.synonym) { "$($colDef.synonym)" } else { Split-CamelCase $name } - if ($colDef.indexing) { $indexing = "$($colDef.indexing)" } - if ($colDef.references) { $references = @($colDef.references) } - } - - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("$indent") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t$indexing") | Out-Null - if ($references.Count -gt 0) { - $sb.AppendLine("$indent`t`t") | Out-Null - foreach ($ref in $references) { - $sb.AppendLine("$indent`t`t`t$ref") | Out-Null - } - $sb.AppendLine("$indent`t`t") | Out-Null - } else { - $sb.AppendLine("$indent`t`t") | Out-Null - } - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -function Build-SimpleChildFragment { - param([string]$tagName, [string]$name, [string]$indent) - # For Form, Template, Command — just a name wrapper - $uuid = New-Guid-String - $synonym = Split-CamelCase $name - $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("$indent<$tagName uuid=`"$uuid`">") | Out-Null - $sb.AppendLine("$indent`t") | Out-Null - $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null - $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - # Forms get additional properties - if ($tagName -eq "Form") { - $sb.AppendLine("$indent`t`tOrdinary") | Out-Null - $sb.AppendLine("$indent`t`tfalse") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - if ($tagName -eq "Template") { - $sb.AppendLine("$indent`t`tSpreadsheetDocument") | Out-Null - } - if ($tagName -eq "Command") { - $sb.AppendLine("$indent`t`tFormNavigationPanelGoTo") | Out-Null - $sb.AppendLine("$indent`t`tAuto") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - $sb.AppendLine("$indent`t`t") | Out-Null - } - $sb.AppendLine("$indent`t") | Out-Null - $sb.Append("$indent") | Out-Null - return $sb.ToString() -} - -# ============================================================ -# Section 7: Name uniqueness check -# ============================================================ - -function Get-AllChildNames { - $names = @{} - if (-not $script:childObjectsEl) { return $names } - foreach ($child in $script:childObjectsEl.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $propsEl = $null - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { - $propsEl = $gc; break - } - } - if (-not $propsEl) { continue } - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { - $n = $gc.InnerText.Trim() - if ($n) { $names[$n] = $child.LocalName } - break - } - } - # Also check ChildObjects of TabularSections for nested names - if ($child.LocalName -eq "TabularSection") { - foreach ($tsCh in $child.ChildNodes) { - if ($tsCh.NodeType -eq 'Element' -and $tsCh.LocalName -eq "ChildObjects") { - foreach ($tsChild in $tsCh.ChildNodes) { - if ($tsChild.NodeType -ne 'Element') { continue } - $tsProps = $null - foreach ($gc in $tsChild.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { - $tsProps = $gc; break - } - } - if ($tsProps) { - foreach ($gc in $tsProps.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { - # TS attr names don't conflict with top-level - break - } - } - } - } - } - } - } - } - return $names -} - -# ============================================================ -# Section 8: Context and allowed child types -# ============================================================ - -$script:validChildTypes = @{ - "Catalog" = @("attributes","tabularSections","forms","templates","commands") - "Document" = @("attributes","tabularSections","forms","templates","commands") - "ExchangePlan" = @("attributes","tabularSections","forms","templates","commands") - "ChartOfAccounts" = @("attributes","tabularSections","forms","templates","commands") - "ChartOfCharacteristicTypes" = @("attributes","tabularSections","forms","templates","commands") - "ChartOfCalculationTypes" = @("attributes","tabularSections","forms","templates","commands") - "BusinessProcess" = @("attributes","tabularSections","forms","templates","commands") - "Task" = @("attributes","tabularSections","forms","templates","commands") - "Report" = @("attributes","tabularSections","forms","templates","commands") - "DataProcessor" = @("attributes","tabularSections","forms","templates","commands") - "Enum" = @("enumValues","forms","templates","commands") - "InformationRegister" = @("dimensions","resources","attributes","forms","templates","commands") - "AccumulationRegister" = @("dimensions","resources","attributes","forms","templates","commands") - "AccountingRegister" = @("dimensions","resources","attributes","forms","templates","commands") - "CalculationRegister" = @("dimensions","resources","attributes","forms","templates","commands") - "DocumentJournal" = @("columns","forms","templates","commands") - "Constant" = @("forms") -} - -# Canonical child order in ChildObjects -$script:childOrder = @( - "Resource", "Dimension", "Attribute", "TabularSection", - "AccountingFlag", "ExtDimensionAccountingFlag", - "EnumValue", "Column", "AddressingAttribute", "Recalculation", - "Form", "Template", "Command" -) - -# Map from DSL child type to XML element name -$script:childTypeToXmlTag = @{ - "attributes" = "Attribute" - "tabularSections" = "TabularSection" - "dimensions" = "Dimension" - "resources" = "Resource" - "enumValues" = "EnumValue" - "columns" = "Column" - "forms" = "Form" - "templates" = "Template" - "commands" = "Command" -} - -# ============================================================ -# Section 9: DSL key normalization -# ============================================================ - -function Resolve-OperationKey([string]$key) { - $k = $key.ToLower().Trim() - if ($script:operationSynonyms.ContainsKey($k)) { - return $script:operationSynonyms[$k] - } - return $null -} - -function Resolve-ChildTypeKey([string]$key) { - $k = $key.ToLower().Trim() - if ($script:childTypeSynonyms.ContainsKey($k)) { - return $script:childTypeSynonyms[$k] - } - return $null -} - -# ============================================================ -# Section 9.5: Inline mode converter -# ============================================================ - -function Split-ByCommaOutsideParens([string]$str) { - $result = @() - $depth = 0 - $current = "" - foreach ($ch in $str.ToCharArray()) { - if ($ch -eq '(') { $depth++ } - elseif ($ch -eq ')') { $depth-- } - if ($ch -eq ',' -and $depth -eq 0) { - $result += $current - $current = "" - } else { - $current += $ch - } - } - if ($current) { $result += $current } - return ,$result -} - -function Convert-InlineToDefinition([string]$operation, [string]$value) { - # Parse operation: "add-attribute" → ("add", "attribute") - $opParts = $operation -split '-', 2 - $op = $opParts[0] # add, remove, modify, set - $target = $opParts[1] # attribute, ts, owner, owners, property, etc. - - # Complex property targets - $complexTargetMap = @{ - "owner" = "Owners"; "owners" = "Owners" - "registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords" - "basedOn" = "BasedOn" - "inputByString" = "InputByString" - } - - if ($complexTargetMap.ContainsKey($target)) { - $propName = $complexTargetMap[$target] - $values = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - # For InputByString, auto-prefix with MetaType.Name. - if ($propName -eq "InputByString") { - $prefix = "$($script:objType).$($script:objName)." - $values = @($values | ForEach-Object { - if ($_ -notmatch '\.') { - "$prefix$_" - } elseif ($_ -notmatch '^(Catalog|Document|InformationRegister|AccumulationRegister|AccountingRegister|CalculationRegister|ChartOfCharacteristicTypes|ChartOfCalculationTypes|ChartOfAccounts|ExchangePlan|BusinessProcess|Task|Enum|Report|DataProcessor)\.') { - "$prefix$_" - } else { $_ } - }) - } - $def = New-Object PSCustomObject - $complexAction = if ($op -eq "set") { "set" } else { $op } - $def | Add-Member -NotePropertyName "_complex" -NotePropertyValue @( - @{ action = $complexAction; property = $propName; values = $values } - ) - return $def - } - - # TS attribute operations: dot notation "TSName.AttrDef" - if ($target -eq "ts-attribute") { - $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - # Group by TS name - $tsGroups = [ordered]@{} - foreach ($item in $items) { - $dotIdx = $item.IndexOf('.') - if ($dotIdx -le 0) { - Warn "Invalid ts-attribute format (expected TSName.AttrDef): $item" - continue - } - $tsName = $item.Substring(0, $dotIdx).Trim() - $rest = $item.Substring($dotIdx + 1).Trim() - if (-not $tsGroups.Contains($tsName)) { - $tsGroups[$tsName] = @() - } - $tsGroups[$tsName] += $rest - } - - # Build: { modify: { tabularSections: { TSName: { add/remove/modify: ... } } } } - $tsModObj = New-Object PSCustomObject - foreach ($tsName in $tsGroups.Keys) { - $tsChanges = New-Object PSCustomObject - switch ($op) { - "add" { - $tsChanges | Add-Member -NotePropertyName "add" -NotePropertyValue $tsGroups[$tsName] - } - "remove" { - $tsChanges | Add-Member -NotePropertyName "remove" -NotePropertyValue $tsGroups[$tsName] - } - "modify" { - $attrModObj = New-Object PSCustomObject - foreach ($elemDef in $tsGroups[$tsName]) { - $colonIdx = $elemDef.IndexOf(':') - if ($colonIdx -le 0) { - Warn "Invalid modify format (expected Name: key=val): $elemDef" - continue - } - $elemName = $elemDef.Substring(0, $colonIdx).Trim() - $changesPart = $elemDef.Substring($colonIdx + 1).Trim() - $changesObj = New-Object PSCustomObject - $changePairs = Split-ByCommaOutsideParens $changesPart - foreach ($cp in $changePairs) { - $cp = $cp.Trim() - $eqIdx = $cp.IndexOf('=') - if ($eqIdx -gt 0) { - $ck = $cp.Substring(0, $eqIdx).Trim() - $cv = $cp.Substring($eqIdx + 1).Trim() - $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv - } - } - $attrModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj - } - $tsChanges | Add-Member -NotePropertyName "modify" -NotePropertyValue $attrModObj - } - } - $tsModObj | Add-Member -NotePropertyName $tsName -NotePropertyValue $tsChanges - } - $def = New-Object PSCustomObject - $modifyObj = New-Object PSCustomObject - $modifyObj | Add-Member -NotePropertyName "tabularSections" -NotePropertyValue $tsModObj - $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj - return $def - } - - # Target → JSON DSL child type - $targetMap = @{ - "attribute" = "attributes" - "ts" = "tabularSections" - "dimension" = "dimensions" - "resource" = "resources" - "enumValue" = "enumValues" - "column" = "columns" - "form" = "forms" - "template" = "templates" - "command" = "commands" - "property" = "properties" - } - - $childType = $targetMap[$target] - if (-not $childType) { - Write-Error "Unknown inline target: $target" - exit 1 - } - - $def = New-Object PSCustomObject - - switch ($op) { - "add" { - $items = @() - if ($childType -eq "tabularSections") { - # TS format: "TSName: attr1_shorthand, attr2_shorthand, ..." - $tsValues = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - foreach ($tsVal in $tsValues) { - $colonIdx = $tsVal.IndexOf(':') - if ($colonIdx -gt 0) { - $tsName = $tsVal.Substring(0, $colonIdx).Trim() - $attrsPart = $tsVal.Substring($colonIdx + 1).Trim() - # Split attrs by comma (paren-aware), reassemble if part doesn't start with "Name:" - $rawParts = Split-ByCommaOutsideParens $attrsPart - $attrStrs = @() - $current = "" - foreach ($rp in $rawParts) { - $rp = $rp.Trim() - if ($current -and $rp -match '^[А-Яа-яЁёA-Za-z_]\w*\s*:') { - $attrStrs += $current - $current = $rp - } elseif ($current) { - $current += ", $rp" - } else { - $current = $rp - } - } - if ($current) { $attrStrs += $current } - $items += [PSCustomObject]@{ name = $tsName; attrs = $attrStrs } - } else { - # Just a name, no attrs - $items += $tsVal - } - } - } else { - # Batch split by ;; - $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } - $addObj = New-Object PSCustomObject - $addObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items - $def | Add-Member -NotePropertyName "add" -NotePropertyValue $addObj - } - "remove" { - $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $removeObj = New-Object PSCustomObject - $removeObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items - $def | Add-Member -NotePropertyName "remove" -NotePropertyValue $removeObj - } - "modify" { - if ($childType -eq "properties") { - # "CodeLength=11 ;; DescriptionLength=150" - $kvPairs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $propsObj = New-Object PSCustomObject - foreach ($kv in $kvPairs) { - $eqIdx = $kv.IndexOf('=') - if ($eqIdx -gt 0) { - $k = $kv.Substring(0, $eqIdx).Trim() - $v = $kv.Substring($eqIdx + 1).Trim() - $propsObj | Add-Member -NotePropertyName $k -NotePropertyValue $v - } else { - Warn "Invalid property format (expected Key=Value): $kv" - } - } - $modifyObj = New-Object PSCustomObject - $modifyObj | Add-Member -NotePropertyName "properties" -NotePropertyValue $propsObj - $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj - } else { - # "ElementName: key=val, key=val ;; Element2: key=val" - $elemDefs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $childModObj = New-Object PSCustomObject - foreach ($elemDef in $elemDefs) { - $colonIdx = $elemDef.IndexOf(':') - if ($colonIdx -le 0) { - Warn "Invalid modify format (expected Name: key=val): $elemDef" - continue - } - $elemName = $elemDef.Substring(0, $colonIdx).Trim() - $changesPart = $elemDef.Substring($colonIdx + 1).Trim() - $changesObj = New-Object PSCustomObject - $changePairs = Split-ByCommaOutsideParens $changesPart - foreach ($cp in $changePairs) { - $cp = $cp.Trim() - $eqIdx = $cp.IndexOf('=') - if ($eqIdx -gt 0) { - $ck = $cp.Substring(0, $eqIdx).Trim() - $cv = $cp.Substring($eqIdx + 1).Trim() - $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv - } - } - $childModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj - } - $modifyObj = New-Object PSCustomObject - $modifyObj | Add-Member -NotePropertyName $childType -NotePropertyValue $childModObj - $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj - } - } - } - - return $def -} - -# ============================================================ -# Section 10: ADD operations -# ============================================================ - -function Find-InsertionPoint { - param([string]$xmlTag, $parsed) - # Returns $refNode for Insert-BeforeElement (null = append) - - if (-not $script:childObjectsEl) { return $null } - - # Positional: after/before - if ($parsed.after) { - $afterEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.after - if ($afterEl) { - # Insert after = insert before the next element sibling - $next = $afterEl.NextSibling - while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } - if ($next -and $next.LocalName -eq $xmlTag) { return $next } - return $null # append - } else { - Warn "after='$($parsed.after)': element '$($parsed.after)' not found in $xmlTag, appending" - } - } - if ($parsed.before) { - $beforeEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.before - if ($beforeEl) { return $beforeEl } - Warn "before='$($parsed.before)': element '$($parsed.before)' not found in $xmlTag, appending" - } - - # Default: after last element of this type, or in canonical position - $lastOfType = Find-LastElementOfType $script:childObjectsEl $xmlTag - if ($lastOfType) { - $next = $lastOfType.NextSibling - while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } - return $next # null means append (which is correct: after last of type) - } - - # No elements of this type yet — find canonical position - $tagIdx = [array]::IndexOf($script:childOrder, $xmlTag) - if ($tagIdx -lt 0) { return $null } - - # Find first element of any type that comes AFTER in the canonical order - for ($i = $tagIdx + 1; $i -lt $script:childOrder.Count; $i++) { - $nextTag = $script:childOrder[$i] - $firstOfNext = Find-FirstElementOfType $script:childObjectsEl $nextTag - if ($firstOfNext) { return $firstOfNext } - } - - return $null # append at end -} - -function Process-Add($addDef) { - $addDef.PSObject.Properties | ForEach-Object { - $rawKey = $_.Name - $items = $_.Value - $childType = Resolve-ChildTypeKey $rawKey - - if (-not $childType) { - Warn "Unknown add child type: $rawKey" - return - } - - # Validate allowed - $allowed = $script:validChildTypes[$script:objType] - if ($allowed -and $childType -notin $allowed) { - Warn "$childType not allowed for $($script:objType), skipping" - return - } - - $xmlTag = $script:childTypeToXmlTag[$childType] - if (-not $xmlTag) { - Warn "No XML tag mapping for $childType" - return - } - - Ensure-ChildObjectsOpen - $indent = Get-ChildIndent $script:childObjectsEl - $existingNames = Get-AllChildNames - - switch ($childType) { - "attributes" { - foreach ($item in $items) { - $parsed = Parse-AttributeShorthand $item - if ($existingNames.ContainsKey($parsed.name)) { - Warn "Attribute '$($parsed.name)' already exists, skipping" - continue - } - $context = Get-AttributeContext - $fragmentXml = Build-AttributeFragment $parsed $context $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "Attribute" $parsed - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added attribute: $($parsed.name)" - $script:addCount++ - $existingNames[$parsed.name] = "Attribute" - } - } - "tabularSections" { - foreach ($item in $items) { - $tsName = if ($item -is [string]) { "$item" } else { "$($item.name)" } - if ($existingNames.ContainsKey($tsName)) { - Warn "TabularSection '$tsName' already exists, skipping" - continue - } - $tsDef = if ($item -is [string]) { @{ name = $item } } else { $item } - $fragmentXml = Build-TabularSectionFragment $tsDef $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "TabularSection" @{ after = ""; before = "" } - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added tabular section: $tsName" - $script:addCount++ - $existingNames[$tsName] = "TabularSection" - } - } - "dimensions" { - foreach ($item in $items) { - $parsed = Parse-AttributeShorthand $item - if ($existingNames.ContainsKey($parsed.name)) { - Warn "Dimension '$($parsed.name)' already exists, skipping" - continue - } - $fragmentXml = Build-DimensionFragment $parsed $script:objType $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "Dimension" $parsed - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added dimension: $($parsed.name)" - $script:addCount++ - $existingNames[$parsed.name] = "Dimension" - } - } - "resources" { - foreach ($item in $items) { - $parsed = Parse-AttributeShorthand $item - if ($existingNames.ContainsKey($parsed.name)) { - Warn "Resource '$($parsed.name)' already exists, skipping" - continue - } - $fragmentXml = Build-ResourceFragment $parsed $script:objType $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "Resource" $parsed - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added resource: $($parsed.name)" - $script:addCount++ - $existingNames[$parsed.name] = "Resource" - } - } - "enumValues" { - foreach ($item in $items) { - $parsed = Parse-EnumValueShorthand $item - if ($existingNames.ContainsKey($parsed.name)) { - Warn "EnumValue '$($parsed.name)' already exists, skipping" - continue - } - $fragmentXml = Build-EnumValueFragment $parsed $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "EnumValue" $parsed - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added enum value: $($parsed.name)" - $script:addCount++ - $existingNames[$parsed.name] = "EnumValue" - } - } - "columns" { - foreach ($item in $items) { - $colName = if ($item -is [string]) { "$item" } else { "$($item.name)" } - if ($existingNames.ContainsKey($colName)) { - Warn "Column '$colName' already exists, skipping" - continue - } - $fragmentXml = Build-ColumnFragment $item $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint "Column" @{ after = ""; before = "" } - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added column: $colName" - $script:addCount++ - $existingNames[$colName] = "Column" - } - } - { $_ -in @("forms","templates","commands") } { - $tagMap = @{ "forms" = "Form"; "templates" = "Template"; "commands" = "Command" } - $tag = $tagMap[$childType] - foreach ($item in $items) { - $itemName = if ($item -is [string]) { "$item" } else { "$($item.name)" } - if ($existingNames.ContainsKey($itemName)) { - Warn "$tag '$itemName' already exists, skipping" - continue - } - $fragmentXml = Build-SimpleChildFragment $tag $itemName $indent - $nodes = Import-Fragment $fragmentXml - $refNode = Find-InsertionPoint $tag @{ after = ""; before = "" } - foreach ($node in $nodes) { - Insert-BeforeElement $script:childObjectsEl $node $refNode $indent - } - Info "Added $($tag.ToLower()): $itemName" - $script:addCount++ - $existingNames[$itemName] = $tag - } - } - } - } -} - -# ============================================================ -# Section 11: REMOVE operations -# ============================================================ - -function Process-Remove($removeDef) { - $removeDef.PSObject.Properties | ForEach-Object { - $rawKey = $_.Name - $names = $_.Value - $childType = Resolve-ChildTypeKey $rawKey - - if (-not $childType) { - Warn "Unknown remove child type: $rawKey" - return - } - if ($childType -eq "properties") { - Warn "Cannot remove properties — use modify instead" - return - } - - $xmlTag = $script:childTypeToXmlTag[$childType] - if (-not $xmlTag -or -not $script:childObjectsEl) { - Warn "No ChildObjects or unknown tag for $childType" - return - } - - foreach ($name in $names) { - $nameStr = "$name" - $el = Find-ElementByName $script:childObjectsEl $xmlTag $nameStr - if (-not $el) { - Warn "$xmlTag '$nameStr' not found, skipping remove" - continue - } - Remove-NodeWithWhitespace $el - Info "Removed $($xmlTag.ToLower()): $nameStr" - $script:removeCount++ - } - } - - # Collapse if empty - Collapse-ChildObjectsIfEmpty -} - -# ============================================================ -# Section 12: MODIFY operations -# ============================================================ - -function Modify-Properties($propsDef) { - $propsDef.PSObject.Properties | ForEach-Object { - $propName = $_.Name - $propValue = $_.Value - - # Find the property element in Properties - $propEl = $null - foreach ($child in $script:propertiesEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { - $propEl = $child - break - } - } - - if (-not $propEl) { - Warn "Property '$propName' not found in Properties" - return - } - - # Complex property: Owners, RegisterRecords, BasedOn, InputByString - if ($script:complexPropertyMap.ContainsKey($propName)) { - $valuesList = @() - if ($propValue -is [array]) { - $valuesList = @($propValue | ForEach-Object { "$_" }) - } else { - $valuesList = @("$propValue" -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } - Set-ComplexProperty $propName $valuesList - return - } - - # Handle boolean values - $valueStr = "$propValue" - if ($propValue -is [bool]) { - $valueStr = if ($propValue) { "true" } else { "false" } - } - - $propEl.InnerText = $valueStr - Info "Modified property: $propName = $valueStr" - $script:modifyCount++ - } -} - -function Modify-ChildElements($modifyDef, [string]$childType) { - $xmlTag = $script:childTypeToXmlTag[$childType] - if (-not $xmlTag -or -not $script:childObjectsEl) { - Warn "No ChildObjects or unknown tag for $childType" - return - } - - $modifyDef.PSObject.Properties | ForEach-Object { - $elemName = $_.Name - $changes = $_.Value - - $el = Find-ElementByName $script:childObjectsEl $xmlTag $elemName - if (-not $el) { - Warn "$xmlTag '$elemName' not found for modify" - return - } - - # Find Properties inside the element - $propsEl = $null - foreach ($gc in $el.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { - $propsEl = $gc; break - } - } - if (-not $propsEl) { - Warn "$xmlTag '$elemName': no Properties element found" - return - } - - $changes.PSObject.Properties | ForEach-Object { - $changeProp = $_.Name - $changeValue = $_.Value - - # TS child attribute operations (add/remove/modify attrs inside a TabularSection) - if ($xmlTag -eq "TabularSection" -and $changeProp -in @("add","remove","modify")) { - # Find ChildObjects inside this TS element - $tsChildObjEl = $null - foreach ($gc in $el.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "ChildObjects") { - $tsChildObjEl = $gc; break - } - } - - switch ($changeProp) { - "add" { - if (-not $tsChildObjEl) { - Warn "TS '$elemName' has no ChildObjects element, cannot add attributes" - return - } - # Ensure ChildObjects is open (not self-closing empty) - $hasTsChildElements = $false - foreach ($ch in $tsChildObjEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $hasTsChildElements = $true; break } - } - if (-not $hasTsChildElements) { - $tsCoIndent = Get-ChildIndent $el - $tsCloseWs = $script:xmlDoc.CreateWhitespace("`r`n$tsCoIndent") - $tsChildObjEl.AppendChild($tsCloseWs) | Out-Null - } - foreach ($attrDef in @($changeValue)) { - $parsed = Parse-AttributeShorthand $attrDef - $existing = Find-ElementByName $tsChildObjEl "Attribute" $parsed.name - if ($existing) { - Warn "Attribute '$($parsed.name)' already exists in TS '$elemName', skipping" - continue - } - $tsAttrIndent = Get-ChildIndent $tsChildObjEl - $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } - $fragmentXml = Build-AttributeFragment $parsed $tsAttrContext $tsAttrIndent - $nodes = Import-Fragment $fragmentXml - $savedCO = $script:childObjectsEl - $script:childObjectsEl = $tsChildObjEl - $refNode = Find-InsertionPoint "Attribute" $parsed - $script:childObjectsEl = $savedCO - foreach ($node in $nodes) { - Insert-BeforeElement $tsChildObjEl $node $refNode $tsAttrIndent - } - Info "Added attribute to TS '$elemName': $($parsed.name)" - $script:addCount++ - } - } - "remove" { - if (-not $tsChildObjEl) { - Warn "TS '$elemName' has no ChildObjects, cannot remove attributes" - return - } - foreach ($attrName in @($changeValue)) { - $attrEl = Find-ElementByName $tsChildObjEl "Attribute" "$attrName" - if (-not $attrEl) { - Warn "Attribute '$attrName' not found in TS '$elemName', skipping" - continue - } - Remove-NodeWithWhitespace $attrEl - Info "Removed attribute from TS '$elemName': $attrName" - $script:removeCount++ - } - } - "modify" { - if (-not $tsChildObjEl) { - Warn "TS '$elemName' has no ChildObjects, cannot modify attributes" - return - } - # Temporarily swap childObjectsEl and recurse - $savedChildObjEl = $script:childObjectsEl - $script:childObjectsEl = $tsChildObjEl - Modify-ChildElements $changeValue "attributes" - $script:childObjectsEl = $savedChildObjEl - } - } - return # Skip normal property modification - } - - switch ($changeProp) { - "name" { - # Rename - $nameEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { - $nameEl = $gc; break - } - } - if ($nameEl) { - $oldName = $nameEl.InnerText.Trim() - $newName = "$changeValue" - $nameEl.InnerText = $newName - - # Update Synonym if it was auto-generated (matches old CamelCase split) - $oldSynonym = Split-CamelCase $oldName - $synEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { - $synEl = $gc; break - } - } - if ($synEl) { - # Check if current synonym matches auto-generated from old name - $currentSyn = "" - foreach ($item in $synEl.ChildNodes) { - if ($item.NodeType -eq 'Element' -and $item.LocalName -eq "item") { - foreach ($gc in $item.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "content") { - $currentSyn = $gc.InnerText.Trim() - } - } - } - } - if ($currentSyn -eq $oldSynonym -or -not $currentSyn) { - $newSynonym = Split-CamelCase $newName - $synXml = Build-MLTextXml (Get-ChildIndent $propsEl) "Synonym" $newSynonym - $newSynNodes = Import-Fragment $synXml - if ($newSynNodes.Count -gt 0) { - $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null - Remove-NodeWithWhitespace $synEl - } - } - } - - Info "Renamed ${xmlTag}: $oldName -> $newName" - $script:modifyCount++ - } - } - "type" { - # Change type - $typeEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Type") { - $typeEl = $gc; break - } - } - $newTypeStr = "$changeValue" - $typeIndent = Get-ChildIndent $propsEl - $newTypeXml = Build-ValueTypeXml $typeIndent $newTypeStr - - $newTypeNodes = Import-Fragment $newTypeXml - if ($typeEl -and $newTypeNodes.Count -gt 0) { - $propsEl.InsertAfter($newTypeNodes[0], $typeEl) | Out-Null - Remove-NodeWithWhitespace $typeEl - } elseif ($newTypeNodes.Count -gt 0) { - # No existing Type — insert after Comment - $commentEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Comment") { - $commentEl = $gc; break - } - } - if ($commentEl) { - Insert-BeforeElement $propsEl $newTypeNodes[0] $commentEl.NextSibling $typeIndent - } - } - - # Also update FillValue if present - $fillValEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "FillValue") { - $fillValEl = $gc; break - } - } - if ($fillValEl) { - $fillIndent = Get-ChildIndent $propsEl - $newFillXml = Build-FillValueXml $fillIndent $newTypeStr - $newFillNodes = Import-Fragment $newFillXml - if ($newFillNodes.Count -gt 0) { - $propsEl.InsertAfter($newFillNodes[0], $fillValEl) | Out-Null - Remove-NodeWithWhitespace $fillValEl - } - } - - Info "Changed type of $xmlTag '$elemName': $newTypeStr" - $script:modifyCount++ - } - "synonym" { - $synEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { - $synEl = $gc; break - } - } - $synIndent = Get-ChildIndent $propsEl - $newSynXml = Build-MLTextXml $synIndent "Synonym" "$changeValue" - $newSynNodes = Import-Fragment $newSynXml - if ($synEl -and $newSynNodes.Count -gt 0) { - $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null - Remove-NodeWithWhitespace $synEl - } - Info "Changed synonym of $xmlTag '$elemName': $changeValue" - $script:modifyCount++ - } - default { - # Scalar property change (Indexing, FillChecking, Use, etc.) - $scalarEl = $null - foreach ($gc in $propsEl.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $changeProp) { - $scalarEl = $gc; break - } - } - if ($scalarEl) { - $valueStr = "$changeValue" - if ($changeValue -is [bool]) { - $valueStr = if ($changeValue) { "true" } else { "false" } - } else { - $valueStr = Normalize-EnumValue $changeProp $valueStr - } - $scalarEl.InnerText = $valueStr - Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr" - $script:modifyCount++ - } else { - Warn "$xmlTag '$elemName': property '$changeProp' not found" - } - } - } - } - } -} - -function Process-Modify($modifyDef) { - $modifyDef.PSObject.Properties | ForEach-Object { - $rawKey = $_.Name - $value = $_.Value - $childType = Resolve-ChildTypeKey $rawKey - - if (-not $childType) { - Warn "Unknown modify child type: $rawKey" - return - } - - if ($childType -eq "properties") { - Modify-Properties $value - } else { - Modify-ChildElements $value $childType - } - } -} - -# ============================================================ -# Section 12.5: Complex property helpers -# ============================================================ - -$script:complexPropertyMap = @{ - "Owners" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } - "RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } - "BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } - "InputByString" = @{ tag = "xr:Field"; attr = $null } -} - -function Find-PropertyElement([string]$propName) { - foreach ($child in $script:propertiesEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { - return $child - } - } - return $null -} - -function Get-ComplexPropertyValues([System.Xml.XmlElement]$propEl) { - $values = @() - foreach ($child in $propEl.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $values += $child.InnerText.Trim() - } - } - return $values -} - -function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) { - $mapEntry = $script:complexPropertyMap[$propertyName] - if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } - - $propEl = Find-PropertyElement $propertyName - if (-not $propEl) { - Warn "Property element '$propertyName' not found in Properties" - return - } - - # Get existing values to check duplicates - $existing = Get-ComplexPropertyValues $propEl - - $indent = Get-ChildIndent $script:propertiesEl - $childIndent = "$indent`t" - - # Check if element is self-closing (empty) - $isEmpty = $true - foreach ($ch in $propEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $isEmpty = $false; break } - } - - # If self-closing / empty, add closing whitespace - if ($isEmpty -and $propEl.ChildNodes.Count -eq 0) { - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") - $propEl.AppendChild($closeWs) | Out-Null - } - - foreach ($val in $values) { - if ($val -in $existing) { - Warn "$propertyName already contains '$val', skipping" - continue - } - $tag = $mapEntry.tag - $attrStr = $mapEntry.attr - if ($attrStr) { - $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" - } else { - $fragXml = "<$tag>$(Esc-Xml $val)" - } - $nodes = Import-Fragment $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $propEl $node $null $childIndent - } - Info "Added $propertyName item: $val" - $script:addCount++ - } -} - -function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) { - $propEl = Find-PropertyElement $propertyName - if (-not $propEl) { - Warn "Property element '$propertyName' not found in Properties" - return - } - - foreach ($val in $values) { - $found = $false - foreach ($child in @($propEl.ChildNodes)) { - if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $val) { - Remove-NodeWithWhitespace $child - Info "Removed $propertyName item: $val" - $script:removeCount++ - $found = $true - break - } - } - if (-not $found) { - Warn "$propertyName item '$val' not found, skipping" - } - } - - # Collapse if empty - $hasElements = $false - foreach ($ch in $propEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } - } - if (-not $hasElements) { - while ($propEl.HasChildNodes) { - $propEl.RemoveChild($propEl.FirstChild) | Out-Null - } - } -} - -function Set-ComplexProperty([string]$propertyName, [string[]]$values) { - $mapEntry = $script:complexPropertyMap[$propertyName] - if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } - - $propEl = Find-PropertyElement $propertyName - if (-not $propEl) { - Warn "Property element '$propertyName' not found in Properties" - return - } - - $indent = Get-ChildIndent $script:propertiesEl - $childIndent = "$indent`t" - - # Remove all existing children - while ($propEl.HasChildNodes) { - $propEl.RemoveChild($propEl.FirstChild) | Out-Null - } - - if ($values.Count -eq 0) { - # Leave self-closing - Info "Cleared $propertyName" - $script:modifyCount++ - return - } - - # Add closing whitespace - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") - $propEl.AppendChild($closeWs) | Out-Null - - # Add each value - foreach ($val in $values) { - $tag = $mapEntry.tag - $attrStr = $mapEntry.attr - if ($attrStr) { - $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" - } else { - $fragXml = "<$tag>$(Esc-Xml $val)" - } - $nodes = Import-Fragment $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $propEl $node $null $childIndent - } - } - $count = $values.Count - Info "Set $propertyName`: $count items" - $script:modifyCount++ -} - -# ============================================================ -# Section 13: Main processing -# ============================================================ - -# --- Inline mode conversion --- -if ($Operation) { - $def = Convert-InlineToDefinition $Operation $Value -} -if (-not $def) { - Write-Error "No definition loaded" - exit 1 -} - -# --- Process complex property operations --- -if ($def.PSObject.Properties.Match("_complex").Count -gt 0 -and $def._complex) { - foreach ($cop in $def._complex) { - switch ($cop.action) { - "add" { Add-ComplexPropertyItem $cop.property $cop.values } - "remove" { Remove-ComplexPropertyItem $cop.property $cop.values } - "set" { Set-ComplexProperty $cop.property $cop.values } - } - } -} - -# --- Process standard operations --- -$def.PSObject.Properties | ForEach-Object { - $prop = $_ - if ($prop.Name -eq "_complex") { return } - $opKey = Resolve-OperationKey $prop.Name - if (-not $opKey) { - Warn "Unknown operation: $($prop.Name)" - return - } - - switch ($opKey) { - "add" { Process-Add $prop.Value } - "remove" { Process-Remove $prop.Value } - "modify" { Process-Modify $prop.Value } - } -} - -# ============================================================ -# Section 14: Save + validate -# ============================================================ - -# Save XML -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = New-Object System.Text.UTF8Encoding($true) # with BOM -$settings.Indent = $false # preserve original whitespace -$settings.NewLineHandling = [System.Xml.NewLineHandling]::None - -# Write using XmlWriter to get proper encoding declaration -$memStream = New-Object System.IO.MemoryStream -$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) -$script:xmlDoc.Save($writer) -$writer.Flush() -$writer.Close() - -$bytes = $memStream.ToArray() -$memStream.Close() - -# Fix encoding case: utf-8 → UTF-8 (cosmetic, 1C accepts both) -$text = [System.Text.Encoding]::UTF8.GetString($bytes) -# Remove BOM from string if present (we'll add it as bytes) -if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { - $text = $text.Substring(1) -} -$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') - -# Write with BOM -$utf8Bom = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) - -Info "Saved: $resolvedPath" - -# ============================================================ -# Section 15: Auto-validate -# ============================================================ - -if (-not $NoValidate) { - $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\meta-validate") "scripts\meta-validate.ps1" - $validateScript = [System.IO.Path]::GetFullPath($validateScript) - if (Test-Path $validateScript) { - Write-Host "" - Write-Host "--- Running meta-validate ---" -ForegroundColor DarkGray - & powershell.exe -NoProfile -File $validateScript -ObjectPath $resolvedPath - } else { - Write-Host "" - Write-Host "[SKIP] meta-validate not found at: $validateScript" -ForegroundColor DarkGray - } -} - -# ============================================================ -# Section 16: Summary -# ============================================================ - -Write-Host "" -Write-Host "=== meta-edit summary ===" -ForegroundColor Green -Write-Host " Object: $($script:objType).$($script:objName)" -Write-Host " Added: $($script:addCount)" -Write-Host " Removed: $($script:removeCount)" -Write-Host " Modified: $($script:modifyCount)" -if ($script:warnCount -gt 0) { - Write-Host " Warnings: $($script:warnCount)" -ForegroundColor Yellow -} - -$totalChanges = $script:addCount + $script:removeCount + $script:modifyCount -if ($totalChanges -eq 0) { - Write-Host " No changes applied." -ForegroundColor Yellow -} - -exit 0 +# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [string]$DefinitionFile, + + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ObjectPath, + + # Inline mode (alternative to DefinitionFile) + [ValidateSet( + "add-attribute", "add-ts", "add-dimension", "add-resource", + "add-enumValue", "add-column", "add-form", "add-template", "add-command", + "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", + "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", + "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", + "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", + "add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts", + "modify-attribute", "modify-dimension", "modify-resource", + "modify-enumValue", "modify-column", + "modify-property", + "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString" + )] + [string]$Operation, + [string]$Value, + + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# ============================================================ +# Section 1: Parameters + loading +# ============================================================ + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { + Write-Error "Cannot use both -DefinitionFile and -Operation" + exit 1 +} +if (-not $DefinitionFile -and -not $Operation) { + Write-Error "Either -DefinitionFile or -Operation is required" + exit 1 +} + +# --- Enum value normalization (same as meta-compile) --- +$script:enumValueAliases = @{ + "Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers" + "RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate" + "ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent" + "NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse" + "Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod" + "None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month" + "Quarterly" = "Quarter"; "Yearly" = "Year" + "Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"; "Месяц" = "Month" + "Квартал" = "Quarter"; "Год" = "Year" + "ПозицияРегистратора" = "RecorderPosition" + "Автоматический" = "Automatic"; "Управляемый" = "Managed" + "Использовать" = "Use"; "НеИспользовать" = "DontUse" + "Разрешить" = "Allow"; "Запретить" = "Deny" + "ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays" + "ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode" + "НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning" + "НеИндексировать" = "DontIndex"; "Индексировать" = "Index" + "ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder" +} + +$script:validEnumValues = @{ + "RegisterType" = @("Balance","Turnovers") + "WriteMode" = @("Independent","RecorderSubordinate") + "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") + "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") + "DataLockControlMode" = @("Automatic","Managed") + "FullTextSearch" = @("Use","DontUse") + "DataHistory" = @("Use","DontUse") + "DefaultPresentation" = @("AsDescription","AsCode") + "Posting" = @("Allow","Deny") + "RealTimePosting" = @("Allow","Deny") + "EditType" = @("InDialog","InList","BothWays") + "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") + "CodeType" = @("String","Number") + "CodeAllowedLength" = @("Variable","Fixed") + "NumberType" = @("String","Number") + "NumberAllowedLength" = @("Variable","Fixed") + "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") + "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") + "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") + "ReuseSessions" = @("DontUse","AutoUse") + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") +} + +function Normalize-EnumValue { + param([string]$propName, [string]$value) + # 1. Check alias dictionary — silent auto-correct + if ($script:enumValueAliases.ContainsKey($value)) { + return $script:enumValueAliases[$value] + } + # 2. Case-insensitive match against valid values — silent + $valid = $script:validEnumValues[$propName] + if ($valid) { + foreach ($v in $valid) { + if ($v -ieq $value) { return $v } + } + # 3. Known property, unknown value — error with hint + Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')" + exit 1 + } + # 4. Unknown property — pass-through (no validation data) + return $value +} + +# --- Load JSON definition (DefinitionFile mode) --- +$def = $null +if ($DefinitionFile) { + if (-not (Test-Path $DefinitionFile)) { + Write-Error "Definition file not found: $DefinitionFile" + exit 1 + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $def = $jsonText | ConvertFrom-Json +} + +# --- Resolve object path --- +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } elseif (Test-Path $sibling) { + $ObjectPath = $sibling + } else { + Write-Error "Directory given but no $dirName.xml found inside or as sibling" + exit 1 + } +} +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $ObjectPath)) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) + $parentDir = Split-Path $ObjectPath + $parentDirName = Split-Path $parentDir -Leaf + if ($fileName -eq $parentDirName) { + $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Error "Object file not found: $ObjectPath" + exit 1 +} +$resolvedPath = (Resolve-Path $ObjectPath).Path + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $resolvedPath 'editable' + +# --- Load XML --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +# --- Counters --- +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 +$script:warnCount = 0 + +function Warn($msg) { + Write-Host "[WARN] $msg" -ForegroundColor Yellow + $script:warnCount++ +} + +function Info($msg) { + Write-Host "[INFO] $msg" -ForegroundColor Cyan +} + +# ============================================================ +# Section 2: Detect object type +# ============================================================ + +$root = $script:xmlDoc.DocumentElement +if ($root.LocalName -ne "MetaDataObject") { + Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)" + exit 1 +} + +# Find the first child element — this is the object type element +$script:objElement = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $script:objElement = $child + break + } +} +if (-not $script:objElement) { + Write-Error "No object element found under MetaDataObject" + exit 1 +} + +$script:objType = $script:objElement.LocalName +$script:mdNs = $script:objElement.NamespaceURI + +# Find Properties and ChildObjects +$script:propertiesEl = $null +$script:childObjectsEl = $null +foreach ($child in $script:objElement.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propertiesEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjectsEl = $child } +} + +if (-not $script:propertiesEl) { + Write-Error "No found in $($script:objType)" + exit 1 +} + +# Extract object name +$script:objName = "" +foreach ($child in $script:propertiesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { + $script:objName = $child.InnerText.Trim() + break + } +} + +Info "Object: $($script:objType).$($script:objName)" + +# ============================================================ +# Section 3: Synonym tables +# ============================================================ + +# Operation synonyms +$script:operationSynonyms = @{ + "add" = "add"; "добавить" = "add" + "remove" = "remove"; "удалить" = "remove" + "modify" = "modify"; "изменить" = "modify" +} + +# Child type synonyms +$script:childTypeSynonyms = @{ + "attributes" = "attributes"; "реквизиты" = "attributes"; "attrs" = "attributes" + "tabularsections" = "tabularSections"; "табличныечасти" = "tabularSections"; "тч" = "tabularSections"; "ts" = "tabularSections" + "dimensions" = "dimensions"; "измерения" = "dimensions"; "dims" = "dimensions" + "resources" = "resources"; "ресурсы" = "resources"; "res" = "resources" + "enumvalues" = "enumValues"; "значения" = "enumValues"; "values" = "enumValues" + "columns" = "columns"; "графы" = "columns"; "колонки" = "columns" + "forms" = "forms"; "формы" = "forms" + "templates" = "templates"; "макеты" = "templates" + "commands" = "commands"; "команды" = "commands" + "properties" = "properties"; "свойства" = "properties" +} + +# Type synonyms (from meta-compile) +$script:typeSynonyms = New-Object System.Collections.Hashtable +$script:typeSynonyms["число"] = "Number" +$script:typeSynonyms["строка"] = "String" +$script:typeSynonyms["булево"] = "Boolean" +$script:typeSynonyms["дата"] = "Date" +$script:typeSynonyms["датавремя"]= "DateTime" +$script:typeSynonyms["хранилищезначения"] = "ValueStorage" +$script:typeSynonyms["number"] = "Number" +$script:typeSynonyms["string"] = "String" +$script:typeSynonyms["boolean"] = "Boolean" +$script:typeSynonyms["date"] = "Date" +$script:typeSynonyms["datetime"] = "DateTime" +$script:typeSynonyms["valuestorage"] = "ValueStorage" +$script:typeSynonyms["bool"] = "Boolean" +# Reference synonyms +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:typeSynonyms["задачассылка"] = "TaskRef" +$script:typeSynonyms["определяемыйтип"] = "DefinedType" +$script:typeSynonyms["definedtype"] = "DefinedType" +$script:typeSynonyms["catalogref"] = "CatalogRef" +$script:typeSynonyms["documentref"] = "DocumentRef" +$script:typeSynonyms["enumref"] = "EnumRef" + +# ============================================================ +# Section 4: Type system +# ============================================================ + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function Split-CamelCase { + param([string]$name) + if (-not $name) { return $name } + $result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2') + $result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2') + if ($result.Length -gt 1) { + $result = $result.Substring(0,1) + $result.Substring(1).ToLower() + } + return $result +} + +function New-Guid-String { + return [System.Guid]::NewGuid().ToString() +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + # Parameterized: Number(15,2), Строка(100) + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + # Reference: СправочникСсылка.Организации + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + # Simple + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + return $typeStr +} + +function Build-TypeContentXml { + param([string]$indent, [string]$typeStr) + if (-not $typeStr) { return "" } + + # Composite type: "Type1 + Type2 + Type3" + if ($typeStr.Contains(' + ')) { + $parts = $typeStr -split '\s*\+\s*' + $sb = New-Object System.Text.StringBuilder + foreach ($part in $parts) { + $inner = Build-TypeContentXml $indent $part.Trim() + if ($inner) { $sb.AppendLine($inner) | Out-Null } + } + return $sb.ToString().TrimEnd("`r","`n") + } + + $typeStr = Resolve-TypeStr $typeStr + $sb = New-Object System.Text.StringBuilder + + # Boolean + if ($typeStr -eq "Boolean") { + $sb.AppendLine("$indentxs:boolean") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # ValueStorage + if ($typeStr -eq "ValueStorage") { + $sb.AppendLine("$indentxs:base64Binary") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # String or String(N) + if ($typeStr -match '^String(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "10" } + $sb.AppendLine("$indentxs:string") | Out-Null + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t$len") | Out-Null + $sb.AppendLine("$indent`tVariable") | Out-Null + $sb.AppendLine("$indent") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # Number(D,F) or Number(D,F,nonneg) + if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1]; $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + $sb.AppendLine("$indentxs:decimal") | Out-Null + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t$digits") | Out-Null + $sb.AppendLine("$indent`t$fraction") | Out-Null + $sb.AppendLine("$indent`t$sign") | Out-Null + $sb.AppendLine("$indent") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # Number without params → Number(10,0) + if ($typeStr -eq "Number") { + $sb.AppendLine("$indentxs:decimal") | Out-Null + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t10") | Out-Null + $sb.AppendLine("$indent`t0") | Out-Null + $sb.AppendLine("$indent`tAny") | Out-Null + $sb.AppendLine("$indent") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # Date / DateTime + if ($typeStr -eq "Date") { + $sb.AppendLine("$indentxs:dateTime") | Out-Null + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`tDate") | Out-Null + $sb.AppendLine("$indent") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + if ($typeStr -eq "DateTime") { + $sb.AppendLine("$indentxs:dateTime") | Out-Null + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`tDateTime") | Out-Null + $sb.AppendLine("$indent") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # DefinedType + if ($typeStr -match '^DefinedType\.(.+)$') { + $dtName = $Matches[1] + $sb.AppendLine("$indentcfg:DefinedType.$dtName") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # Reference types — use local xmlns declaration for 1C compatibility + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { + $sb.AppendLine("$indentd5p1:$typeStr") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") + } + + # Fallback + $sb.AppendLine("$indent$typeStr") | Out-Null + return $sb.ToString().TrimEnd("`r","`n") +} + +function Build-ValueTypeXml { + param([string]$indent, [string]$typeStr) + $inner = Build-TypeContentXml "$indent`t" $typeStr + return "$indent`r`n$inner`r`n$indent" +} + +function Build-FillValueXml { + param([string]$indent, [string]$typeStr) + if (-not $typeStr) { + return "$indent" + } + $typeStr = Resolve-TypeStr $typeStr + if ($typeStr -eq "Boolean") { + return "$indentfalse" + } + if ($typeStr -match '^String') { + return "$indent" + } + if ($typeStr -match '^Number') { + return "$indent0" + } + return "$indent" +} + +function Build-MLTextXml { + param([string]$indent, [string]$tag, [string]$text) + if (-not $text) { + return "$indent<$tag/>" + } + $lines = @( + "$indent<$tag>" + "$indent`t" + "$indent`t`tru" + "$indent`t`t$(Esc-Xml $text)" + "$indent`t" + "$indent" + ) + return $lines -join "`r`n" +} + +# ============================================================ +# Section 5: DOM helpers +# ============================================================ + +$script:metaNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +function Import-Fragment([string]$xmlString) { + $wrapper = @" +<_W xmlns="http://v8.1c.ru/8.3/MDClasses" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:v8="http://v8.1c.ru/8.1/data/core" + xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" + xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" + xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString +"@ + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + $text = $child.Value + if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($text -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + # Fallback: count depth + $depth = 0 + $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Find-ElementByName($container, [string]$elemLocalName, [string]$nameValue) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -ne $elemLocalName) { continue } + # Look for Properties/Name or just Name child + $propsEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { + $propsEl = $gc; break + } + } + $searchIn = if ($propsEl) { $propsEl } else { $child } + foreach ($gc in $searchIn.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name" -and $gc.InnerText.Trim() -eq $nameValue) { + return $child + } + } + } + return $null +} + +function Find-LastElementOfType($container, [string]$localName) { + $last = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { + $last = $child + } + } + return $last +} + +function Find-FirstElementOfType($container, [string]$localName) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { + return $child + } + } + return $null +} + +function Ensure-ChildObjectsOpen { + if ($script:childObjectsEl) { + # Check if it's self-closing (no child elements) + $hasElements = $false + foreach ($ch in $script:childObjectsEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } + } + if (-not $hasElements) { + # It's empty — we need to add whitespace for proper formatting + $indent = Get-ChildIndent $script:objElement + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") + $script:childObjectsEl.AppendChild($closeWs) | Out-Null + } + return + } + # No ChildObjects at all — create one after Properties + $indent = Get-ChildIndent $script:objElement + $coXml = "`r`n$indent`r`n$indent" + # Insert after Properties + $refNode = $null + $foundProps = $false + foreach ($child in $script:objElement.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Properties") { + $foundProps = $true + continue + } + if ($foundProps -and $child.NodeType -eq 'Element') { + $refNode = $child + break + } + } + + $coEl = $script:xmlDoc.CreateElement("ChildObjects", $script:mdNs) + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") + $coEl.AppendChild($closeWs) | Out-Null + + $wsB = $script:xmlDoc.CreateWhitespace("`r`n$indent") + if ($refNode) { + $script:objElement.InsertBefore($wsB, $refNode) | Out-Null + $script:objElement.InsertBefore($coEl, $wsB) | Out-Null + } else { + # After last child + $trailing = $script:objElement.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $script:objElement.InsertBefore($wsB, $trailing) | Out-Null + $script:objElement.InsertBefore($coEl, $trailing) | Out-Null + } else { + $script:objElement.AppendChild($wsB) | Out-Null + $script:objElement.AppendChild($coEl) | Out-Null + } + } + $script:childObjectsEl = $coEl +} + +function Collapse-ChildObjectsIfEmpty { + if (-not $script:childObjectsEl) { return } + $hasElements = $false + foreach ($ch in $script:childObjectsEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } + } + if (-not $hasElements) { + # Remove all whitespace children + while ($script:childObjectsEl.HasChildNodes) { + $script:childObjectsEl.RemoveChild($script:childObjectsEl.FirstChild) | Out-Null + } + } +} + +# ============================================================ +# Section 6: Fragment builders +# ============================================================ + +function Parse-AttributeShorthand { + param($val) + + if ($val -is [string]) { + $str = "$val" + $parsed = @{ + name = ""; type = ""; synonym = ""; comment = "" + flags = @(); fillChecking = ""; indexing = "" + after = ""; before = "" + } + # Extract positional markers: >> after Name, << before Name + if ($str -match '\s*>>\s*after\s+(\S+)\s*$') { + $parsed.after = $Matches[1] + $str = ($str -replace '\s*>>\s*after\s+\S+\s*$', '').Trim() + } elseif ($str -match '\s*<<\s*before\s+(\S+)\s*$') { + $parsed.before = $Matches[1] + $str = ($str -replace '\s*<<\s*before\s+\S+\s*$', '').Trim() + } + # Split by | for flags + $parts = $str -split '\|', 2 + $mainPart = $parts[0].Trim() + if ($parts.Count -gt 1) { + $flagStr = $parts[1].Trim() + $parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) + } + # Split by : for name and type + $colonParts = $mainPart -split ':', 2 + $parsed.name = $colonParts[0].Trim() + if ($colonParts.Count -gt 1) { + $parsed.type = $colonParts[1].Trim() + } + $parsed.synonym = Split-CamelCase $parsed.name + return $parsed + } + + # Object form + $name = "$($val.name)" + $result = @{ + name = $name + type = if ($val.type -is [array]) { ($val.type | ForEach-Object { "$_" }) -join ' + ' } elseif ($val.type) { "$($val.type)" } else { "" } + synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } + comment = if ($val.comment) { "$($val.comment)" } else { "" } + flags = @(if ($val.flags) { $val.flags } else { @() }) + fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } + indexing = if ($val.indexing) { "$($val.indexing)" } else { "" } + after = if ($val.after) { "$($val.after)" } else { "" } + before = if ($val.before) { "$($val.before)" } else { "" } + } + # Map flags to properties + if ($result.flags -contains "req" -and -not $result.fillChecking) { + $result.fillChecking = "ShowError" + } + if ($result.flags -contains "index" -and -not $result.indexing) { + $result.indexing = "Index" + } + if ($result.flags -contains "indexadditional" -and -not $result.indexing) { + $result.indexing = "IndexWithAdditionalOrder" + } + return $result +} + +function Parse-EnumValueShorthand { + param($val) + if ($val -is [string]) { + $name = "$val" + return @{ + name = $name + synonym = Split-CamelCase $name + comment = "" + after = ""; before = "" + } + } + $name = "$($val.name)" + return @{ + name = $name + synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } + comment = if ($val.comment) { "$($val.comment)" } else { "" } + after = if ($val.after) { "$($val.after)" } else { "" } + before = if ($val.before) { "$($val.before)" } else { "" } + } +} + +# Determine attribute context from object type +function Get-AttributeContext { + switch ($script:objType) { + "Catalog" { return "catalog" } + "Document" { return "document" } + { $_ -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") } { return "register" } + { $_ -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport") } { return "processor" } + default { return "object" } + } +} + +$script:reservedAttrNames = @{ + "Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование" + "Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец" + "IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных" + "Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность" + "Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый" + "Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача" + "Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс" + "ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого" + "CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись" + "Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый" +} + +function Build-AttributeFragment { + param($parsed, [string]$context, [string]$indent) + + if (-not $context) { $context = Get-AttributeContext } + + # Check reserved attribute names + $attrName = $parsed.name + if ($script:reservedAttrNames.ContainsKey($attrName)) { + Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." + } + $ruValues = $script:reservedAttrNames.Values + if ($ruValues -contains $attrName) { + Write-Warning "Attribute '$attrName' conflicts with a standard attribute name (Russian). This may cause errors when loading into 1C." + } + + $uuid = New-Guid-String + $sb = New-Object System.Text.StringBuilder + + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + # Type + $typeStr = $parsed.type + if ($typeStr) { + $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null + } else { + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`txs:string") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + # FillFromFillingValue/FillValue — not for register, tabular (config TS), or processor (non-stored top-level) + if ($context -notin @("register", "tabular", "processor")) { + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine($(Build-FillValueXml "$indent`t`t" $typeStr)) | Out-Null + } + + # FillChecking + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + if ($parsed.fillChecking) { $fillChecking = Normalize-EnumValue "FillChecking" $parsed.fillChecking } + $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null + + $sb.AppendLine("$indent`t`tItems") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + + # Use — catalog only + if ($context -eq "catalog") { + $sb.AppendLine("$indent`t`tForItem") | Out-Null + } + + # Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular) + if ($context -notin @("processor", "processor-tabular")) { + $indexing = "DontIndex" + if ($parsed.flags -contains "index") { $indexing = "Index" } + if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } + if ($parsed.indexing) { $indexing = Normalize-EnumValue "Indexing" $parsed.indexing } + $sb.AppendLine("$indent`t`t$indexing") | Out-Null + + $sb.AppendLine("$indent`t`tUse") | Out-Null + $sb.AppendLine("$indent`t`tUse") | Out-Null + } + + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-TabularSectionFragment { + param($tsDef, [string]$indent) + + $tsName = "$($tsDef.name)" + $tsSynonym = if ($tsDef.synonym) { "$($tsDef.synonym)" } else { Split-CamelCase $tsName } + $uuid = New-Guid-String + $objType = $script:objType + $objName = $script:objName + + $typePrefix = "${objType}TabularSection" + $rowPrefix = "${objType}TabularSectionRow" + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("$indent") | Out-Null + + # InternalInfo + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null + $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null + $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + + # Properties + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $tsName)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $tsSynonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tDontCheck") | Out-Null + + # StandardAttributes (LineNumber) + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`tDontCheck") | Out-Null + $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null + $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + # Use — catalog only + if ($objType -eq "Catalog") { + $sb.AppendLine("$indent`t`tForItem") | Out-Null + } + + $sb.AppendLine("$indent`t") | Out-Null + + # ChildObjects with attrs + $columns = @() + if ($tsDef.attrs) { $columns = @($tsDef.attrs) } + elseif ($tsDef.attributes) { $columns = @($tsDef.attributes) } + elseif ($tsDef.реквизиты) { $columns = @($tsDef.реквизиты) } + + $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } + if ($columns.Count -gt 0) { + $sb.AppendLine("$indent`t") | Out-Null + foreach ($col in $columns) { + $colParsed = Parse-AttributeShorthand $col + $sb.AppendLine($(Build-AttributeFragment $colParsed $tsAttrContext "$indent`t`t")) | Out-Null + } + $sb.AppendLine("$indent`t") | Out-Null + } else { + $sb.AppendLine("$indent`t") | Out-Null + } + + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-DimensionFragment { + param($parsed, [string]$registerType, [string]$indent) + + if (-not $registerType) { $registerType = $script:objType } + $uuid = New-Guid-String + $sb = New-Object System.Text.StringBuilder + + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + $typeStr = $parsed.type + if ($typeStr) { + $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null + } else { + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`txs:string") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + # InformationRegister: FillFromFillingValue, FillValue + if ($registerType -eq "InformationRegister") { + $fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" } + $sb.AppendLine("$indent`t`t$fillFrom") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null + + $sb.AppendLine("$indent`t`tItems") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + + # InformationRegister: Master, MainFilter, DenyIncompleteValues + if ($registerType -eq "InformationRegister") { + $master = if ($parsed.flags -contains "master") { "true" } else { "false" } + $mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" } + $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } + $sb.AppendLine("$indent`t`t$master") | Out-Null + $sb.AppendLine("$indent`t`t$mainFilter") | Out-Null + $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null + } + + # AccumulationRegister: DenyIncompleteValues + if ($registerType -eq "AccumulationRegister") { + $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } + $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null + } + + $indexing = "DontIndex" + if ($parsed.flags -contains "index") { $indexing = "Index" } + $sb.AppendLine("$indent`t`t$indexing") | Out-Null + + $sb.AppendLine("$indent`t`tUse") | Out-Null + + # AccumulationRegister: UseInTotals + if ($registerType -eq "AccumulationRegister") { + $useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" } + $sb.AppendLine("$indent`t`t$useInTotals") | Out-Null + } + + # InformationRegister: DataHistory + if ($registerType -eq "InformationRegister") { + $sb.AppendLine("$indent`t`tUse") | Out-Null + } + + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-ResourceFragment { + param($parsed, [string]$registerType, [string]$indent) + + if (-not $registerType) { $registerType = $script:objType } + $uuid = New-Guid-String + $sb = New-Object System.Text.StringBuilder + + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + $typeStr = $parsed.type + if ($typeStr) { + $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null + } else { + # Default: Number(15,2) + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`txs:decimal") | Out-Null + $sb.AppendLine("$indent`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t`t`t15") | Out-Null + $sb.AppendLine("$indent`t`t`t`t2") | Out-Null + $sb.AppendLine("$indent`t`t`t`tAny") | Out-Null + $sb.AppendLine("$indent`t`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + + # InformationRegister: FillFromFillingValue, FillValue + if ($registerType -eq "InformationRegister") { + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + + $fillChecking = "DontCheck" + if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } + $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null + + $sb.AppendLine("$indent`t`tItems") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + + # InformationRegister: Indexing, FullTextSearch, DataHistory + if ($registerType -eq "InformationRegister") { + $sb.AppendLine("$indent`t`tDontIndex") | Out-Null + $sb.AppendLine("$indent`t`tUse") | Out-Null + $sb.AppendLine("$indent`t`tUse") | Out-Null + } + + # AccumulationRegister: FullTextSearch + if ($registerType -eq "AccumulationRegister") { + $sb.AppendLine("$indent`t`tUse") | Out-Null + } + + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-EnumValueFragment { + param($parsed, [string]$indent) + + $uuid = New-Guid-String + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-ColumnFragment { + param($colDef, [string]$indent) + + $uuid = New-Guid-String + $name = "" + $synonym = "" + $indexing = "DontIndex" + $references = @() + + if ($colDef -is [string]) { + $name = "$colDef" + $synonym = Split-CamelCase $name + } else { + $name = "$($colDef.name)" + $synonym = if ($colDef.synonym) { "$($colDef.synonym)" } else { Split-CamelCase $name } + if ($colDef.indexing) { $indexing = "$($colDef.indexing)" } + if ($colDef.references) { $references = @($colDef.references) } + } + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("$indent") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t$indexing") | Out-Null + if ($references.Count -gt 0) { + $sb.AppendLine("$indent`t`t") | Out-Null + foreach ($ref in $references) { + $sb.AppendLine("$indent`t`t`t$ref") | Out-Null + } + $sb.AppendLine("$indent`t`t") | Out-Null + } else { + $sb.AppendLine("$indent`t`t") | Out-Null + } + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +function Build-SimpleChildFragment { + param([string]$tagName, [string]$name, [string]$indent) + # For Form, Template, Command — just a name wrapper + $uuid = New-Guid-String + $synonym = Split-CamelCase $name + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("$indent<$tagName uuid=`"$uuid`">") | Out-Null + $sb.AppendLine("$indent`t") | Out-Null + $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null + $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + # Forms get additional properties + if ($tagName -eq "Form") { + $sb.AppendLine("$indent`t`tOrdinary") | Out-Null + $sb.AppendLine("$indent`t`tfalse") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + if ($tagName -eq "Template") { + $sb.AppendLine("$indent`t`tSpreadsheetDocument") | Out-Null + } + if ($tagName -eq "Command") { + $sb.AppendLine("$indent`t`tFormNavigationPanelGoTo") | Out-Null + $sb.AppendLine("$indent`t`tAuto") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + $sb.AppendLine("$indent`t`t") | Out-Null + } + $sb.AppendLine("$indent`t") | Out-Null + $sb.Append("$indent") | Out-Null + return $sb.ToString() +} + +# ============================================================ +# Section 7: Name uniqueness check +# ============================================================ + +function Get-AllChildNames { + $names = @{} + if (-not $script:childObjectsEl) { return $names } + foreach ($child in $script:childObjectsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $propsEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { + $propsEl = $gc; break + } + } + if (-not $propsEl) { continue } + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { + $n = $gc.InnerText.Trim() + if ($n) { $names[$n] = $child.LocalName } + break + } + } + # Also check ChildObjects of TabularSections for nested names + if ($child.LocalName -eq "TabularSection") { + foreach ($tsCh in $child.ChildNodes) { + if ($tsCh.NodeType -eq 'Element' -and $tsCh.LocalName -eq "ChildObjects") { + foreach ($tsChild in $tsCh.ChildNodes) { + if ($tsChild.NodeType -ne 'Element') { continue } + $tsProps = $null + foreach ($gc in $tsChild.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { + $tsProps = $gc; break + } + } + if ($tsProps) { + foreach ($gc in $tsProps.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { + # TS attr names don't conflict with top-level + break + } + } + } + } + } + } + } + } + return $names +} + +# ============================================================ +# Section 8: Context and allowed child types +# ============================================================ + +$script:validChildTypes = @{ + "Catalog" = @("attributes","tabularSections","forms","templates","commands") + "Document" = @("attributes","tabularSections","forms","templates","commands") + "ExchangePlan" = @("attributes","tabularSections","forms","templates","commands") + "ChartOfAccounts" = @("attributes","tabularSections","forms","templates","commands") + "ChartOfCharacteristicTypes" = @("attributes","tabularSections","forms","templates","commands") + "ChartOfCalculationTypes" = @("attributes","tabularSections","forms","templates","commands") + "BusinessProcess" = @("attributes","tabularSections","forms","templates","commands") + "Task" = @("attributes","tabularSections","forms","templates","commands") + "Report" = @("attributes","tabularSections","forms","templates","commands") + "DataProcessor" = @("attributes","tabularSections","forms","templates","commands") + "Enum" = @("enumValues","forms","templates","commands") + "InformationRegister" = @("dimensions","resources","attributes","forms","templates","commands") + "AccumulationRegister" = @("dimensions","resources","attributes","forms","templates","commands") + "AccountingRegister" = @("dimensions","resources","attributes","forms","templates","commands") + "CalculationRegister" = @("dimensions","resources","attributes","forms","templates","commands") + "DocumentJournal" = @("columns","forms","templates","commands") + "Constant" = @("forms") +} + +# Canonical child order in ChildObjects +$script:childOrder = @( + "Resource", "Dimension", "Attribute", "TabularSection", + "AccountingFlag", "ExtDimensionAccountingFlag", + "EnumValue", "Column", "AddressingAttribute", "Recalculation", + "Form", "Template", "Command" +) + +# Map from DSL child type to XML element name +$script:childTypeToXmlTag = @{ + "attributes" = "Attribute" + "tabularSections" = "TabularSection" + "dimensions" = "Dimension" + "resources" = "Resource" + "enumValues" = "EnumValue" + "columns" = "Column" + "forms" = "Form" + "templates" = "Template" + "commands" = "Command" +} + +# ============================================================ +# Section 9: DSL key normalization +# ============================================================ + +function Resolve-OperationKey([string]$key) { + $k = $key.ToLower().Trim() + if ($script:operationSynonyms.ContainsKey($k)) { + return $script:operationSynonyms[$k] + } + return $null +} + +function Resolve-ChildTypeKey([string]$key) { + $k = $key.ToLower().Trim() + if ($script:childTypeSynonyms.ContainsKey($k)) { + return $script:childTypeSynonyms[$k] + } + return $null +} + +# ============================================================ +# Section 9.5: Inline mode converter +# ============================================================ + +function Split-ByCommaOutsideParens([string]$str) { + $result = @() + $depth = 0 + $current = "" + foreach ($ch in $str.ToCharArray()) { + if ($ch -eq '(') { $depth++ } + elseif ($ch -eq ')') { $depth-- } + if ($ch -eq ',' -and $depth -eq 0) { + $result += $current + $current = "" + } else { + $current += $ch + } + } + if ($current) { $result += $current } + return ,$result +} + +function Convert-InlineToDefinition([string]$operation, [string]$value) { + # Parse operation: "add-attribute" → ("add", "attribute") + $opParts = $operation -split '-', 2 + $op = $opParts[0] # add, remove, modify, set + $target = $opParts[1] # attribute, ts, owner, owners, property, etc. + + # Complex property targets + $complexTargetMap = @{ + "owner" = "Owners"; "owners" = "Owners" + "registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords" + "basedOn" = "BasedOn" + "inputByString" = "InputByString" + } + + if ($complexTargetMap.ContainsKey($target)) { + $propName = $complexTargetMap[$target] + $values = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + # For InputByString, auto-prefix with MetaType.Name. + if ($propName -eq "InputByString") { + $prefix = "$($script:objType).$($script:objName)." + $values = @($values | ForEach-Object { + if ($_ -notmatch '\.') { + "$prefix$_" + } elseif ($_ -notmatch '^(Catalog|Document|InformationRegister|AccumulationRegister|AccountingRegister|CalculationRegister|ChartOfCharacteristicTypes|ChartOfCalculationTypes|ChartOfAccounts|ExchangePlan|BusinessProcess|Task|Enum|Report|DataProcessor)\.') { + "$prefix$_" + } else { $_ } + }) + } + $def = New-Object PSCustomObject + $complexAction = if ($op -eq "set") { "set" } else { $op } + $def | Add-Member -NotePropertyName "_complex" -NotePropertyValue @( + @{ action = $complexAction; property = $propName; values = $values } + ) + return $def + } + + # TS attribute operations: dot notation "TSName.AttrDef" + if ($target -eq "ts-attribute") { + $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + # Group by TS name + $tsGroups = [ordered]@{} + foreach ($item in $items) { + $dotIdx = $item.IndexOf('.') + if ($dotIdx -le 0) { + Warn "Invalid ts-attribute format (expected TSName.AttrDef): $item" + continue + } + $tsName = $item.Substring(0, $dotIdx).Trim() + $rest = $item.Substring($dotIdx + 1).Trim() + if (-not $tsGroups.Contains($tsName)) { + $tsGroups[$tsName] = @() + } + $tsGroups[$tsName] += $rest + } + + # Build: { modify: { tabularSections: { TSName: { add/remove/modify: ... } } } } + $tsModObj = New-Object PSCustomObject + foreach ($tsName in $tsGroups.Keys) { + $tsChanges = New-Object PSCustomObject + switch ($op) { + "add" { + $tsChanges | Add-Member -NotePropertyName "add" -NotePropertyValue $tsGroups[$tsName] + } + "remove" { + $tsChanges | Add-Member -NotePropertyName "remove" -NotePropertyValue $tsGroups[$tsName] + } + "modify" { + $attrModObj = New-Object PSCustomObject + foreach ($elemDef in $tsGroups[$tsName]) { + $colonIdx = $elemDef.IndexOf(':') + if ($colonIdx -le 0) { + Warn "Invalid modify format (expected Name: key=val): $elemDef" + continue + } + $elemName = $elemDef.Substring(0, $colonIdx).Trim() + $changesPart = $elemDef.Substring($colonIdx + 1).Trim() + $changesObj = New-Object PSCustomObject + $changePairs = Split-ByCommaOutsideParens $changesPart + foreach ($cp in $changePairs) { + $cp = $cp.Trim() + $eqIdx = $cp.IndexOf('=') + if ($eqIdx -gt 0) { + $ck = $cp.Substring(0, $eqIdx).Trim() + $cv = $cp.Substring($eqIdx + 1).Trim() + $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv + } + } + $attrModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj + } + $tsChanges | Add-Member -NotePropertyName "modify" -NotePropertyValue $attrModObj + } + } + $tsModObj | Add-Member -NotePropertyName $tsName -NotePropertyValue $tsChanges + } + $def = New-Object PSCustomObject + $modifyObj = New-Object PSCustomObject + $modifyObj | Add-Member -NotePropertyName "tabularSections" -NotePropertyValue $tsModObj + $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj + return $def + } + + # Target → JSON DSL child type + $targetMap = @{ + "attribute" = "attributes" + "ts" = "tabularSections" + "dimension" = "dimensions" + "resource" = "resources" + "enumValue" = "enumValues" + "column" = "columns" + "form" = "forms" + "template" = "templates" + "command" = "commands" + "property" = "properties" + } + + $childType = $targetMap[$target] + if (-not $childType) { + Write-Error "Unknown inline target: $target" + exit 1 + } + + $def = New-Object PSCustomObject + + switch ($op) { + "add" { + $items = @() + if ($childType -eq "tabularSections") { + # TS format: "TSName: attr1_shorthand, attr2_shorthand, ..." + $tsValues = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + foreach ($tsVal in $tsValues) { + $colonIdx = $tsVal.IndexOf(':') + if ($colonIdx -gt 0) { + $tsName = $tsVal.Substring(0, $colonIdx).Trim() + $attrsPart = $tsVal.Substring($colonIdx + 1).Trim() + # Split attrs by comma (paren-aware), reassemble if part doesn't start with "Name:" + $rawParts = Split-ByCommaOutsideParens $attrsPart + $attrStrs = @() + $current = "" + foreach ($rp in $rawParts) { + $rp = $rp.Trim() + if ($current -and $rp -match '^[А-Яа-яЁёA-Za-z_]\w*\s*:') { + $attrStrs += $current + $current = $rp + } elseif ($current) { + $current += ", $rp" + } else { + $current = $rp + } + } + if ($current) { $attrStrs += $current } + $items += [PSCustomObject]@{ name = $tsName; attrs = $attrStrs } + } else { + # Just a name, no attrs + $items += $tsVal + } + } + } else { + # Batch split by ;; + $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + $addObj = New-Object PSCustomObject + $addObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items + $def | Add-Member -NotePropertyName "add" -NotePropertyValue $addObj + } + "remove" { + $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $removeObj = New-Object PSCustomObject + $removeObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items + $def | Add-Member -NotePropertyName "remove" -NotePropertyValue $removeObj + } + "modify" { + if ($childType -eq "properties") { + # "CodeLength=11 ;; DescriptionLength=150" + $kvPairs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $propsObj = New-Object PSCustomObject + foreach ($kv in $kvPairs) { + $eqIdx = $kv.IndexOf('=') + if ($eqIdx -gt 0) { + $k = $kv.Substring(0, $eqIdx).Trim() + $v = $kv.Substring($eqIdx + 1).Trim() + $propsObj | Add-Member -NotePropertyName $k -NotePropertyValue $v + } else { + Warn "Invalid property format (expected Key=Value): $kv" + } + } + $modifyObj = New-Object PSCustomObject + $modifyObj | Add-Member -NotePropertyName "properties" -NotePropertyValue $propsObj + $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj + } else { + # "ElementName: key=val, key=val ;; Element2: key=val" + $elemDefs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $childModObj = New-Object PSCustomObject + foreach ($elemDef in $elemDefs) { + $colonIdx = $elemDef.IndexOf(':') + if ($colonIdx -le 0) { + Warn "Invalid modify format (expected Name: key=val): $elemDef" + continue + } + $elemName = $elemDef.Substring(0, $colonIdx).Trim() + $changesPart = $elemDef.Substring($colonIdx + 1).Trim() + $changesObj = New-Object PSCustomObject + $changePairs = Split-ByCommaOutsideParens $changesPart + foreach ($cp in $changePairs) { + $cp = $cp.Trim() + $eqIdx = $cp.IndexOf('=') + if ($eqIdx -gt 0) { + $ck = $cp.Substring(0, $eqIdx).Trim() + $cv = $cp.Substring($eqIdx + 1).Trim() + $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv + } + } + $childModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj + } + $modifyObj = New-Object PSCustomObject + $modifyObj | Add-Member -NotePropertyName $childType -NotePropertyValue $childModObj + $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj + } + } + } + + return $def +} + +# ============================================================ +# Section 10: ADD operations +# ============================================================ + +function Find-InsertionPoint { + param([string]$xmlTag, $parsed) + # Returns $refNode for Insert-BeforeElement (null = append) + + if (-not $script:childObjectsEl) { return $null } + + # Positional: after/before + if ($parsed.after) { + $afterEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.after + if ($afterEl) { + # Insert after = insert before the next element sibling + $next = $afterEl.NextSibling + while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } + if ($next -and $next.LocalName -eq $xmlTag) { return $next } + return $null # append + } else { + Warn "after='$($parsed.after)': element '$($parsed.after)' not found in $xmlTag, appending" + } + } + if ($parsed.before) { + $beforeEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.before + if ($beforeEl) { return $beforeEl } + Warn "before='$($parsed.before)': element '$($parsed.before)' not found in $xmlTag, appending" + } + + # Default: after last element of this type, or in canonical position + $lastOfType = Find-LastElementOfType $script:childObjectsEl $xmlTag + if ($lastOfType) { + $next = $lastOfType.NextSibling + while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } + return $next # null means append (which is correct: after last of type) + } + + # No elements of this type yet — find canonical position + $tagIdx = [array]::IndexOf($script:childOrder, $xmlTag) + if ($tagIdx -lt 0) { return $null } + + # Find first element of any type that comes AFTER in the canonical order + for ($i = $tagIdx + 1; $i -lt $script:childOrder.Count; $i++) { + $nextTag = $script:childOrder[$i] + $firstOfNext = Find-FirstElementOfType $script:childObjectsEl $nextTag + if ($firstOfNext) { return $firstOfNext } + } + + return $null # append at end +} + +function Process-Add($addDef) { + $addDef.PSObject.Properties | ForEach-Object { + $rawKey = $_.Name + $items = $_.Value + $childType = Resolve-ChildTypeKey $rawKey + + if (-not $childType) { + Warn "Unknown add child type: $rawKey" + return + } + + # Validate allowed + $allowed = $script:validChildTypes[$script:objType] + if ($allowed -and $childType -notin $allowed) { + Warn "$childType not allowed for $($script:objType), skipping" + return + } + + $xmlTag = $script:childTypeToXmlTag[$childType] + if (-not $xmlTag) { + Warn "No XML tag mapping for $childType" + return + } + + Ensure-ChildObjectsOpen + $indent = Get-ChildIndent $script:childObjectsEl + $existingNames = Get-AllChildNames + + switch ($childType) { + "attributes" { + foreach ($item in $items) { + $parsed = Parse-AttributeShorthand $item + if ($existingNames.ContainsKey($parsed.name)) { + Warn "Attribute '$($parsed.name)' already exists, skipping" + continue + } + $context = Get-AttributeContext + $fragmentXml = Build-AttributeFragment $parsed $context $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Attribute" $parsed + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added attribute: $($parsed.name)" + $script:addCount++ + $existingNames[$parsed.name] = "Attribute" + } + } + "tabularSections" { + foreach ($item in $items) { + $tsName = if ($item -is [string]) { "$item" } else { "$($item.name)" } + if ($existingNames.ContainsKey($tsName)) { + Warn "TabularSection '$tsName' already exists, skipping" + continue + } + $tsDef = if ($item -is [string]) { @{ name = $item } } else { $item } + $fragmentXml = Build-TabularSectionFragment $tsDef $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "TabularSection" @{ after = ""; before = "" } + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added tabular section: $tsName" + $script:addCount++ + $existingNames[$tsName] = "TabularSection" + } + } + "dimensions" { + foreach ($item in $items) { + $parsed = Parse-AttributeShorthand $item + if ($existingNames.ContainsKey($parsed.name)) { + Warn "Dimension '$($parsed.name)' already exists, skipping" + continue + } + $fragmentXml = Build-DimensionFragment $parsed $script:objType $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Dimension" $parsed + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added dimension: $($parsed.name)" + $script:addCount++ + $existingNames[$parsed.name] = "Dimension" + } + } + "resources" { + foreach ($item in $items) { + $parsed = Parse-AttributeShorthand $item + if ($existingNames.ContainsKey($parsed.name)) { + Warn "Resource '$($parsed.name)' already exists, skipping" + continue + } + $fragmentXml = Build-ResourceFragment $parsed $script:objType $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Resource" $parsed + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added resource: $($parsed.name)" + $script:addCount++ + $existingNames[$parsed.name] = "Resource" + } + } + "enumValues" { + foreach ($item in $items) { + $parsed = Parse-EnumValueShorthand $item + if ($existingNames.ContainsKey($parsed.name)) { + Warn "EnumValue '$($parsed.name)' already exists, skipping" + continue + } + $fragmentXml = Build-EnumValueFragment $parsed $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "EnumValue" $parsed + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added enum value: $($parsed.name)" + $script:addCount++ + $existingNames[$parsed.name] = "EnumValue" + } + } + "columns" { + foreach ($item in $items) { + $colName = if ($item -is [string]) { "$item" } else { "$($item.name)" } + if ($existingNames.ContainsKey($colName)) { + Warn "Column '$colName' already exists, skipping" + continue + } + $fragmentXml = Build-ColumnFragment $item $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint "Column" @{ after = ""; before = "" } + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added column: $colName" + $script:addCount++ + $existingNames[$colName] = "Column" + } + } + { $_ -in @("forms","templates","commands") } { + $tagMap = @{ "forms" = "Form"; "templates" = "Template"; "commands" = "Command" } + $tag = $tagMap[$childType] + foreach ($item in $items) { + $itemName = if ($item -is [string]) { "$item" } else { "$($item.name)" } + if ($existingNames.ContainsKey($itemName)) { + Warn "$tag '$itemName' already exists, skipping" + continue + } + $fragmentXml = Build-SimpleChildFragment $tag $itemName $indent + $nodes = Import-Fragment $fragmentXml + $refNode = Find-InsertionPoint $tag @{ after = ""; before = "" } + foreach ($node in $nodes) { + Insert-BeforeElement $script:childObjectsEl $node $refNode $indent + } + Info "Added $($tag.ToLower()): $itemName" + $script:addCount++ + $existingNames[$itemName] = $tag + } + } + } + } +} + +# ============================================================ +# Section 11: REMOVE operations +# ============================================================ + +function Process-Remove($removeDef) { + $removeDef.PSObject.Properties | ForEach-Object { + $rawKey = $_.Name + $names = $_.Value + $childType = Resolve-ChildTypeKey $rawKey + + if (-not $childType) { + Warn "Unknown remove child type: $rawKey" + return + } + if ($childType -eq "properties") { + Warn "Cannot remove properties — use modify instead" + return + } + + $xmlTag = $script:childTypeToXmlTag[$childType] + if (-not $xmlTag -or -not $script:childObjectsEl) { + Warn "No ChildObjects or unknown tag for $childType" + return + } + + foreach ($name in $names) { + $nameStr = "$name" + $el = Find-ElementByName $script:childObjectsEl $xmlTag $nameStr + if (-not $el) { + Warn "$xmlTag '$nameStr' not found, skipping remove" + continue + } + Remove-NodeWithWhitespace $el + Info "Removed $($xmlTag.ToLower()): $nameStr" + $script:removeCount++ + } + } + + # Collapse if empty + Collapse-ChildObjectsIfEmpty +} + +# ============================================================ +# Section 12: MODIFY operations +# ============================================================ + +function Modify-Properties($propsDef) { + $propsDef.PSObject.Properties | ForEach-Object { + $propName = $_.Name + $propValue = $_.Value + + # Find the property element in Properties + $propEl = $null + foreach ($child in $script:propertiesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + $propEl = $child + break + } + } + + if (-not $propEl) { + Warn "Property '$propName' not found in Properties" + return + } + + # Complex property: Owners, RegisterRecords, BasedOn, InputByString + if ($script:complexPropertyMap.ContainsKey($propName)) { + $valuesList = @() + if ($propValue -is [array]) { + $valuesList = @($propValue | ForEach-Object { "$_" }) + } else { + $valuesList = @("$propValue" -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + Set-ComplexProperty $propName $valuesList + return + } + + # Handle boolean values + $valueStr = "$propValue" + if ($propValue -is [bool]) { + $valueStr = if ($propValue) { "true" } else { "false" } + } + + $propEl.InnerText = $valueStr + Info "Modified property: $propName = $valueStr" + $script:modifyCount++ + } +} + +function Modify-ChildElements($modifyDef, [string]$childType) { + $xmlTag = $script:childTypeToXmlTag[$childType] + if (-not $xmlTag -or -not $script:childObjectsEl) { + Warn "No ChildObjects or unknown tag for $childType" + return + } + + $modifyDef.PSObject.Properties | ForEach-Object { + $elemName = $_.Name + $changes = $_.Value + + $el = Find-ElementByName $script:childObjectsEl $xmlTag $elemName + if (-not $el) { + Warn "$xmlTag '$elemName' not found for modify" + return + } + + # Find Properties inside the element + $propsEl = $null + foreach ($gc in $el.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { + $propsEl = $gc; break + } + } + if (-not $propsEl) { + Warn "$xmlTag '$elemName': no Properties element found" + return + } + + $changes.PSObject.Properties | ForEach-Object { + $changeProp = $_.Name + $changeValue = $_.Value + + # TS child attribute operations (add/remove/modify attrs inside a TabularSection) + if ($xmlTag -eq "TabularSection" -and $changeProp -in @("add","remove","modify")) { + # Find ChildObjects inside this TS element + $tsChildObjEl = $null + foreach ($gc in $el.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "ChildObjects") { + $tsChildObjEl = $gc; break + } + } + + switch ($changeProp) { + "add" { + if (-not $tsChildObjEl) { + Warn "TS '$elemName' has no ChildObjects element, cannot add attributes" + return + } + # Ensure ChildObjects is open (not self-closing empty) + $hasTsChildElements = $false + foreach ($ch in $tsChildObjEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasTsChildElements = $true; break } + } + if (-not $hasTsChildElements) { + $tsCoIndent = Get-ChildIndent $el + $tsCloseWs = $script:xmlDoc.CreateWhitespace("`r`n$tsCoIndent") + $tsChildObjEl.AppendChild($tsCloseWs) | Out-Null + } + foreach ($attrDef in @($changeValue)) { + $parsed = Parse-AttributeShorthand $attrDef + $existing = Find-ElementByName $tsChildObjEl "Attribute" $parsed.name + if ($existing) { + Warn "Attribute '$($parsed.name)' already exists in TS '$elemName', skipping" + continue + } + $tsAttrIndent = Get-ChildIndent $tsChildObjEl + $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } + $fragmentXml = Build-AttributeFragment $parsed $tsAttrContext $tsAttrIndent + $nodes = Import-Fragment $fragmentXml + $savedCO = $script:childObjectsEl + $script:childObjectsEl = $tsChildObjEl + $refNode = Find-InsertionPoint "Attribute" $parsed + $script:childObjectsEl = $savedCO + foreach ($node in $nodes) { + Insert-BeforeElement $tsChildObjEl $node $refNode $tsAttrIndent + } + Info "Added attribute to TS '$elemName': $($parsed.name)" + $script:addCount++ + } + } + "remove" { + if (-not $tsChildObjEl) { + Warn "TS '$elemName' has no ChildObjects, cannot remove attributes" + return + } + foreach ($attrName in @($changeValue)) { + $attrEl = Find-ElementByName $tsChildObjEl "Attribute" "$attrName" + if (-not $attrEl) { + Warn "Attribute '$attrName' not found in TS '$elemName', skipping" + continue + } + Remove-NodeWithWhitespace $attrEl + Info "Removed attribute from TS '$elemName': $attrName" + $script:removeCount++ + } + } + "modify" { + if (-not $tsChildObjEl) { + Warn "TS '$elemName' has no ChildObjects, cannot modify attributes" + return + } + # Temporarily swap childObjectsEl and recurse + $savedChildObjEl = $script:childObjectsEl + $script:childObjectsEl = $tsChildObjEl + Modify-ChildElements $changeValue "attributes" + $script:childObjectsEl = $savedChildObjEl + } + } + return # Skip normal property modification + } + + switch ($changeProp) { + "name" { + # Rename + $nameEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { + $nameEl = $gc; break + } + } + if ($nameEl) { + $oldName = $nameEl.InnerText.Trim() + $newName = "$changeValue" + $nameEl.InnerText = $newName + + # Update Synonym if it was auto-generated (matches old CamelCase split) + $oldSynonym = Split-CamelCase $oldName + $synEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { + $synEl = $gc; break + } + } + if ($synEl) { + # Check if current synonym matches auto-generated from old name + $currentSyn = "" + foreach ($item in $synEl.ChildNodes) { + if ($item.NodeType -eq 'Element' -and $item.LocalName -eq "item") { + foreach ($gc in $item.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "content") { + $currentSyn = $gc.InnerText.Trim() + } + } + } + } + if ($currentSyn -eq $oldSynonym -or -not $currentSyn) { + $newSynonym = Split-CamelCase $newName + $synXml = Build-MLTextXml (Get-ChildIndent $propsEl) "Synonym" $newSynonym + $newSynNodes = Import-Fragment $synXml + if ($newSynNodes.Count -gt 0) { + $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null + Remove-NodeWithWhitespace $synEl + } + } + } + + Info "Renamed ${xmlTag}: $oldName -> $newName" + $script:modifyCount++ + } + } + "type" { + # Change type + $typeEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Type") { + $typeEl = $gc; break + } + } + $newTypeStr = "$changeValue" + $typeIndent = Get-ChildIndent $propsEl + $newTypeXml = Build-ValueTypeXml $typeIndent $newTypeStr + + $newTypeNodes = Import-Fragment $newTypeXml + if ($typeEl -and $newTypeNodes.Count -gt 0) { + $propsEl.InsertAfter($newTypeNodes[0], $typeEl) | Out-Null + Remove-NodeWithWhitespace $typeEl + } elseif ($newTypeNodes.Count -gt 0) { + # No existing Type — insert after Comment + $commentEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Comment") { + $commentEl = $gc; break + } + } + if ($commentEl) { + Insert-BeforeElement $propsEl $newTypeNodes[0] $commentEl.NextSibling $typeIndent + } + } + + # Also update FillValue if present + $fillValEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "FillValue") { + $fillValEl = $gc; break + } + } + if ($fillValEl) { + $fillIndent = Get-ChildIndent $propsEl + $newFillXml = Build-FillValueXml $fillIndent $newTypeStr + $newFillNodes = Import-Fragment $newFillXml + if ($newFillNodes.Count -gt 0) { + $propsEl.InsertAfter($newFillNodes[0], $fillValEl) | Out-Null + Remove-NodeWithWhitespace $fillValEl + } + } + + Info "Changed type of $xmlTag '$elemName': $newTypeStr" + $script:modifyCount++ + } + "synonym" { + $synEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { + $synEl = $gc; break + } + } + $synIndent = Get-ChildIndent $propsEl + $newSynXml = Build-MLTextXml $synIndent "Synonym" "$changeValue" + $newSynNodes = Import-Fragment $newSynXml + if ($synEl -and $newSynNodes.Count -gt 0) { + $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null + Remove-NodeWithWhitespace $synEl + } + Info "Changed synonym of $xmlTag '$elemName': $changeValue" + $script:modifyCount++ + } + default { + # Scalar property change (Indexing, FillChecking, Use, etc.) + $scalarEl = $null + foreach ($gc in $propsEl.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $changeProp) { + $scalarEl = $gc; break + } + } + if ($scalarEl) { + $valueStr = "$changeValue" + if ($changeValue -is [bool]) { + $valueStr = if ($changeValue) { "true" } else { "false" } + } else { + $valueStr = Normalize-EnumValue $changeProp $valueStr + } + $scalarEl.InnerText = $valueStr + Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr" + $script:modifyCount++ + } else { + Warn "$xmlTag '$elemName': property '$changeProp' not found" + } + } + } + } + } +} + +function Process-Modify($modifyDef) { + $modifyDef.PSObject.Properties | ForEach-Object { + $rawKey = $_.Name + $value = $_.Value + $childType = Resolve-ChildTypeKey $rawKey + + if (-not $childType) { + Warn "Unknown modify child type: $rawKey" + return + } + + if ($childType -eq "properties") { + Modify-Properties $value + } else { + Modify-ChildElements $value $childType + } + } +} + +# ============================================================ +# Section 12.5: Complex property helpers +# ============================================================ + +$script:complexPropertyMap = @{ + "Owners" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } + "RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } + "BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } + "InputByString" = @{ tag = "xr:Field"; attr = $null } +} + +function Find-PropertyElement([string]$propName) { + foreach ($child in $script:propertiesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + return $child + } + } + return $null +} + +function Get-ComplexPropertyValues([System.Xml.XmlElement]$propEl) { + $values = @() + foreach ($child in $propEl.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $values += $child.InnerText.Trim() + } + } + return $values +} + +function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) { + $mapEntry = $script:complexPropertyMap[$propertyName] + if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } + + $propEl = Find-PropertyElement $propertyName + if (-not $propEl) { + Warn "Property element '$propertyName' not found in Properties" + return + } + + # Get existing values to check duplicates + $existing = Get-ComplexPropertyValues $propEl + + $indent = Get-ChildIndent $script:propertiesEl + $childIndent = "$indent`t" + + # Check if element is self-closing (empty) + $isEmpty = $true + foreach ($ch in $propEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $isEmpty = $false; break } + } + + # If self-closing / empty, add closing whitespace + if ($isEmpty -and $propEl.ChildNodes.Count -eq 0) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") + $propEl.AppendChild($closeWs) | Out-Null + } + + foreach ($val in $values) { + if ($val -in $existing) { + Warn "$propertyName already contains '$val', skipping" + continue + } + $tag = $mapEntry.tag + $attrStr = $mapEntry.attr + if ($attrStr) { + $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" + } else { + $fragXml = "<$tag>$(Esc-Xml $val)" + } + $nodes = Import-Fragment $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $propEl $node $null $childIndent + } + Info "Added $propertyName item: $val" + $script:addCount++ + } +} + +function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) { + $propEl = Find-PropertyElement $propertyName + if (-not $propEl) { + Warn "Property element '$propertyName' not found in Properties" + return + } + + foreach ($val in $values) { + $found = $false + foreach ($child in @($propEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $val) { + Remove-NodeWithWhitespace $child + Info "Removed $propertyName item: $val" + $script:removeCount++ + $found = $true + break + } + } + if (-not $found) { + Warn "$propertyName item '$val' not found, skipping" + } + } + + # Collapse if empty + $hasElements = $false + foreach ($ch in $propEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } + } + if (-not $hasElements) { + while ($propEl.HasChildNodes) { + $propEl.RemoveChild($propEl.FirstChild) | Out-Null + } + } +} + +function Set-ComplexProperty([string]$propertyName, [string[]]$values) { + $mapEntry = $script:complexPropertyMap[$propertyName] + if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } + + $propEl = Find-PropertyElement $propertyName + if (-not $propEl) { + Warn "Property element '$propertyName' not found in Properties" + return + } + + $indent = Get-ChildIndent $script:propertiesEl + $childIndent = "$indent`t" + + # Remove all existing children + while ($propEl.HasChildNodes) { + $propEl.RemoveChild($propEl.FirstChild) | Out-Null + } + + if ($values.Count -eq 0) { + # Leave self-closing + Info "Cleared $propertyName" + $script:modifyCount++ + return + } + + # Add closing whitespace + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") + $propEl.AppendChild($closeWs) | Out-Null + + # Add each value + foreach ($val in $values) { + $tag = $mapEntry.tag + $attrStr = $mapEntry.attr + if ($attrStr) { + $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" + } else { + $fragXml = "<$tag>$(Esc-Xml $val)" + } + $nodes = Import-Fragment $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $propEl $node $null $childIndent + } + } + $count = $values.Count + Info "Set $propertyName`: $count items" + $script:modifyCount++ +} + +# ============================================================ +# Section 13: Main processing +# ============================================================ + +# --- Inline mode conversion --- +if ($Operation) { + $def = Convert-InlineToDefinition $Operation $Value +} +if (-not $def) { + Write-Error "No definition loaded" + exit 1 +} + +# --- Process complex property operations --- +if ($def.PSObject.Properties.Match("_complex").Count -gt 0 -and $def._complex) { + foreach ($cop in $def._complex) { + switch ($cop.action) { + "add" { Add-ComplexPropertyItem $cop.property $cop.values } + "remove" { Remove-ComplexPropertyItem $cop.property $cop.values } + "set" { Set-ComplexProperty $cop.property $cop.values } + } + } +} + +# --- Process standard operations --- +$def.PSObject.Properties | ForEach-Object { + $prop = $_ + if ($prop.Name -eq "_complex") { return } + $opKey = Resolve-OperationKey $prop.Name + if (-not $opKey) { + Warn "Unknown operation: $($prop.Name)" + return + } + + switch ($opKey) { + "add" { Process-Add $prop.Value } + "remove" { Process-Remove $prop.Value } + "modify" { Process-Modify $prop.Value } + } +} + +# ============================================================ +# Section 14: Save + validate +# ============================================================ + +# Save XML +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) # with BOM +$settings.Indent = $false # preserve original whitespace +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +# Write using XmlWriter to get proper encoding declaration +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush() +$writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() + +# Fix encoding case: utf-8 → UTF-8 (cosmetic, 1C accepts both) +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +# Remove BOM from string if present (we'll add it as bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { + $text = $text.Substring(1) +} +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +# Write with BOM +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) + +Info "Saved: $resolvedPath" + +# ============================================================ +# Section 15: Auto-validate +# ============================================================ + +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\meta-validate") "scripts\meta-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running meta-validate ---" -ForegroundColor DarkGray + & powershell.exe -NoProfile -File $validateScript -ObjectPath $resolvedPath + } else { + Write-Host "" + Write-Host "[SKIP] meta-validate not found at: $validateScript" -ForegroundColor DarkGray + } +} + +# ============================================================ +# Section 16: Summary +# ============================================================ + +Write-Host "" +Write-Host "=== meta-edit summary ===" -ForegroundColor Green +Write-Host " Object: $($script:objType).$($script:objName)" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +if ($script:warnCount -gt 0) { + Write-Host " Warnings: $($script:warnCount)" -ForegroundColor Yellow +} + +$totalChanges = $script:addCount + $script:removeCount + $script:modifyCount +if ($totalChanges -eq 0) { + Write-Host " No changes applied." -ForegroundColor Yellow +} + +exit 0 diff --git a/.claude/skills/meta-info/SKILL.md b/.claude/skills/meta-info/SKILL.md index a59fdb99..7b15ef8d 100644 --- a/.claude/skills/meta-info/SKILL.md +++ b/.claude/skills/meta-info/SKILL.md @@ -1,87 +1,87 @@ ---- -name: meta-info -description: Анализ структуры объекта метаданных 1С из XML-выгрузки — реквизиты, табличные части, формы, движения, типы. Используй для изучения структуры объектов (вместо чтения XML-файлов напрямую) и как подготовительный шаг при написании запросов и кода, работающего с объектами -argument-hint: [-Mode overview|brief|full] [-Name <элемент>] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /meta-info — Структура объекта метаданных 1С - -Читает XML объекта метаданных из выгрузки конфигурации 1С и выводит компактное описание структуры. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `ObjectPath` | Путь к XML-файлу объекта или каталогу (авто-резолв `/.xml`) | -| `Mode` | Режим: `overview` (default), `brief`, `full` | -| `Name` | Drill-down по имени элемента (реквизит, ТЧ, значение перечисления, шаблон URL, операция) | -| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | -| `OutFile` | Записать результат в файл (UTF-8 BOM) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -ObjectPath "<путь>" -``` - -## Три режима - -| Режим | Что показывает | -|---|---| -| `overview` *(default)* | Заголовок + ключевые свойства + структура без раскрытия деталей | -| `brief` | Всё одной-двумя строками: имена полей, счётчики | -| `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы | - -`-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, шаблон URL, операцию веб-сервиса). - -## Поддерживаемые типы (23) - -**Ссылочные:** Справочник, Документ, Перечисление, Бизнес-процесс, Задача, План обмена, План счетов, ПВХ, ПВР -**Регистры:** Регистр сведений, Регистр накопления, Регистр бухгалтерии, Регистр расчёта -**Сервисные:** Отчёт, Обработка, HTTP-сервис, Веб-сервис, Общий модуль, Регламентное задание, Подписка на событие -**Прочие:** Константа, Журнал документов, Определяемый тип - -## Примеры - -```powershell -# Справочник — overview -... -ObjectPath Catalogs/Валюты/Валюты.xml - -# Документ — полная сводка с колонками ТЧ, движениями, формами -... -ObjectPath Documents/АвансовыйОтчет/АвансовыйОтчет.xml -Mode full - -# Регистр сведений — краткая сводка -... -ObjectPath InformationRegisters/КурсыВалют/КурсыВалют.xml -Mode brief - -# Drill-down в ТЧ документа -... -ObjectPath Documents/АвансовыйОтчет/АвансовыйОтчет.xml -Name Товары - -# Drill-down в реквизит -... -ObjectPath Catalogs/Валюты/Валюты.xml -Name ОсновнаяВалюта - -# Общий модуль — флаги контекста и повторное использование -... -ObjectPath CommonModules/ОбщегоНазначения/ОбщегоНазначения.xml - -# HTTP-сервис — шаблоны URL и методы -... -ObjectPath HTTPServices/ExternalAPI/ExternalAPI.xml - -# HTTP-сервис — drill-down в шаблон URL -... -ObjectPath HTTPServices/ExternalAPI/ExternalAPI.xml -Name АктуальныеЗадачи - -# Веб-сервис — операции с параметрами -... -ObjectPath WebServices/EnterpriseDataUpload_1_0_1_1/EnterpriseDataUpload_1_0_1_1.xml - -# Веб-сервис — drill-down в операцию -... -ObjectPath WebServices/EnterpriseDataUpload_1_0_1_1/EnterpriseDataUpload_1_0_1_1.xml -Name TestConnection - -# Подписка на событие — full раскрывает список источников -... -ObjectPath EventSubscriptions/ПолныйРегистрацияУдаления/ПолныйРегистрацияУдаления.xml -Mode full - -# Регламентное задание -... -ObjectPath ScheduledJobs/АвтоматическоеЗакрытиеМесяца/АвтоматическоеЗакрытиеМесяца.xml - -# Определяемый тип -... -ObjectPath DefinedTypes/GLN/GLN.xml -``` +--- +name: meta-info +description: Анализ структуры объекта метаданных 1С из XML-выгрузки — реквизиты, табличные части, формы, движения, типы. Используй для изучения структуры объектов (вместо чтения XML-файлов напрямую) и как подготовительный шаг при написании запросов и кода, работающего с объектами +argument-hint: [-Mode overview|brief|full] [-Name <элемент>] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /meta-info — Структура объекта метаданных 1С + +Читает XML объекта метаданных из выгрузки конфигурации 1С и выводит компактное описание структуры. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ObjectPath` | Путь к XML-файлу объекта или каталогу (авто-резолв `/.xml`) | +| `Mode` | Режим: `overview` (default), `brief`, `full` | +| `Name` | Drill-down по имени элемента (реквизит, ТЧ, значение перечисления, шаблон URL, операция) | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -ObjectPath "<путь>" +``` + +## Три режима + +| Режим | Что показывает | +|---|---| +| `overview` *(default)* | Заголовок + ключевые свойства + структура без раскрытия деталей | +| `brief` | Всё одной-двумя строками: имена полей, счётчики | +| `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы | + +`-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, шаблон URL, операцию веб-сервиса). + +## Поддерживаемые типы (23) + +**Ссылочные:** Справочник, Документ, Перечисление, Бизнес-процесс, Задача, План обмена, План счетов, ПВХ, ПВР +**Регистры:** Регистр сведений, Регистр накопления, Регистр бухгалтерии, Регистр расчёта +**Сервисные:** Отчёт, Обработка, HTTP-сервис, Веб-сервис, Общий модуль, Регламентное задание, Подписка на событие +**Прочие:** Константа, Журнал документов, Определяемый тип + +## Примеры + +```powershell +# Справочник — overview +... -ObjectPath Catalogs/Валюты/Валюты.xml + +# Документ — полная сводка с колонками ТЧ, движениями, формами +... -ObjectPath Documents/АвансовыйОтчет/АвансовыйОтчет.xml -Mode full + +# Регистр сведений — краткая сводка +... -ObjectPath InformationRegisters/КурсыВалют/КурсыВалют.xml -Mode brief + +# Drill-down в ТЧ документа +... -ObjectPath Documents/АвансовыйОтчет/АвансовыйОтчет.xml -Name Товары + +# Drill-down в реквизит +... -ObjectPath Catalogs/Валюты/Валюты.xml -Name ОсновнаяВалюта + +# Общий модуль — флаги контекста и повторное использование +... -ObjectPath CommonModules/ОбщегоНазначения/ОбщегоНазначения.xml + +# HTTP-сервис — шаблоны URL и методы +... -ObjectPath HTTPServices/ExternalAPI/ExternalAPI.xml + +# HTTP-сервис — drill-down в шаблон URL +... -ObjectPath HTTPServices/ExternalAPI/ExternalAPI.xml -Name АктуальныеЗадачи + +# Веб-сервис — операции с параметрами +... -ObjectPath WebServices/EnterpriseDataUpload_1_0_1_1/EnterpriseDataUpload_1_0_1_1.xml + +# Веб-сервис — drill-down в операцию +... -ObjectPath WebServices/EnterpriseDataUpload_1_0_1_1/EnterpriseDataUpload_1_0_1_1.xml -Name TestConnection + +# Подписка на событие — full раскрывает список источников +... -ObjectPath EventSubscriptions/ПолныйРегистрацияУдаления/ПолныйРегистрацияУдаления.xml -Mode full + +# Регламентное задание +... -ObjectPath ScheduledJobs/АвтоматическоеЗакрытиеМесяца/АвтоматическоеЗакрытиеМесяца.xml + +# Определяемый тип +... -ObjectPath DefinedTypes/GLN/GLN.xml +``` diff --git a/.claude/skills/meta-info/scripts/meta-info.ps1 b/.claude/skills/meta-info/scripts/meta-info.ps1 index 47c3ea4f..0fe3818c 100644 --- a/.claude/skills/meta-info/scripts/meta-info.ps1 +++ b/.claude/skills/meta-info/scripts/meta-info.ps1 @@ -1,1199 +1,1199 @@ -# meta-info v1.3 — Compact summary of 1C metadata object -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath, - [ValidateSet("overview","brief","full")] - [string]$Mode = "overview", - [string]$Name, - [int]$Limit = 150, - [int]$Offset = 0, - [string]$OutFile -) - -$ErrorActionPreference = 'Stop' -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Output helper (always collect, paginate at the end) --- -$script:lines = @() -function Out([string]$text) { $script:lines += $text } - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { - $ObjectPath = Join-Path (Get-Location).Path $ObjectPath -} - -# Directory -> find XML inside or as sibling -if (Test-Path $ObjectPath -PathType Container) { - $dirName = Split-Path $ObjectPath -Leaf - $candidate = Join-Path $ObjectPath "$dirName.xml" - $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" - if (Test-Path $candidate) { - $ObjectPath = $candidate - } elseif (Test-Path $sibling) { - $ObjectPath = $sibling - } else { - $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) - if ($xmlFiles.Count -gt 0) { - $ObjectPath = $xmlFiles[0].FullName - } else { - Write-Host "[ERROR] No XML file found in directory: $ObjectPath" - exit 1 - } - } -} - -# File not found — check Dir/Name/Name.xml → Dir/Name.xml (common LLM mistake) -if (-not (Test-Path $ObjectPath)) { - $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) - $parentDir = Split-Path $ObjectPath - $parentDirName = Split-Path $parentDir -Leaf - if ($fileName -eq $parentDirName) { - $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" - if (Test-Path $candidate) { $ObjectPath = $candidate } - } -} -if (-not (Test-Path $ObjectPath)) { - Write-Host "[ERROR] File not found: $ObjectPath" - exit 1 -} - -# --- Load XML --- -[xml]$xmlDoc = Get-Content -Path $ObjectPath -Encoding UTF8 -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") -$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") - -$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) -if (-not $mdRoot) { - Write-Host "[ERROR] Not a valid 1C metadata XML file" - exit 1 -} - -# --- Detect object type --- -$typeNode = $null -$mdType = "" -foreach ($child in $mdRoot.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq "http://v8.1c.ru/8.3/MDClasses") { - $typeNode = $child - $mdType = $child.LocalName - break - } -} - -if (-not $typeNode) { - Write-Host "[ERROR] Cannot detect metadata type" - exit 1 -} - -# --- Type name maps --- -$typeNameMap = @{ - "Catalog"="Справочник"; "Document"="Документ"; "Enum"="Перечисление" - "Constant"="Константа"; "InformationRegister"="Регистр сведений" - "AccumulationRegister"="Регистр накопления"; "AccountingRegister"="Регистр бухгалтерии" - "CalculationRegister"="Регистр расчёта"; "ChartOfAccounts"="План счетов" - "ChartOfCharacteristicTypes"="План видов характеристик" - "ChartOfCalculationTypes"="План видов расчёта"; "BusinessProcess"="Бизнес-процесс" - "Task"="Задача"; "ExchangePlan"="План обмена"; "DocumentJournal"="Журнал документов" - "Report"="Отчёт"; "DataProcessor"="Обработка" - "DefinedType"="Определяемый тип"; "CommonModule"="Общий модуль" - "ScheduledJob"="Регламентное задание"; "EventSubscription"="Подписка на событие" - "HTTPService"="HTTP-сервис"; "WebService"="Веб-сервис" -} - -$refTypeMap = @{ - "CatalogRef"="СправочникСсылка"; "DocumentRef"="ДокументСсылка" - "EnumRef"="ПеречислениеСсылка"; "ChartOfAccountsRef"="ПланСчетовСсылка" - "ChartOfCharacteristicTypesRef"="ПВХСсылка"; "ChartOfCalculationTypesRef"="ПВРСсылка" - "ExchangePlanRef"="ПланОбменаСсылка"; "BusinessProcessRef"="БизнесПроцессСсылка" - "TaskRef"="ЗадачаСсылка" -} - -$regTypeMap = @{ - "AccumulationRegister"="РН"; "AccountingRegister"="РБ"; "CalculationRegister"="РР" - "InformationRegister"="РС" -} - -$periodMap = @{ - "Nonperiodical"="Непериодический"; "Day"="День"; "Month"="Месяц" - "Quarter"="Квартал"; "Year"="Год"; "Second"="Секунда" -} - -$writeModeMap = @{ - "Independent"="независимая"; "RecorderSubordinate"="подчинение регистратору" -} - -$reuseMap = @{ - "DontUse"="нет"; "DuringRequest"="на время вызова"; "DuringSession"="на время сеанса" -} - -$eventMap = @{ - "BeforeWrite"="ПередЗаписью"; "OnWrite"="ПриЗаписи"; "AfterWrite"="ПослеЗаписи" - "BeforeDelete"="ПередУдалением"; "Posting"="ОбработкаПроведения" - "UndoPosting"="ОбработкаУдаленияПроведения" - "OnReadAtServer"="ПриЧтенииНаСервере" - "FillCheckProcessing"="ОбработкаПроверкиЗаполнения" -} - -$objectTypeMap = @{ - "CatalogObject"="СправочникОбъект"; "DocumentObject"="ДокументОбъект" - "ChartOfAccountsObject"="ПланСчетовОбъект" - "ChartOfCharacteristicTypesObject"="ПВХОбъект" - "BusinessProcessObject"="БизнесПроцессОбъект"; "TaskObject"="ЗадачаОбъект" - "ExchangePlanObject"="ПланОбменаОбъект" - "InformationRegisterRecordSet"="НаборЗаписейРС" - "AccumulationRegisterRecordSet"="НаборЗаписейРН" - "AccountingRegisterRecordSet"="НаборЗаписейРБ" -} - -$numberPeriodMap = @{ - "Year"="по году"; "Quarter"="по кварталу"; "Month"="по месяцу"; "Day"="по дню" - "WholeCatalog"="сквозная" -} - -$ruTypeName = if ($typeNameMap.ContainsKey($mdType)) { $typeNameMap[$mdType] } else { $mdType } - -# --- Helpers --- - -function Get-MLText($node) { - if (-not $node) { return "" } - $c = $node.SelectSingleNode("v8:item[v8:lang='ru']/v8:content", $ns) - if ($c) { return $c.InnerText } - $c = $node.SelectSingleNode("v8:item/v8:content", $ns) - if ($c) { return $c.InnerText } - $text = $node.InnerText.Trim() - if ($text) { return $text } - return "" -} - -function Format-Type($typeNode) { - if (-not $typeNode) { return "" } - $types = @() - foreach ($t in $typeNode.SelectNodes("v8:Type", $ns)) { - $raw = $t.InnerText - $types += Format-SingleType $raw $typeNode - } - foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) { - $raw = $t.InnerText - if ($raw -match '^cfg:DefinedType\.(.+)$') { - $types += "ОпределяемыйТип.$($Matches[1])" - } elseif ($raw -match '^cfg:Characteristic\.(.+)$') { - $types += "Характеристика.$($Matches[1])" - } else { - $types += $raw - } - } - if ($types.Count -eq 0) { return "" } - if ($types.Count -eq 1) { return $types[0] } - return ($types -join " | ") -} - -function Format-SingleType([string]$raw, $parentNode) { - switch -Wildcard ($raw) { - "xs:string" { - $sq = $parentNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) - $len = if ($sq) { $sq.InnerText } else { "" } - if ($len) { return "Строка($len)" } else { return "Строка" } - } - "xs:decimal" { - $dg = $parentNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) - $fr = $parentNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) - $d = if ($dg) { $dg.InnerText } else { "" } - $f = if ($fr) { $fr.InnerText } else { "0" } - if ($d) { return "Число($d,$f)" } else { return "Число" } - } - "xs:boolean" { return "Булево" } - "xs:dateTime" { - $dq = $parentNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) - if ($dq) { - switch ($dq.InnerText) { - "Date" { return "Дата" } - "Time" { return "Время" } - "DateTime" { return "ДатаВремя" } - default { return "Дата" } - } - } - return "ДатаВремя" - } - "v8:ValueStorage" { return "ХранилищеЗначения" } - "v8:UUID" { return "УникальныйИдентификатор" } - "v8:Null" { return "Null" } - default { - # Normalize d5p1:/dNpN: -> cfg: - $raw = $raw -replace '^d\d+p\d+:', 'cfg:' - # cfg:CatalogRef.Xxx -> СправочникСсылка.Xxx - if ($raw -match '^cfg:(\w+)Ref\.(.+)$') { - $prefix = "$($Matches[1])Ref" - $objn = $Matches[2] - if ($refTypeMap.ContainsKey($prefix)) { - return "$($refTypeMap[$prefix]).$objn" - } - } - # cfg:EnumRef.Xxx - if ($raw -match '^cfg:EnumRef\.(.+)$') { - return "ПеречислениеСсылка.$($Matches[1])" - } - # cfg:Characteristic.Xxx - if ($raw -match '^cfg:Characteristic\.(.+)$') { - return "Характеристика.$($Matches[1])" - } - # cfg:DefinedType.Xxx - if ($raw -match '^cfg:DefinedType\.(.+)$') { - return "ОпределяемыйТип.$($Matches[1])" - } - # Strip cfg: prefix for unknown - if ($raw -match '^cfg:(.+)$') { - return $Matches[1] - } - return $raw - } - } -} - -function Format-Flags($propsNode, [bool]$isDimension = $false) { - $flags = @() - $fc = $propsNode.SelectSingleNode("md:FillChecking", $ns) - if ($fc -and $fc.InnerText -eq "ShowError") { $flags += "обязательный" } - - $idx = $propsNode.SelectSingleNode("md:Indexing", $ns) - if ($idx) { - switch ($idx.InnerText) { - "Index" { $flags += "индекс" } - "IndexWithAdditionalOrder" { $flags += "индекс+доп" } - } - } - - if ($isDimension) { - $master = $propsNode.SelectSingleNode("md:Master", $ns) - if ($master -and $master.InnerText -eq "true") { $flags += "ведущее" } - } - - $ml = $propsNode.SelectSingleNode("md:MultiLine", $ns) - if ($ml -and $ml.InnerText -eq "true") { $flags += "многострочный" } - - $use = $propsNode.SelectSingleNode("md:Use", $ns) - if ($use) { - switch ($use.InnerText) { - "ForFolder" { $flags += "для папок" } - "ForFolderAndItem" { $flags += "для папок и элементов" } - } - } - - if ($flags.Count -eq 0) { return "" } - return " [$($flags -join ', ')]" -} - -function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) { - $result = @() - foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) { - $aprops = $attr.SelectSingleNode("md:Properties", $ns) - if (-not $aprops) { continue } - $attrName = $aprops.SelectSingleNode("md:Name", $ns).InnerText - $typeStr = Format-Type $aprops.SelectSingleNode("md:Type", $ns) - $aflags = Format-Flags $aprops $isDimension - $result += @{ Name=$attrName; Type=$typeStr; Flags=$aflags; Props=$aprops } - } - return $result -} - -function Get-TabularSections($parentNode) { - $result = @() - foreach ($ts in $parentNode.SelectNodes("md:TabularSection", $ns)) { - $tprops = $ts.SelectSingleNode("md:Properties", $ns) - $tsName = $tprops.SelectSingleNode("md:Name", $ns).InnerText - $tchildObjs = $ts.SelectSingleNode("md:ChildObjects", $ns) - $cols = @() - if ($tchildObjs) { $cols = @(Get-Attributes $tchildObjs) } - $result += @{ Name=$tsName; Columns=$cols; ColCount=$cols.Count } - } - return $result -} - -function Format-AttrLine([hashtable]$attr, [int]$maxNameLen = 30) { - $padded = $attr.Name.PadRight($maxNameLen) - return " $padded $($attr.Type)$($attr.Flags)" -} - -function Get-MaxNameLen($attrs) { - $max = 10 - foreach ($a in $attrs) { - if ($a.Name.Length -gt $max) { $max = $a.Name.Length } - } - return [Math]::Min($max + 2, 40) -} - -function Get-SimpleChildren($parentNode, [string]$tag) { - $result = @() - foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) { - $result += $child.InnerText - } - return $result -} - -function Sort-AttrsRefFirst($attrs) { - $refs = @() - $prims = @() - foreach ($a in $attrs) { - $t = $a.Type - if ($t -match 'Ссылка\.' -or $t -match 'Характеристика\.' -or $t -match 'ОпределяемыйТип\.' -or $t -match 'ПланСчетовСсылка' -or $t -match 'ПВХСсылка' -or $t -match 'ПВРСсылка') { - $refs += $a - } else { - $prims += $a - } - } - return @($refs) + @($prims) -} - -function Decline-Cols([int]$n) { - $m = $n % 10 - $h = $n % 100 - if ($h -ge 11 -and $h -le 19) { return "колонок" } - if ($m -eq 1) { return "колонка" } - if ($m -ge 2 -and $m -le 4) { return "колонки" } - return "колонок" -} - -function Format-SourceType([string]$raw) { - $raw = $raw -replace '^d\d+p\d+:', 'cfg:' - if ($raw -match '^cfg:(\w+)\.(.+)$') { - $prefix = $Matches[1]; $name = $Matches[2] - if ($objectTypeMap.ContainsKey($prefix)) { return "$($objectTypeMap[$prefix]).$name" } - } - if ($raw -match '^cfg:(.+)$') { return $Matches[1] } - return $raw -} - -function Get-HTTPEndpoints($childObjs) { - $result = @() - foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) { - $tp = $tpl.SelectSingleNode("md:Properties", $ns) - $tplName = $tp.SelectSingleNode("md:Name", $ns).InnerText - $template = $tp.SelectSingleNode("md:Template", $ns).InnerText - $methods = @() - $tplCO = $tpl.SelectSingleNode("md:ChildObjects", $ns) - if ($tplCO) { - foreach ($m in $tplCO.SelectNodes("md:Method", $ns)) { - $mp = $m.SelectSingleNode("md:Properties", $ns) - $httpMethod = $mp.SelectSingleNode("md:HTTPMethod", $ns).InnerText - $handler = $mp.SelectSingleNode("md:Handler", $ns).InnerText - $methods += @{ HTTPMethod=$httpMethod; Handler=$handler; Name=$mp.SelectSingleNode("md:Name",$ns).InnerText } - } - } - $result += @{ Name=$tplName; Template=$template; Methods=$methods } - } - return $result -} - -function Get-WSOperations($childObjs) { - $result = @() - foreach ($op in $childObjs.SelectNodes("md:Operation", $ns)) { - $oprops = $op.SelectSingleNode("md:Properties", $ns) - $opName = $oprops.SelectSingleNode("md:Name", $ns).InnerText - $retType = $oprops.SelectSingleNode("md:XDTOReturningValueType", $ns) - $retStr = if ($retType -and $retType.InnerText) { $retType.InnerText } else { "void" } - $procName = $oprops.SelectSingleNode("md:ProcedureName", $ns) - $params = @() - $opCO = $op.SelectSingleNode("md:ChildObjects", $ns) - if ($opCO) { - foreach ($p in $opCO.SelectNodes("md:Parameter", $ns)) { - $pp = $p.SelectSingleNode("md:Properties", $ns) - $pName = $pp.SelectSingleNode("md:Name", $ns).InnerText - $pType = $pp.SelectSingleNode("md:XDTOValueType", $ns) - $pTypeStr = if ($pType) { $pType.InnerText } else { "?" } - $dir = $pp.SelectSingleNode("md:TransferDirection", $ns) - $dirStr = if ($dir -and $dir.InnerText -ne "In") { " [$($dir.InnerText.ToLower())]" } else { "" } - $params += "${pName}: ${pTypeStr}${dirStr}" - } - } - $paramStr = $params -join ", " - $result += @{ Name=$opName; Params=$paramStr; ReturnType=$retStr; ProcName=$(if ($procName) { $procName.InnerText } else { "" }) } - } - return $result -} - -# --- Support status of this object (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the -# object's support rule. Never throws — degrades to "не на поддержке". -function Get-ObjectSupportStatus([string]$objUuid) { - try { - # Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin). - $d = [System.IO.Path]::GetDirectoryName($ObjectPath) - $binPath = $null - for ($i = 0; $i -lt 8 -and $d; $i++) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return "не на поддержке" } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return "снято с поддержки (правки свободны)" } - if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } - if (-not $objUuid) { return "не на поддержке" } - # Conservative fold: locked if f1=0 in ANY vendor block. - $u = [regex]::Escape($objUuid.ToLower()) - $best = $null - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - if ($null -eq $best) { return "не на поддержке" } - switch ($best) { - 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } - 1 { return "редактируется с сохранением поддержки" } - 2 { return "снято с поддержки (правки свободны)" } - } - return "не на поддержке" - } catch { return "не на поддержке" } -} - -# --- Extract metadata --- -$props = $typeNode.SelectSingleNode("md:Properties", $ns) -$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) -$objName = $props.SelectSingleNode("md:Name", $ns).InnerText -$synNode = $props.SelectSingleNode("md:Synonym", $ns) -$synonym = Get-MLText $synNode - -# Presentations (type-choice dialogs show "Представление объекта" as the ref type name) -$objPresentation = Get-MLText $props.SelectSingleNode("md:ObjectPresentation", $ns) -$extObjPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedObjectPresentation", $ns) -$listPresentation = Get-MLText $props.SelectSingleNode("md:ListPresentation", $ns) -$extListPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedListPresentation", $ns) - -# Reference (ref-typed) metadata objects — those with a ...Ref type -$refMdTypes = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes", - "ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task") -$isRefObject = $refMdTypes -contains $mdType - -# Effective type presentation: ObjectPresentation -> Synonym -> Name -$typePresentation = if ($objPresentation) { $objPresentation } - elseif ($synonym) { $synonym } - else { $objName } - -# --- Handle -Name drill-down --- -$drillDone = $false -if ($Name -and $childObjs) { - # Search in attributes/dimensions/resources - $attrTags = @("Attribute","Dimension","Resource") - foreach ($tag in $attrTags) { - if ($drillDone) { break } - foreach ($attr in $childObjs.SelectNodes("md:$tag", $ns)) { - $ap = $attr.SelectSingleNode("md:Properties", $ns) - if (-not $ap) { continue } - $an = $ap.SelectSingleNode("md:Name", $ns).InnerText - if ($an -eq $Name) { - $tagRu = switch ($tag) { - "Attribute" { "Реквизит" } - "Dimension" { "Измерение" } - "Resource" { "Ресурс" } - } - Out "$($tagRu): $an" - $typeStr = Format-Type $ap.SelectSingleNode("md:Type", $ns) - Out " Тип: $typeStr" - $fc = $ap.SelectSingleNode("md:FillChecking", $ns) - Out " Обязательный: $(if ($fc -and $fc.InnerText -eq 'ShowError') { 'да' } else { 'нет' })" - $idx = $ap.SelectSingleNode("md:Indexing", $ns) - $idxRu = if (-not $idx -or $idx.InnerText -eq 'DontIndex') { 'нет' } - elseif ($idx.InnerText -eq 'Index') { 'Индекс' } - elseif ($idx.InnerText -eq 'IndexWithAdditionalOrder') { 'Индекс с доп. упорядочиванием' } - else { $idx.InnerText } - Out " Индексирование: $idxRu" - $ml = $ap.SelectSingleNode("md:MultiLine", $ns) - if ($ml -and $ml.InnerText -eq "true") { Out " Многострочный: да" } - $use = $ap.SelectSingleNode("md:Use", $ns) - if ($use -and $use.InnerText -ne "ForItem") { - $useRu = switch ($use.InnerText) { - "ForFolder" { "для папок" } - "ForFolderAndItem" { "для папок и элементов" } - default { $use.InnerText } - } - Out " Использование: $useRu" - } - $fv = $ap.SelectSingleNode("md:FillValue", $ns) - if ($fv -and -not ($fv.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance") -eq "true") -and $fv.InnerText) { - $fvText = $fv.InnerText - if ($fvText -match '\.EmptyRef$') { $fvText = "Пустая ссылка" } - elseif ($fvText -eq "false") { $fvText = "Ложь" } - elseif ($fvText -eq "true") { $fvText = "Истина" } - Out " Значение заполнения: $fvText" - } else { - Out " Значение заполнения: —" - } - if ($tag -eq "Dimension") { - $master = $ap.SelectSingleNode("md:Master", $ns) - Out " Ведущее: $(if ($master -and $master.InnerText -eq 'true') { 'да' } else { 'нет' })" - $mf = $ap.SelectSingleNode("md:MainFilter", $ns) - Out " Основной отбор: $(if ($mf -and $mf.InnerText -eq 'true') { 'да' } else { 'нет' })" - } - $synA = $ap.SelectSingleNode("md:Synonym", $ns) - $synText = Get-MLText $synA - if ($synText -and $synText -ne $an) { Out " Синоним: $synText" } - $drillDone = $true - break - } - } - } - - # Search in tabular sections - if (-not $drillDone) { - foreach ($ts in $childObjs.SelectNodes("md:TabularSection", $ns)) { - $tp = $ts.SelectSingleNode("md:Properties", $ns) - $tn = $tp.SelectSingleNode("md:Name", $ns).InnerText - if ($tn -eq $Name) { - $tsCO = $ts.SelectSingleNode("md:ChildObjects", $ns) - $cols = @() - if ($tsCO) { $cols = @(Get-Attributes $tsCO) } - Out "ТЧ: $tn ($($cols.Count) $(Decline-Cols $cols.Count)):" - if ($cols.Count -gt 0) { - $ml = Get-MaxNameLen $cols - foreach ($c in $cols) { Out (Format-AttrLine $c $ml) } - } - $drillDone = $true - break - } - } - } - - # Search in enum values - if (-not $drillDone) { - foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { - $ep = $ev.SelectSingleNode("md:Properties", $ns) - $en = $ep.SelectSingleNode("md:Name", $ns).InnerText - if ($en -eq $Name) { - $synE = $ep.SelectSingleNode("md:Synonym", $ns) - $synText = Get-MLText $synE - Out "Значение перечисления: $en" - if ($synText) { Out " Синоним: `"$synText`"" } - $cm = $ep.SelectSingleNode("md:Comment", $ns) - if ($cm -and $cm.InnerText) { Out " Комментарий: $($cm.InnerText)" } - $drillDone = $true - break - } - } - } - - # Search in HTTPService URLTemplates - if (-not $drillDone -and $mdType -eq "HTTPService" -and $childObjs) { - foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) { - $tp = $tpl.SelectSingleNode("md:Properties", $ns) - if ($tp.SelectSingleNode("md:Name", $ns).InnerText -eq $Name) { - $template = $tp.SelectSingleNode("md:Template", $ns).InnerText - Out "Шаблон URL: $Name" - Out " Путь: $template" - $tplCO = $tpl.SelectSingleNode("md:ChildObjects", $ns) - if ($tplCO) { - foreach ($m in $tplCO.SelectNodes("md:Method", $ns)) { - $mp = $m.SelectSingleNode("md:Properties", $ns) - $httpMethod = $mp.SelectSingleNode("md:HTTPMethod", $ns).InnerText - $handler = $mp.SelectSingleNode("md:Handler", $ns).InnerText - Out " $httpMethod → $handler" - } - } - $drillDone = $true; break - } - } - } - - # Search in WebService Operations - if (-not $drillDone -and $mdType -eq "WebService" -and $childObjs) { - foreach ($op in $childObjs.SelectNodes("md:Operation", $ns)) { - $oprops = $op.SelectSingleNode("md:Properties", $ns) - if ($oprops.SelectSingleNode("md:Name", $ns).InnerText -eq $Name) { - Out "Операция: $Name" - $retType = $oprops.SelectSingleNode("md:XDTOReturningValueType", $ns) - Out " Возвращает: $(if ($retType -and $retType.InnerText) { $retType.InnerText } else { 'void' })" - $procName = $oprops.SelectSingleNode("md:ProcedureName", $ns) - if ($procName -and $procName.InnerText) { Out " Процедура: $($procName.InnerText)" } - $comment = $oprops.SelectSingleNode("md:Comment", $ns) - if ($comment -and $comment.InnerText) { Out " Комментарий: $($comment.InnerText)" } - $opCO = $op.SelectSingleNode("md:ChildObjects", $ns) - if ($opCO) { - $params = $opCO.SelectNodes("md:Parameter", $ns) - if ($params.Count -gt 0) { - Out " Параметры:" - foreach ($p in $params) { - $pp = $p.SelectSingleNode("md:Properties", $ns) - $pName = $pp.SelectSingleNode("md:Name", $ns).InnerText - $pType = $pp.SelectSingleNode("md:XDTOValueType", $ns) - $dir = $pp.SelectSingleNode("md:TransferDirection", $ns) - $dirStr = if ($dir -and $dir.InnerText -ne "In") { " [$($dir.InnerText.ToLower())]" } else { "" } - Out " ${pName}: $(if ($pType) { $pType.InnerText } else { '?' })${dirStr}" - } - } - } - $drillDone = $true; break - } - } - } - - if (-not $drillDone) { - Write-Host "[ERROR] '$Name' not found in $objName" - exit 1 - } -} - -# --- Main output (not drill-down) --- -if (-not $drillDone) { - - # --- Build header --- - $header = "=== $ruTypeName`: $objName" - if ($synonym -and $synonym -ne $objName) { $header += " — `"$synonym`"" } - $header += " ===" - Out $header - Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))" - - # --- Type presentation (ref objects) --- - if ($isRefObject) { - Out "Представление типа: $typePresentation" - if ($Mode -eq "full") { - if ($objPresentation) { Out "Представление объекта: $objPresentation" } - if ($extObjPresentation) { Out "Расширенное представление объекта: $extObjPresentation" } - if ($listPresentation) { Out "Представление списка: $listPresentation" } - if ($extListPresentation) { Out "Расширенное представление списка: $extListPresentation" } - } - } - - # --- Mode: brief --- - if ($Mode -eq "brief") { - # Attributes - $attrs = @() - if ($childObjs) { $attrs = @(Get-Attributes $childObjs) } - if ($attrs.Count -gt 0) { - $names = ($attrs | ForEach-Object { $_.Name }) -join ", " - Out "Реквизиты ($($attrs.Count)): $names" - } - - # Dimensions/Resources for registers - if ($mdType -match "Register$") { - $dims = @() - if ($childObjs) { $dims = @(Get-Attributes $childObjs "Dimension" $true) } - if ($dims.Count -gt 0) { - $names = ($dims | ForEach-Object { $_.Name }) -join ", " - Out "Измерения ($($dims.Count)): $names" - } - $res = @() - if ($childObjs) { $res = @(Get-Attributes $childObjs "Resource") } - if ($res.Count -gt 0) { - $names = ($res | ForEach-Object { $_.Name }) -join ", " - Out "Ресурсы ($($res.Count)): $names" - } - } - - # Tabular sections - $tss = @() - if ($childObjs) { $tss = @(Get-TabularSections $childObjs) } - if ($tss.Count -gt 0) { - $tsParts = $tss | ForEach-Object { "$($_.Name)($($_.ColCount))" } - Out "ТЧ ($($tss.Count)): $($tsParts -join ', ')" - } - - # Enum values - if ($mdType -eq "Enum") { - $vals = @() - if ($childObjs) { - foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { - $ep = $ev.SelectSingleNode("md:Properties", $ns) - $vals += $ep.SelectSingleNode("md:Name", $ns).InnerText - } - } - if ($vals.Count -gt 0) { - Out "Значения ($($vals.Count)): $($vals -join ', ')" - } - } - - # DefinedType brief - if ($mdType -eq "DefinedType") { - $typeNode2 = $props.SelectSingleNode("md:Type", $ns) - if ($typeNode2) { - $types = @() - foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { - $types += Format-SingleType $t.InnerText $typeNode2 - } - if ($types.Count -gt 0) { - Out "Типы ($($types.Count)): $($types -join ', ')" - } - } - } - - # CommonModule brief (same as overview — already compact) - if ($mdType -eq "CommonModule") { - $flags = @() - if ($props.SelectSingleNode("md:Global", $ns).InnerText -eq "true") { $flags += "Глобальный" } - if ($props.SelectSingleNode("md:Server", $ns).InnerText -eq "true") { $flags += "Сервер" } - if ($props.SelectSingleNode("md:ServerCall", $ns).InnerText -eq "true") { $flags += "Вызов сервера" } - if ($props.SelectSingleNode("md:ClientManagedApplication", $ns).InnerText -eq "true") { $flags += "Клиент управляемое" } - if ($props.SelectSingleNode("md:ClientOrdinaryApplication", $ns).InnerText -eq "true") { $flags += "Обычный клиент" } - if ($props.SelectSingleNode("md:ExternalConnection", $ns).InnerText -eq "true") { $flags += "Внешнее соединение" } - if ($props.SelectSingleNode("md:Privileged", $ns).InnerText -eq "true") { $flags += "Привилегированный" } - $reuse = $props.SelectSingleNode("md:ReturnValuesReuse", $ns) - if ($reuse -and $reuse.InnerText -ne "DontUse") { - $reuseRu = if ($reuseMap.ContainsKey($reuse.InnerText)) { $reuseMap[$reuse.InnerText] } else { $reuse.InnerText } - $flags += "Повторное использование: $reuseRu" - } - if ($flags.Count -gt 0) { Out ($flags -join " | ") } - } - - # ScheduledJob brief (same as overview — already compact) - if ($mdType -eq "ScheduledJob") { - $method = $props.SelectSingleNode("md:MethodName", $ns) - if ($method -and $method.InnerText) { - $mName = $method.InnerText - if ($mName -match '^CommonModule\.(.+)$') { $mName = $Matches[1] } - Out "Метод: $mName" - } - $sjParts = @() - $use = $props.SelectSingleNode("md:Use", $ns) - $sjParts += "Использование: $(if ($use -and $use.InnerText -eq 'true') { 'да' } else { 'нет' })" - $predef = $props.SelectSingleNode("md:Predefined", $ns) - $sjParts += "Предопределённое: $(if ($predef -and $predef.InnerText -eq 'true') { 'да' } else { 'нет' })" - $restartCnt = $props.SelectSingleNode("md:RestartCountOnFailure", $ns) - $restartInt = $props.SelectSingleNode("md:RestartIntervalOnFailure", $ns) - if ($restartCnt -and [int]$restartCnt.InnerText -gt 0) { - $sjParts += "Перезапуск: $($restartCnt.InnerText) (через $($restartInt.InnerText) сек)" - } - Out ($sjParts -join " | ") - } - - # EventSubscription brief - if ($mdType -eq "EventSubscription") { - $esParts = @() - $event = $props.SelectSingleNode("md:Event", $ns) - if ($event -and $event.InnerText) { - $evRu = if ($eventMap.ContainsKey($event.InnerText)) { $eventMap[$event.InnerText] } else { $event.InnerText } - $esParts += "Событие: $evRu" - } - $handler = $props.SelectSingleNode("md:Handler", $ns) - if ($handler -and $handler.InnerText) { - $hName = $handler.InnerText - if ($hName -match '^CommonModule\.(.+)$') { $hName = $Matches[1] } - $esParts += "Обработчик: $hName" - } - $source = $props.SelectSingleNode("md:Source", $ns) - if ($source) { - $srcCount = $source.SelectNodes("v8:Type", $ns).Count - if ($srcCount -gt 0) { $esParts += "Источники: $srcCount" } - } - if ($esParts.Count -gt 0) { Out ($esParts -join " | ") } - } - - # HTTPService brief - if ($mdType -eq "HTTPService") { - $rootURL = $props.SelectSingleNode("md:RootURL", $ns) - if ($rootURL -and $rootURL.InnerText) { Out "Корневой URL: /$($rootURL.InnerText)" } - if ($childObjs) { - $endpoints = @(Get-HTTPEndpoints $childObjs) - if ($endpoints.Count -gt 0) { - $totalMethods = ($endpoints | ForEach-Object { $_.Methods.Count } | Measure-Object -Sum).Sum - Out "Шаблоны: $($endpoints.Count) | Методы: $totalMethods" - } - } - } - - # WebService brief - if ($mdType -eq "WebService") { - $nsUrl = $props.SelectSingleNode("md:Namespace", $ns) - if ($nsUrl -and $nsUrl.InnerText) { Out "Пространство имён: $($nsUrl.InnerText)" } - if ($childObjs) { - $ops = @(Get-WSOperations $childObjs) - if ($ops.Count -gt 0) { Out "Операции: $($ops.Count)" } - } - } - } else { - # --- Mode: overview / full --- - - # Document-specific header properties - if ($mdType -eq "Document") { - $numType = $props.SelectSingleNode("md:NumberType", $ns) - $numLen = $props.SelectSingleNode("md:NumberLength", $ns) - $numPer = $props.SelectSingleNode("md:NumberPeriodicity", $ns) - $autoNum = $props.SelectSingleNode("md:Autonumbering", $ns) - $posting = $props.SelectSingleNode("md:Posting", $ns) - - $parts = @() - if ($numType -and $numLen) { - $nt = if ($numType.InnerText -eq "String") { "Строка" } else { "Число" } - $piece = "Номер: $nt($($numLen.InnerText))" - if ($numPer) { - $perRu = if ($numberPeriodMap.ContainsKey($numPer.InnerText)) { $numberPeriodMap[$numPer.InnerText] } else { $numPer.InnerText } - $piece += ", $perRu" - } - if ($autoNum -and $autoNum.InnerText -eq "true") { $piece += ", авто" } - $parts += $piece - } - if ($posting) { - $parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })" - } - if ($parts.Count -gt 0) { Out ($parts -join " | ") } - } - - # Catalog-specific header properties - if ($mdType -eq "Catalog") { - $parts = @() - $hier = $props.SelectSingleNode("md:Hierarchical", $ns) - if ($hier -and $hier.InnerText -eq "true") { - $ht = $props.SelectSingleNode("md:HierarchyType", $ns) - $htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" } - $limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns) - $levelNode = $props.SelectSingleNode("md:LevelCount", $ns) - if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) { - $htText += ", уровней: $($levelNode.InnerText)" - } else { - $htText += ", без ограничения уровней" - } - $parts += "Иерархический: $htText" - } - $codeLen = $props.SelectSingleNode("md:CodeLength", $ns) - $descLen = $props.SelectSingleNode("md:DescriptionLength", $ns) - if ($codeLen -and [int]$codeLen.InnerText -gt 0) { $parts += "Код($($codeLen.InnerText))" } - if ($descLen -and [int]$descLen.InnerText -gt 0) { $parts += "Наименование($($descLen.InnerText))" } - if ($parts.Count -gt 0) { Out ($parts -join " | ") } - } - - # Register-specific header properties - if ($mdType -match "Register$") { - $parts = @() - if ($mdType -eq "InformationRegister") { - $per = $props.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) - if ($per) { - $perRu = if ($periodMap.ContainsKey($per.InnerText)) { $periodMap[$per.InnerText] } else { $per.InnerText } - $parts += "Периодичность: $perRu" - } - $wm = $props.SelectSingleNode("md:WriteMode", $ns) - if ($wm) { - $wmRu = if ($writeModeMap.ContainsKey($wm.InnerText)) { $writeModeMap[$wm.InnerText] } else { $wm.InnerText } - $parts += "Запись: $wmRu" - } - } - if ($mdType -eq "AccumulationRegister") { - $regKind = $props.SelectSingleNode("md:RegisterType", $ns) - if ($regKind) { - $rkRu = switch ($regKind.InnerText) { - "Balances" { "остатки" } - "Turnovers" { "обороты" } - default { $regKind.InnerText } - } - $parts += "Вид: $rkRu" - } - } - if ($parts.Count -gt 0) { Out ($parts -join " | ") } - } - - # Constant-specific: show type - if ($mdType -eq "Constant") { - $typeStr = Format-Type $props.SelectSingleNode("md:Type", $ns) - if ($typeStr) { Out "Тип: $typeStr" } - } - - # Report-specific: MainDataCompositionSchema - if ($mdType -eq "Report") { - $mainDCS = $props.SelectSingleNode("md:MainDataCompositionSchema", $ns) - if ($mainDCS -and $mainDCS.InnerText) { - $dcsName = $mainDCS.InnerText - if ($dcsName -match '\.Template\.(.+)$') { $dcsName = $Matches[1] } - Out "Основная СКД: $dcsName" - } - } - - # DefinedType: show types - if ($mdType -eq "DefinedType") { - $typeNode2 = $props.SelectSingleNode("md:Type", $ns) - if ($typeNode2) { - $types = @() - foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { - $types += Format-SingleType $t.InnerText $typeNode2 - } - if ($types.Count -gt 0) { - Out "Типы ($($types.Count)):" - foreach ($t in $types) { Out " $t" } - } - } - } - - # CommonModule: show flags - if ($mdType -eq "CommonModule") { - $flags = @() - if ($props.SelectSingleNode("md:Global", $ns).InnerText -eq "true") { $flags += "Глобальный" } - if ($props.SelectSingleNode("md:Server", $ns).InnerText -eq "true") { $flags += "Сервер" } - if ($props.SelectSingleNode("md:ServerCall", $ns).InnerText -eq "true") { $flags += "Вызов сервера" } - if ($props.SelectSingleNode("md:ClientManagedApplication", $ns).InnerText -eq "true") { $flags += "Клиент управляемое" } - if ($props.SelectSingleNode("md:ClientOrdinaryApplication", $ns).InnerText -eq "true") { $flags += "Обычный клиент" } - if ($props.SelectSingleNode("md:ExternalConnection", $ns).InnerText -eq "true") { $flags += "Внешнее соединение" } - if ($props.SelectSingleNode("md:Privileged", $ns).InnerText -eq "true") { $flags += "Привилегированный" } - $reuse = $props.SelectSingleNode("md:ReturnValuesReuse", $ns) - if ($reuse -and $reuse.InnerText -ne "DontUse") { - $reuseRu = if ($reuseMap.ContainsKey($reuse.InnerText)) { $reuseMap[$reuse.InnerText] } else { $reuse.InnerText } - $flags += "Повторное использование: $reuseRu" - } - if ($flags.Count -gt 0) { Out ($flags -join " | ") } - } - - # ScheduledJob: show method and flags - if ($mdType -eq "ScheduledJob") { - $method = $props.SelectSingleNode("md:MethodName", $ns) - if ($method -and $method.InnerText) { - $mName = $method.InnerText - if ($mName -match '^CommonModule\.(.+)$') { $mName = $Matches[1] } - Out "Метод: $mName" - } - $sjParts = @() - $use = $props.SelectSingleNode("md:Use", $ns) - $sjParts += "Использование: $(if ($use -and $use.InnerText -eq 'true') { 'да' } else { 'нет' })" - $predef = $props.SelectSingleNode("md:Predefined", $ns) - $sjParts += "Предопределённое: $(if ($predef -and $predef.InnerText -eq 'true') { 'да' } else { 'нет' })" - $restartCnt = $props.SelectSingleNode("md:RestartCountOnFailure", $ns) - $restartInt = $props.SelectSingleNode("md:RestartIntervalOnFailure", $ns) - if ($restartCnt -and [int]$restartCnt.InnerText -gt 0) { - $sjParts += "Перезапуск: $($restartCnt.InnerText) (через $($restartInt.InnerText) сек)" - } - Out ($sjParts -join " | ") - } - - # EventSubscription: show event, handler, sources - if ($mdType -eq "EventSubscription") { - $event = $props.SelectSingleNode("md:Event", $ns) - if ($event -and $event.InnerText) { - $evRu = if ($eventMap.ContainsKey($event.InnerText)) { $eventMap[$event.InnerText] } else { $event.InnerText } - Out "Событие: $evRu" - } - $handler = $props.SelectSingleNode("md:Handler", $ns) - if ($handler -and $handler.InnerText) { - $hName = $handler.InnerText - if ($hName -match '^CommonModule\.(.+)$') { $hName = $Matches[1] } - Out "Обработчик: $hName" - } - $source = $props.SelectSingleNode("md:Source", $ns) - if ($source) { - $srcTypes = @() - foreach ($t in $source.SelectNodes("v8:Type", $ns)) { - $srcTypes += Format-SourceType $t.InnerText - } - if ($srcTypes.Count -gt 0) { - if ($Mode -eq "full") { - Out "Источники ($($srcTypes.Count)):" - foreach ($s in $srcTypes) { Out " $s" } - } else { - Out "Источники ($($srcTypes.Count))" - } - } - } - } - - # HTTPService: show root URL and endpoints - if ($mdType -eq "HTTPService") { - $rootURL = $props.SelectSingleNode("md:RootURL", $ns) - if ($rootURL -and $rootURL.InnerText) { Out "Корневой URL: /$($rootURL.InnerText)" } - if ($childObjs) { - $endpoints = @(Get-HTTPEndpoints $childObjs) - if ($endpoints.Count -gt 0) { - Out "" - Out "Шаблоны URL ($($endpoints.Count)):" - foreach ($ep in $endpoints) { - Out " $($ep.Template)" - foreach ($m in $ep.Methods) { - Out " $($m.HTTPMethod.PadRight(6)) → $($m.Handler)" - } - } - } - } - } - - # WebService: show namespace and operations - if ($mdType -eq "WebService") { - $nsUrl = $props.SelectSingleNode("md:Namespace", $ns) - if ($nsUrl -and $nsUrl.InnerText) { Out "Пространство имён: $($nsUrl.InnerText)" } - if ($childObjs) { - $ops = @(Get-WSOperations $childObjs) - if ($ops.Count -gt 0) { - Out "" - Out "Операции ($($ops.Count)):" - foreach ($op in $ops) { - Out " $($op.Name)($($op.Params)) → $($op.ReturnType)" - } - } - } - } - - # --- Enum values --- - if ($mdType -eq "Enum" -and $childObjs) { - $vals = @() - foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { - $ep = $ev.SelectSingleNode("md:Properties", $ns) - $vName = $ep.SelectSingleNode("md:Name", $ns).InnerText - $vSyn = Get-MLText $ep.SelectSingleNode("md:Synonym", $ns) - $vals += @{ Name=$vName; Synonym=$vSyn } - } - if ($vals.Count -gt 0) { - Out "" - Out "Значения ($($vals.Count)):" - $ml = Get-MaxNameLen $vals - foreach ($v in $vals) { - $padded = $v.Name.PadRight($ml) - $synText = if ($v.Synonym -and $v.Synonym -ne $v.Name) { "`"$($v.Synonym)`"" } else { "" } - Out " $padded $synText" - } - } - } - - # --- Dimensions (registers) --- - if ($mdType -match "Register$" -and $childObjs) { - $dims = @(Get-Attributes $childObjs "Dimension" $true) - if ($dims.Count -gt 0) { - Out "" - Out "Измерения ($($dims.Count)):" - $ml = Get-MaxNameLen $dims - foreach ($d in $dims) { Out (Format-AttrLine $d $ml) } - } - } - - # --- Resources (registers) --- - if ($mdType -match "Register$" -and $childObjs) { - $res = @(Get-Attributes $childObjs "Resource") - if ($res.Count -gt 0) { - Out "" - Out "Ресурсы ($($res.Count)):" - $ml = Get-MaxNameLen $res - foreach ($r in $res) { Out (Format-AttrLine $r $ml) } - } - } - - # --- Attributes --- - if ($childObjs -and $mdType -ne "Enum") { - $attrs = @(Get-Attributes $childObjs) - if ($attrs.Count -gt 0) { - Out "" - Out "Реквизиты ($($attrs.Count)):" - $sorted = Sort-AttrsRefFirst $attrs - $ml = Get-MaxNameLen $sorted - foreach ($a in $sorted) { Out (Format-AttrLine $a $ml) } - } - } - - # --- Tabular sections --- - if ($childObjs -and $mdType -ne "Enum") { - $tss = @(Get-TabularSections $childObjs) - if ($tss.Count -gt 0) { - if ($Mode -eq "full") { - foreach ($ts in $tss) { - Out "" - Out "ТЧ $($ts.Name) ($($ts.ColCount) $(Decline-Cols $ts.ColCount)):" - if ($ts.ColCount -gt 0) { - $sortedCols = Sort-AttrsRefFirst $ts.Columns - $ml = Get-MaxNameLen $sortedCols - foreach ($c in $sortedCols) { Out (Format-AttrLine $c $ml) } - } - } - } else { - # overview — just names with column counts - Out "" - $tsParts = $tss | ForEach-Object { "$($_.Name)($($_.ColCount))" } - Out "ТЧ ($($tss.Count)): $($tsParts -join ', ')" - } - } - } - - # Forms/Templates/Commands in overview for Reports & DataProcessors - if ($Mode -eq "overview" -and $childObjs -and ($mdType -eq "Report" -or $mdType -eq "DataProcessor")) { - $forms = @(Get-SimpleChildren $childObjs "Form") - if ($forms.Count -gt 0) { Out "Формы: $($forms -join ', ')" } - $templates = @(Get-SimpleChildren $childObjs "Template") - if ($templates.Count -gt 0) { Out "Макеты: $($templates -join ', ')" } - $commands = @(Get-SimpleChildren $childObjs "Command") - if ($commands.Count -gt 0) { Out "Команды: $($commands -join ', ')" } - } - - # --- Full mode: additional sections --- - if ($Mode -eq "full" -and $childObjs) { - # Register records (documents) - if ($mdType -eq "Document") { - $regRecs = @() - foreach ($item in $props.SelectNodes("md:RegisterRecords/xr:Item", $ns)) { - $raw = $item.InnerText - if ($raw -match '^(\w+)\.(.+)$') { - $prefix = $Matches[1] - $rname = $Matches[2] - $short = if ($regTypeMap.ContainsKey($prefix)) { $regTypeMap[$prefix] } else { $prefix } - $regRecs += "$short.$rname" - } else { - $regRecs += $raw - } - } - if ($regRecs.Count -gt 0) { - Out "" - Out "Движения ($($regRecs.Count)): $($regRecs -join ', ')" - } - - # BasedOn - $basedOn = @() - foreach ($item in $props.SelectNodes("md:BasedOn/xr:Item", $ns)) { - $raw = $item.InnerText - if ($raw -match '^\w+\.(.+)$') { $basedOn += $Matches[1] } else { $basedOn += $raw } - } - if ($basedOn.Count -gt 0) { - Out "Ввод на основании: $($basedOn -join ', ')" - } - } - - # Forms - $forms = @(Get-SimpleChildren $childObjs "Form") - if ($forms.Count -gt 0) { - Out "Формы: $($forms -join ', ')" - } - - # Templates - $templates = @(Get-SimpleChildren $childObjs "Template") - if ($templates.Count -gt 0) { - Out "Макеты: $($templates -join ', ')" - } - - # Commands - $commands = @(Get-SimpleChildren $childObjs "Command") - if ($commands.Count -gt 0) { - Out "Команды: $($commands -join ', ')" - } - } - } -} - -# --- Pagination and output --- -$totalLines = $script:lines.Count -$outLines = $script:lines - -if ($Offset -gt 0) { - if ($Offset -ge $totalLines) { - Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." - exit 0 - } - $outLines = $outLines[$Offset..($totalLines - 1)] -} - -if ($Limit -gt 0 -and $outLines.Count -gt $Limit) { - $shown = $outLines[0..($Limit - 1)] - $remaining = $totalLines - $Offset - $Limit - $shown += "" - $shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения." - $outLines = $shown -} - -if ($OutFile) { - if (-not [System.IO.Path]::IsPathRooted($OutFile)) { - $OutFile = Join-Path (Get-Location).Path $OutFile - } - $utf8 = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8) - Write-Host "Output written to $OutFile" -} else { - foreach ($l in $outLines) { Write-Host $l } -} +# meta-info v1.3 — Compact summary of 1C metadata object +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath, + [ValidateSet("overview","brief","full")] + [string]$Mode = "overview", + [string]$Name, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper (always collect, paginate at the end) --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +# Directory -> find XML inside or as sibling +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } elseif (Test-Path $sibling) { + $ObjectPath = $sibling + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +# File not found — check Dir/Name/Name.xml → Dir/Name.xml (common LLM mistake) +if (-not (Test-Path $ObjectPath)) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) + $parentDir = Split-Path $ObjectPath + $parentDirName = Split-Path $parentDir -Leaf + if ($fileName -eq $parentDirName) { + $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +# --- Load XML --- +[xml]$xmlDoc = Get-Content -Path $ObjectPath -Encoding UTF8 +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) +if (-not $mdRoot) { + Write-Host "[ERROR] Not a valid 1C metadata XML file" + exit 1 +} + +# --- Detect object type --- +$typeNode = $null +$mdType = "" +foreach ($child in $mdRoot.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq "http://v8.1c.ru/8.3/MDClasses") { + $typeNode = $child + $mdType = $child.LocalName + break + } +} + +if (-not $typeNode) { + Write-Host "[ERROR] Cannot detect metadata type" + exit 1 +} + +# --- Type name maps --- +$typeNameMap = @{ + "Catalog"="Справочник"; "Document"="Документ"; "Enum"="Перечисление" + "Constant"="Константа"; "InformationRegister"="Регистр сведений" + "AccumulationRegister"="Регистр накопления"; "AccountingRegister"="Регистр бухгалтерии" + "CalculationRegister"="Регистр расчёта"; "ChartOfAccounts"="План счетов" + "ChartOfCharacteristicTypes"="План видов характеристик" + "ChartOfCalculationTypes"="План видов расчёта"; "BusinessProcess"="Бизнес-процесс" + "Task"="Задача"; "ExchangePlan"="План обмена"; "DocumentJournal"="Журнал документов" + "Report"="Отчёт"; "DataProcessor"="Обработка" + "DefinedType"="Определяемый тип"; "CommonModule"="Общий модуль" + "ScheduledJob"="Регламентное задание"; "EventSubscription"="Подписка на событие" + "HTTPService"="HTTP-сервис"; "WebService"="Веб-сервис" +} + +$refTypeMap = @{ + "CatalogRef"="СправочникСсылка"; "DocumentRef"="ДокументСсылка" + "EnumRef"="ПеречислениеСсылка"; "ChartOfAccountsRef"="ПланСчетовСсылка" + "ChartOfCharacteristicTypesRef"="ПВХСсылка"; "ChartOfCalculationTypesRef"="ПВРСсылка" + "ExchangePlanRef"="ПланОбменаСсылка"; "BusinessProcessRef"="БизнесПроцессСсылка" + "TaskRef"="ЗадачаСсылка" +} + +$regTypeMap = @{ + "AccumulationRegister"="РН"; "AccountingRegister"="РБ"; "CalculationRegister"="РР" + "InformationRegister"="РС" +} + +$periodMap = @{ + "Nonperiodical"="Непериодический"; "Day"="День"; "Month"="Месяц" + "Quarter"="Квартал"; "Year"="Год"; "Second"="Секунда" +} + +$writeModeMap = @{ + "Independent"="независимая"; "RecorderSubordinate"="подчинение регистратору" +} + +$reuseMap = @{ + "DontUse"="нет"; "DuringRequest"="на время вызова"; "DuringSession"="на время сеанса" +} + +$eventMap = @{ + "BeforeWrite"="ПередЗаписью"; "OnWrite"="ПриЗаписи"; "AfterWrite"="ПослеЗаписи" + "BeforeDelete"="ПередУдалением"; "Posting"="ОбработкаПроведения" + "UndoPosting"="ОбработкаУдаленияПроведения" + "OnReadAtServer"="ПриЧтенииНаСервере" + "FillCheckProcessing"="ОбработкаПроверкиЗаполнения" +} + +$objectTypeMap = @{ + "CatalogObject"="СправочникОбъект"; "DocumentObject"="ДокументОбъект" + "ChartOfAccountsObject"="ПланСчетовОбъект" + "ChartOfCharacteristicTypesObject"="ПВХОбъект" + "BusinessProcessObject"="БизнесПроцессОбъект"; "TaskObject"="ЗадачаОбъект" + "ExchangePlanObject"="ПланОбменаОбъект" + "InformationRegisterRecordSet"="НаборЗаписейРС" + "AccumulationRegisterRecordSet"="НаборЗаписейРН" + "AccountingRegisterRecordSet"="НаборЗаписейРБ" +} + +$numberPeriodMap = @{ + "Year"="по году"; "Quarter"="по кварталу"; "Month"="по месяцу"; "Day"="по дню" + "WholeCatalog"="сквозная" +} + +$ruTypeName = if ($typeNameMap.ContainsKey($mdType)) { $typeNameMap[$mdType] } else { $mdType } + +# --- Helpers --- + +function Get-MLText($node) { + if (-not $node) { return "" } + $c = $node.SelectSingleNode("v8:item[v8:lang='ru']/v8:content", $ns) + if ($c) { return $c.InnerText } + $c = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($c) { return $c.InnerText } + $text = $node.InnerText.Trim() + if ($text) { return $text } + return "" +} + +function Format-Type($typeNode) { + if (-not $typeNode) { return "" } + $types = @() + foreach ($t in $typeNode.SelectNodes("v8:Type", $ns)) { + $raw = $t.InnerText + $types += Format-SingleType $raw $typeNode + } + foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) { + $raw = $t.InnerText + if ($raw -match '^cfg:DefinedType\.(.+)$') { + $types += "ОпределяемыйТип.$($Matches[1])" + } elseif ($raw -match '^cfg:Characteristic\.(.+)$') { + $types += "Характеристика.$($Matches[1])" + } else { + $types += $raw + } + } + if ($types.Count -eq 0) { return "" } + if ($types.Count -eq 1) { return $types[0] } + return ($types -join " | ") +} + +function Format-SingleType([string]$raw, $parentNode) { + switch -Wildcard ($raw) { + "xs:string" { + $sq = $parentNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) + $len = if ($sq) { $sq.InnerText } else { "" } + if ($len) { return "Строка($len)" } else { return "Строка" } + } + "xs:decimal" { + $dg = $parentNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) + $fr = $parentNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) + $d = if ($dg) { $dg.InnerText } else { "" } + $f = if ($fr) { $fr.InnerText } else { "0" } + if ($d) { return "Число($d,$f)" } else { return "Число" } + } + "xs:boolean" { return "Булево" } + "xs:dateTime" { + $dq = $parentNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) + if ($dq) { + switch ($dq.InnerText) { + "Date" { return "Дата" } + "Time" { return "Время" } + "DateTime" { return "ДатаВремя" } + default { return "Дата" } + } + } + return "ДатаВремя" + } + "v8:ValueStorage" { return "ХранилищеЗначения" } + "v8:UUID" { return "УникальныйИдентификатор" } + "v8:Null" { return "Null" } + default { + # Normalize d5p1:/dNpN: -> cfg: + $raw = $raw -replace '^d\d+p\d+:', 'cfg:' + # cfg:CatalogRef.Xxx -> СправочникСсылка.Xxx + if ($raw -match '^cfg:(\w+)Ref\.(.+)$') { + $prefix = "$($Matches[1])Ref" + $objn = $Matches[2] + if ($refTypeMap.ContainsKey($prefix)) { + return "$($refTypeMap[$prefix]).$objn" + } + } + # cfg:EnumRef.Xxx + if ($raw -match '^cfg:EnumRef\.(.+)$') { + return "ПеречислениеСсылка.$($Matches[1])" + } + # cfg:Characteristic.Xxx + if ($raw -match '^cfg:Characteristic\.(.+)$') { + return "Характеристика.$($Matches[1])" + } + # cfg:DefinedType.Xxx + if ($raw -match '^cfg:DefinedType\.(.+)$') { + return "ОпределяемыйТип.$($Matches[1])" + } + # Strip cfg: prefix for unknown + if ($raw -match '^cfg:(.+)$') { + return $Matches[1] + } + return $raw + } + } +} + +function Format-Flags($propsNode, [bool]$isDimension = $false) { + $flags = @() + $fc = $propsNode.SelectSingleNode("md:FillChecking", $ns) + if ($fc -and $fc.InnerText -eq "ShowError") { $flags += "обязательный" } + + $idx = $propsNode.SelectSingleNode("md:Indexing", $ns) + if ($idx) { + switch ($idx.InnerText) { + "Index" { $flags += "индекс" } + "IndexWithAdditionalOrder" { $flags += "индекс+доп" } + } + } + + if ($isDimension) { + $master = $propsNode.SelectSingleNode("md:Master", $ns) + if ($master -and $master.InnerText -eq "true") { $flags += "ведущее" } + } + + $ml = $propsNode.SelectSingleNode("md:MultiLine", $ns) + if ($ml -and $ml.InnerText -eq "true") { $flags += "многострочный" } + + $use = $propsNode.SelectSingleNode("md:Use", $ns) + if ($use) { + switch ($use.InnerText) { + "ForFolder" { $flags += "для папок" } + "ForFolderAndItem" { $flags += "для папок и элементов" } + } + } + + if ($flags.Count -eq 0) { return "" } + return " [$($flags -join ', ')]" +} + +function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) { + $result = @() + foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) { + $aprops = $attr.SelectSingleNode("md:Properties", $ns) + if (-not $aprops) { continue } + $attrName = $aprops.SelectSingleNode("md:Name", $ns).InnerText + $typeStr = Format-Type $aprops.SelectSingleNode("md:Type", $ns) + $aflags = Format-Flags $aprops $isDimension + $result += @{ Name=$attrName; Type=$typeStr; Flags=$aflags; Props=$aprops } + } + return $result +} + +function Get-TabularSections($parentNode) { + $result = @() + foreach ($ts in $parentNode.SelectNodes("md:TabularSection", $ns)) { + $tprops = $ts.SelectSingleNode("md:Properties", $ns) + $tsName = $tprops.SelectSingleNode("md:Name", $ns).InnerText + $tchildObjs = $ts.SelectSingleNode("md:ChildObjects", $ns) + $cols = @() + if ($tchildObjs) { $cols = @(Get-Attributes $tchildObjs) } + $result += @{ Name=$tsName; Columns=$cols; ColCount=$cols.Count } + } + return $result +} + +function Format-AttrLine([hashtable]$attr, [int]$maxNameLen = 30) { + $padded = $attr.Name.PadRight($maxNameLen) + return " $padded $($attr.Type)$($attr.Flags)" +} + +function Get-MaxNameLen($attrs) { + $max = 10 + foreach ($a in $attrs) { + if ($a.Name.Length -gt $max) { $max = $a.Name.Length } + } + return [Math]::Min($max + 2, 40) +} + +function Get-SimpleChildren($parentNode, [string]$tag) { + $result = @() + foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) { + $result += $child.InnerText + } + return $result +} + +function Sort-AttrsRefFirst($attrs) { + $refs = @() + $prims = @() + foreach ($a in $attrs) { + $t = $a.Type + if ($t -match 'Ссылка\.' -or $t -match 'Характеристика\.' -or $t -match 'ОпределяемыйТип\.' -or $t -match 'ПланСчетовСсылка' -or $t -match 'ПВХСсылка' -or $t -match 'ПВРСсылка') { + $refs += $a + } else { + $prims += $a + } + } + return @($refs) + @($prims) +} + +function Decline-Cols([int]$n) { + $m = $n % 10 + $h = $n % 100 + if ($h -ge 11 -and $h -le 19) { return "колонок" } + if ($m -eq 1) { return "колонка" } + if ($m -ge 2 -and $m -le 4) { return "колонки" } + return "колонок" +} + +function Format-SourceType([string]$raw) { + $raw = $raw -replace '^d\d+p\d+:', 'cfg:' + if ($raw -match '^cfg:(\w+)\.(.+)$') { + $prefix = $Matches[1]; $name = $Matches[2] + if ($objectTypeMap.ContainsKey($prefix)) { return "$($objectTypeMap[$prefix]).$name" } + } + if ($raw -match '^cfg:(.+)$') { return $Matches[1] } + return $raw +} + +function Get-HTTPEndpoints($childObjs) { + $result = @() + foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) { + $tp = $tpl.SelectSingleNode("md:Properties", $ns) + $tplName = $tp.SelectSingleNode("md:Name", $ns).InnerText + $template = $tp.SelectSingleNode("md:Template", $ns).InnerText + $methods = @() + $tplCO = $tpl.SelectSingleNode("md:ChildObjects", $ns) + if ($tplCO) { + foreach ($m in $tplCO.SelectNodes("md:Method", $ns)) { + $mp = $m.SelectSingleNode("md:Properties", $ns) + $httpMethod = $mp.SelectSingleNode("md:HTTPMethod", $ns).InnerText + $handler = $mp.SelectSingleNode("md:Handler", $ns).InnerText + $methods += @{ HTTPMethod=$httpMethod; Handler=$handler; Name=$mp.SelectSingleNode("md:Name",$ns).InnerText } + } + } + $result += @{ Name=$tplName; Template=$template; Methods=$methods } + } + return $result +} + +function Get-WSOperations($childObjs) { + $result = @() + foreach ($op in $childObjs.SelectNodes("md:Operation", $ns)) { + $oprops = $op.SelectSingleNode("md:Properties", $ns) + $opName = $oprops.SelectSingleNode("md:Name", $ns).InnerText + $retType = $oprops.SelectSingleNode("md:XDTOReturningValueType", $ns) + $retStr = if ($retType -and $retType.InnerText) { $retType.InnerText } else { "void" } + $procName = $oprops.SelectSingleNode("md:ProcedureName", $ns) + $params = @() + $opCO = $op.SelectSingleNode("md:ChildObjects", $ns) + if ($opCO) { + foreach ($p in $opCO.SelectNodes("md:Parameter", $ns)) { + $pp = $p.SelectSingleNode("md:Properties", $ns) + $pName = $pp.SelectSingleNode("md:Name", $ns).InnerText + $pType = $pp.SelectSingleNode("md:XDTOValueType", $ns) + $pTypeStr = if ($pType) { $pType.InnerText } else { "?" } + $dir = $pp.SelectSingleNode("md:TransferDirection", $ns) + $dirStr = if ($dir -and $dir.InnerText -ne "In") { " [$($dir.InnerText.ToLower())]" } else { "" } + $params += "${pName}: ${pTypeStr}${dirStr}" + } + } + $paramStr = $params -join ", " + $result += @{ Name=$opName; Params=$paramStr; ReturnType=$retStr; ProcName=$(if ($procName) { $procName.InnerText } else { "" }) } + } + return $result +} + +# --- Support status of this object (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the +# object's support rule. Never throws — degrades to "не на поддержке". +function Get-ObjectSupportStatus([string]$objUuid) { + try { + # Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin). + $d = [System.IO.Path]::GetDirectoryName($ObjectPath) + $binPath = $null + for ($i = 0; $i -lt 8 -and $d; $i++) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $objUuid) { return "не на поддержке" } + # Conservative fold: locked if f1=0 in ANY vendor block. + $u = [regex]::Escape($objUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + +# --- Extract metadata --- +$props = $typeNode.SelectSingleNode("md:Properties", $ns) +$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$objName = $props.SelectSingleNode("md:Name", $ns).InnerText +$synNode = $props.SelectSingleNode("md:Synonym", $ns) +$synonym = Get-MLText $synNode + +# Presentations (type-choice dialogs show "Представление объекта" as the ref type name) +$objPresentation = Get-MLText $props.SelectSingleNode("md:ObjectPresentation", $ns) +$extObjPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedObjectPresentation", $ns) +$listPresentation = Get-MLText $props.SelectSingleNode("md:ListPresentation", $ns) +$extListPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedListPresentation", $ns) + +# Reference (ref-typed) metadata objects — those with a ...Ref type +$refMdTypes = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes", + "ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task") +$isRefObject = $refMdTypes -contains $mdType + +# Effective type presentation: ObjectPresentation -> Synonym -> Name +$typePresentation = if ($objPresentation) { $objPresentation } + elseif ($synonym) { $synonym } + else { $objName } + +# --- Handle -Name drill-down --- +$drillDone = $false +if ($Name -and $childObjs) { + # Search in attributes/dimensions/resources + $attrTags = @("Attribute","Dimension","Resource") + foreach ($tag in $attrTags) { + if ($drillDone) { break } + foreach ($attr in $childObjs.SelectNodes("md:$tag", $ns)) { + $ap = $attr.SelectSingleNode("md:Properties", $ns) + if (-not $ap) { continue } + $an = $ap.SelectSingleNode("md:Name", $ns).InnerText + if ($an -eq $Name) { + $tagRu = switch ($tag) { + "Attribute" { "Реквизит" } + "Dimension" { "Измерение" } + "Resource" { "Ресурс" } + } + Out "$($tagRu): $an" + $typeStr = Format-Type $ap.SelectSingleNode("md:Type", $ns) + Out " Тип: $typeStr" + $fc = $ap.SelectSingleNode("md:FillChecking", $ns) + Out " Обязательный: $(if ($fc -and $fc.InnerText -eq 'ShowError') { 'да' } else { 'нет' })" + $idx = $ap.SelectSingleNode("md:Indexing", $ns) + $idxRu = if (-not $idx -or $idx.InnerText -eq 'DontIndex') { 'нет' } + elseif ($idx.InnerText -eq 'Index') { 'Индекс' } + elseif ($idx.InnerText -eq 'IndexWithAdditionalOrder') { 'Индекс с доп. упорядочиванием' } + else { $idx.InnerText } + Out " Индексирование: $idxRu" + $ml = $ap.SelectSingleNode("md:MultiLine", $ns) + if ($ml -and $ml.InnerText -eq "true") { Out " Многострочный: да" } + $use = $ap.SelectSingleNode("md:Use", $ns) + if ($use -and $use.InnerText -ne "ForItem") { + $useRu = switch ($use.InnerText) { + "ForFolder" { "для папок" } + "ForFolderAndItem" { "для папок и элементов" } + default { $use.InnerText } + } + Out " Использование: $useRu" + } + $fv = $ap.SelectSingleNode("md:FillValue", $ns) + if ($fv -and -not ($fv.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance") -eq "true") -and $fv.InnerText) { + $fvText = $fv.InnerText + if ($fvText -match '\.EmptyRef$') { $fvText = "Пустая ссылка" } + elseif ($fvText -eq "false") { $fvText = "Ложь" } + elseif ($fvText -eq "true") { $fvText = "Истина" } + Out " Значение заполнения: $fvText" + } else { + Out " Значение заполнения: —" + } + if ($tag -eq "Dimension") { + $master = $ap.SelectSingleNode("md:Master", $ns) + Out " Ведущее: $(if ($master -and $master.InnerText -eq 'true') { 'да' } else { 'нет' })" + $mf = $ap.SelectSingleNode("md:MainFilter", $ns) + Out " Основной отбор: $(if ($mf -and $mf.InnerText -eq 'true') { 'да' } else { 'нет' })" + } + $synA = $ap.SelectSingleNode("md:Synonym", $ns) + $synText = Get-MLText $synA + if ($synText -and $synText -ne $an) { Out " Синоним: $synText" } + $drillDone = $true + break + } + } + } + + # Search in tabular sections + if (-not $drillDone) { + foreach ($ts in $childObjs.SelectNodes("md:TabularSection", $ns)) { + $tp = $ts.SelectSingleNode("md:Properties", $ns) + $tn = $tp.SelectSingleNode("md:Name", $ns).InnerText + if ($tn -eq $Name) { + $tsCO = $ts.SelectSingleNode("md:ChildObjects", $ns) + $cols = @() + if ($tsCO) { $cols = @(Get-Attributes $tsCO) } + Out "ТЧ: $tn ($($cols.Count) $(Decline-Cols $cols.Count)):" + if ($cols.Count -gt 0) { + $ml = Get-MaxNameLen $cols + foreach ($c in $cols) { Out (Format-AttrLine $c $ml) } + } + $drillDone = $true + break + } + } + } + + # Search in enum values + if (-not $drillDone) { + foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { + $ep = $ev.SelectSingleNode("md:Properties", $ns) + $en = $ep.SelectSingleNode("md:Name", $ns).InnerText + if ($en -eq $Name) { + $synE = $ep.SelectSingleNode("md:Synonym", $ns) + $synText = Get-MLText $synE + Out "Значение перечисления: $en" + if ($synText) { Out " Синоним: `"$synText`"" } + $cm = $ep.SelectSingleNode("md:Comment", $ns) + if ($cm -and $cm.InnerText) { Out " Комментарий: $($cm.InnerText)" } + $drillDone = $true + break + } + } + } + + # Search in HTTPService URLTemplates + if (-not $drillDone -and $mdType -eq "HTTPService" -and $childObjs) { + foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) { + $tp = $tpl.SelectSingleNode("md:Properties", $ns) + if ($tp.SelectSingleNode("md:Name", $ns).InnerText -eq $Name) { + $template = $tp.SelectSingleNode("md:Template", $ns).InnerText + Out "Шаблон URL: $Name" + Out " Путь: $template" + $tplCO = $tpl.SelectSingleNode("md:ChildObjects", $ns) + if ($tplCO) { + foreach ($m in $tplCO.SelectNodes("md:Method", $ns)) { + $mp = $m.SelectSingleNode("md:Properties", $ns) + $httpMethod = $mp.SelectSingleNode("md:HTTPMethod", $ns).InnerText + $handler = $mp.SelectSingleNode("md:Handler", $ns).InnerText + Out " $httpMethod → $handler" + } + } + $drillDone = $true; break + } + } + } + + # Search in WebService Operations + if (-not $drillDone -and $mdType -eq "WebService" -and $childObjs) { + foreach ($op in $childObjs.SelectNodes("md:Operation", $ns)) { + $oprops = $op.SelectSingleNode("md:Properties", $ns) + if ($oprops.SelectSingleNode("md:Name", $ns).InnerText -eq $Name) { + Out "Операция: $Name" + $retType = $oprops.SelectSingleNode("md:XDTOReturningValueType", $ns) + Out " Возвращает: $(if ($retType -and $retType.InnerText) { $retType.InnerText } else { 'void' })" + $procName = $oprops.SelectSingleNode("md:ProcedureName", $ns) + if ($procName -and $procName.InnerText) { Out " Процедура: $($procName.InnerText)" } + $comment = $oprops.SelectSingleNode("md:Comment", $ns) + if ($comment -and $comment.InnerText) { Out " Комментарий: $($comment.InnerText)" } + $opCO = $op.SelectSingleNode("md:ChildObjects", $ns) + if ($opCO) { + $params = $opCO.SelectNodes("md:Parameter", $ns) + if ($params.Count -gt 0) { + Out " Параметры:" + foreach ($p in $params) { + $pp = $p.SelectSingleNode("md:Properties", $ns) + $pName = $pp.SelectSingleNode("md:Name", $ns).InnerText + $pType = $pp.SelectSingleNode("md:XDTOValueType", $ns) + $dir = $pp.SelectSingleNode("md:TransferDirection", $ns) + $dirStr = if ($dir -and $dir.InnerText -ne "In") { " [$($dir.InnerText.ToLower())]" } else { "" } + Out " ${pName}: $(if ($pType) { $pType.InnerText } else { '?' })${dirStr}" + } + } + } + $drillDone = $true; break + } + } + } + + if (-not $drillDone) { + Write-Host "[ERROR] '$Name' not found in $objName" + exit 1 + } +} + +# --- Main output (not drill-down) --- +if (-not $drillDone) { + + # --- Build header --- + $header = "=== $ruTypeName`: $objName" + if ($synonym -and $synonym -ne $objName) { $header += " — `"$synonym`"" } + $header += " ===" + Out $header + Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))" + + # --- Type presentation (ref objects) --- + if ($isRefObject) { + Out "Представление типа: $typePresentation" + if ($Mode -eq "full") { + if ($objPresentation) { Out "Представление объекта: $objPresentation" } + if ($extObjPresentation) { Out "Расширенное представление объекта: $extObjPresentation" } + if ($listPresentation) { Out "Представление списка: $listPresentation" } + if ($extListPresentation) { Out "Расширенное представление списка: $extListPresentation" } + } + } + + # --- Mode: brief --- + if ($Mode -eq "brief") { + # Attributes + $attrs = @() + if ($childObjs) { $attrs = @(Get-Attributes $childObjs) } + if ($attrs.Count -gt 0) { + $names = ($attrs | ForEach-Object { $_.Name }) -join ", " + Out "Реквизиты ($($attrs.Count)): $names" + } + + # Dimensions/Resources for registers + if ($mdType -match "Register$") { + $dims = @() + if ($childObjs) { $dims = @(Get-Attributes $childObjs "Dimension" $true) } + if ($dims.Count -gt 0) { + $names = ($dims | ForEach-Object { $_.Name }) -join ", " + Out "Измерения ($($dims.Count)): $names" + } + $res = @() + if ($childObjs) { $res = @(Get-Attributes $childObjs "Resource") } + if ($res.Count -gt 0) { + $names = ($res | ForEach-Object { $_.Name }) -join ", " + Out "Ресурсы ($($res.Count)): $names" + } + } + + # Tabular sections + $tss = @() + if ($childObjs) { $tss = @(Get-TabularSections $childObjs) } + if ($tss.Count -gt 0) { + $tsParts = $tss | ForEach-Object { "$($_.Name)($($_.ColCount))" } + Out "ТЧ ($($tss.Count)): $($tsParts -join ', ')" + } + + # Enum values + if ($mdType -eq "Enum") { + $vals = @() + if ($childObjs) { + foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { + $ep = $ev.SelectSingleNode("md:Properties", $ns) + $vals += $ep.SelectSingleNode("md:Name", $ns).InnerText + } + } + if ($vals.Count -gt 0) { + Out "Значения ($($vals.Count)): $($vals -join ', ')" + } + } + + # DefinedType brief + if ($mdType -eq "DefinedType") { + $typeNode2 = $props.SelectSingleNode("md:Type", $ns) + if ($typeNode2) { + $types = @() + foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { + $types += Format-SingleType $t.InnerText $typeNode2 + } + if ($types.Count -gt 0) { + Out "Типы ($($types.Count)): $($types -join ', ')" + } + } + } + + # CommonModule brief (same as overview — already compact) + if ($mdType -eq "CommonModule") { + $flags = @() + if ($props.SelectSingleNode("md:Global", $ns).InnerText -eq "true") { $flags += "Глобальный" } + if ($props.SelectSingleNode("md:Server", $ns).InnerText -eq "true") { $flags += "Сервер" } + if ($props.SelectSingleNode("md:ServerCall", $ns).InnerText -eq "true") { $flags += "Вызов сервера" } + if ($props.SelectSingleNode("md:ClientManagedApplication", $ns).InnerText -eq "true") { $flags += "Клиент управляемое" } + if ($props.SelectSingleNode("md:ClientOrdinaryApplication", $ns).InnerText -eq "true") { $flags += "Обычный клиент" } + if ($props.SelectSingleNode("md:ExternalConnection", $ns).InnerText -eq "true") { $flags += "Внешнее соединение" } + if ($props.SelectSingleNode("md:Privileged", $ns).InnerText -eq "true") { $flags += "Привилегированный" } + $reuse = $props.SelectSingleNode("md:ReturnValuesReuse", $ns) + if ($reuse -and $reuse.InnerText -ne "DontUse") { + $reuseRu = if ($reuseMap.ContainsKey($reuse.InnerText)) { $reuseMap[$reuse.InnerText] } else { $reuse.InnerText } + $flags += "Повторное использование: $reuseRu" + } + if ($flags.Count -gt 0) { Out ($flags -join " | ") } + } + + # ScheduledJob brief (same as overview — already compact) + if ($mdType -eq "ScheduledJob") { + $method = $props.SelectSingleNode("md:MethodName", $ns) + if ($method -and $method.InnerText) { + $mName = $method.InnerText + if ($mName -match '^CommonModule\.(.+)$') { $mName = $Matches[1] } + Out "Метод: $mName" + } + $sjParts = @() + $use = $props.SelectSingleNode("md:Use", $ns) + $sjParts += "Использование: $(if ($use -and $use.InnerText -eq 'true') { 'да' } else { 'нет' })" + $predef = $props.SelectSingleNode("md:Predefined", $ns) + $sjParts += "Предопределённое: $(if ($predef -and $predef.InnerText -eq 'true') { 'да' } else { 'нет' })" + $restartCnt = $props.SelectSingleNode("md:RestartCountOnFailure", $ns) + $restartInt = $props.SelectSingleNode("md:RestartIntervalOnFailure", $ns) + if ($restartCnt -and [int]$restartCnt.InnerText -gt 0) { + $sjParts += "Перезапуск: $($restartCnt.InnerText) (через $($restartInt.InnerText) сек)" + } + Out ($sjParts -join " | ") + } + + # EventSubscription brief + if ($mdType -eq "EventSubscription") { + $esParts = @() + $event = $props.SelectSingleNode("md:Event", $ns) + if ($event -and $event.InnerText) { + $evRu = if ($eventMap.ContainsKey($event.InnerText)) { $eventMap[$event.InnerText] } else { $event.InnerText } + $esParts += "Событие: $evRu" + } + $handler = $props.SelectSingleNode("md:Handler", $ns) + if ($handler -and $handler.InnerText) { + $hName = $handler.InnerText + if ($hName -match '^CommonModule\.(.+)$') { $hName = $Matches[1] } + $esParts += "Обработчик: $hName" + } + $source = $props.SelectSingleNode("md:Source", $ns) + if ($source) { + $srcCount = $source.SelectNodes("v8:Type", $ns).Count + if ($srcCount -gt 0) { $esParts += "Источники: $srcCount" } + } + if ($esParts.Count -gt 0) { Out ($esParts -join " | ") } + } + + # HTTPService brief + if ($mdType -eq "HTTPService") { + $rootURL = $props.SelectSingleNode("md:RootURL", $ns) + if ($rootURL -and $rootURL.InnerText) { Out "Корневой URL: /$($rootURL.InnerText)" } + if ($childObjs) { + $endpoints = @(Get-HTTPEndpoints $childObjs) + if ($endpoints.Count -gt 0) { + $totalMethods = ($endpoints | ForEach-Object { $_.Methods.Count } | Measure-Object -Sum).Sum + Out "Шаблоны: $($endpoints.Count) | Методы: $totalMethods" + } + } + } + + # WebService brief + if ($mdType -eq "WebService") { + $nsUrl = $props.SelectSingleNode("md:Namespace", $ns) + if ($nsUrl -and $nsUrl.InnerText) { Out "Пространство имён: $($nsUrl.InnerText)" } + if ($childObjs) { + $ops = @(Get-WSOperations $childObjs) + if ($ops.Count -gt 0) { Out "Операции: $($ops.Count)" } + } + } + } else { + # --- Mode: overview / full --- + + # Document-specific header properties + if ($mdType -eq "Document") { + $numType = $props.SelectSingleNode("md:NumberType", $ns) + $numLen = $props.SelectSingleNode("md:NumberLength", $ns) + $numPer = $props.SelectSingleNode("md:NumberPeriodicity", $ns) + $autoNum = $props.SelectSingleNode("md:Autonumbering", $ns) + $posting = $props.SelectSingleNode("md:Posting", $ns) + + $parts = @() + if ($numType -and $numLen) { + $nt = if ($numType.InnerText -eq "String") { "Строка" } else { "Число" } + $piece = "Номер: $nt($($numLen.InnerText))" + if ($numPer) { + $perRu = if ($numberPeriodMap.ContainsKey($numPer.InnerText)) { $numberPeriodMap[$numPer.InnerText] } else { $numPer.InnerText } + $piece += ", $perRu" + } + if ($autoNum -and $autoNum.InnerText -eq "true") { $piece += ", авто" } + $parts += $piece + } + if ($posting) { + $parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })" + } + if ($parts.Count -gt 0) { Out ($parts -join " | ") } + } + + # Catalog-specific header properties + if ($mdType -eq "Catalog") { + $parts = @() + $hier = $props.SelectSingleNode("md:Hierarchical", $ns) + if ($hier -and $hier.InnerText -eq "true") { + $ht = $props.SelectSingleNode("md:HierarchyType", $ns) + $htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" } + $limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns) + $levelNode = $props.SelectSingleNode("md:LevelCount", $ns) + if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) { + $htText += ", уровней: $($levelNode.InnerText)" + } else { + $htText += ", без ограничения уровней" + } + $parts += "Иерархический: $htText" + } + $codeLen = $props.SelectSingleNode("md:CodeLength", $ns) + $descLen = $props.SelectSingleNode("md:DescriptionLength", $ns) + if ($codeLen -and [int]$codeLen.InnerText -gt 0) { $parts += "Код($($codeLen.InnerText))" } + if ($descLen -and [int]$descLen.InnerText -gt 0) { $parts += "Наименование($($descLen.InnerText))" } + if ($parts.Count -gt 0) { Out ($parts -join " | ") } + } + + # Register-specific header properties + if ($mdType -match "Register$") { + $parts = @() + if ($mdType -eq "InformationRegister") { + $per = $props.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) + if ($per) { + $perRu = if ($periodMap.ContainsKey($per.InnerText)) { $periodMap[$per.InnerText] } else { $per.InnerText } + $parts += "Периодичность: $perRu" + } + $wm = $props.SelectSingleNode("md:WriteMode", $ns) + if ($wm) { + $wmRu = if ($writeModeMap.ContainsKey($wm.InnerText)) { $writeModeMap[$wm.InnerText] } else { $wm.InnerText } + $parts += "Запись: $wmRu" + } + } + if ($mdType -eq "AccumulationRegister") { + $regKind = $props.SelectSingleNode("md:RegisterType", $ns) + if ($regKind) { + $rkRu = switch ($regKind.InnerText) { + "Balances" { "остатки" } + "Turnovers" { "обороты" } + default { $regKind.InnerText } + } + $parts += "Вид: $rkRu" + } + } + if ($parts.Count -gt 0) { Out ($parts -join " | ") } + } + + # Constant-specific: show type + if ($mdType -eq "Constant") { + $typeStr = Format-Type $props.SelectSingleNode("md:Type", $ns) + if ($typeStr) { Out "Тип: $typeStr" } + } + + # Report-specific: MainDataCompositionSchema + if ($mdType -eq "Report") { + $mainDCS = $props.SelectSingleNode("md:MainDataCompositionSchema", $ns) + if ($mainDCS -and $mainDCS.InnerText) { + $dcsName = $mainDCS.InnerText + if ($dcsName -match '\.Template\.(.+)$') { $dcsName = $Matches[1] } + Out "Основная СКД: $dcsName" + } + } + + # DefinedType: show types + if ($mdType -eq "DefinedType") { + $typeNode2 = $props.SelectSingleNode("md:Type", $ns) + if ($typeNode2) { + $types = @() + foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { + $types += Format-SingleType $t.InnerText $typeNode2 + } + if ($types.Count -gt 0) { + Out "Типы ($($types.Count)):" + foreach ($t in $types) { Out " $t" } + } + } + } + + # CommonModule: show flags + if ($mdType -eq "CommonModule") { + $flags = @() + if ($props.SelectSingleNode("md:Global", $ns).InnerText -eq "true") { $flags += "Глобальный" } + if ($props.SelectSingleNode("md:Server", $ns).InnerText -eq "true") { $flags += "Сервер" } + if ($props.SelectSingleNode("md:ServerCall", $ns).InnerText -eq "true") { $flags += "Вызов сервера" } + if ($props.SelectSingleNode("md:ClientManagedApplication", $ns).InnerText -eq "true") { $flags += "Клиент управляемое" } + if ($props.SelectSingleNode("md:ClientOrdinaryApplication", $ns).InnerText -eq "true") { $flags += "Обычный клиент" } + if ($props.SelectSingleNode("md:ExternalConnection", $ns).InnerText -eq "true") { $flags += "Внешнее соединение" } + if ($props.SelectSingleNode("md:Privileged", $ns).InnerText -eq "true") { $flags += "Привилегированный" } + $reuse = $props.SelectSingleNode("md:ReturnValuesReuse", $ns) + if ($reuse -and $reuse.InnerText -ne "DontUse") { + $reuseRu = if ($reuseMap.ContainsKey($reuse.InnerText)) { $reuseMap[$reuse.InnerText] } else { $reuse.InnerText } + $flags += "Повторное использование: $reuseRu" + } + if ($flags.Count -gt 0) { Out ($flags -join " | ") } + } + + # ScheduledJob: show method and flags + if ($mdType -eq "ScheduledJob") { + $method = $props.SelectSingleNode("md:MethodName", $ns) + if ($method -and $method.InnerText) { + $mName = $method.InnerText + if ($mName -match '^CommonModule\.(.+)$') { $mName = $Matches[1] } + Out "Метод: $mName" + } + $sjParts = @() + $use = $props.SelectSingleNode("md:Use", $ns) + $sjParts += "Использование: $(if ($use -and $use.InnerText -eq 'true') { 'да' } else { 'нет' })" + $predef = $props.SelectSingleNode("md:Predefined", $ns) + $sjParts += "Предопределённое: $(if ($predef -and $predef.InnerText -eq 'true') { 'да' } else { 'нет' })" + $restartCnt = $props.SelectSingleNode("md:RestartCountOnFailure", $ns) + $restartInt = $props.SelectSingleNode("md:RestartIntervalOnFailure", $ns) + if ($restartCnt -and [int]$restartCnt.InnerText -gt 0) { + $sjParts += "Перезапуск: $($restartCnt.InnerText) (через $($restartInt.InnerText) сек)" + } + Out ($sjParts -join " | ") + } + + # EventSubscription: show event, handler, sources + if ($mdType -eq "EventSubscription") { + $event = $props.SelectSingleNode("md:Event", $ns) + if ($event -and $event.InnerText) { + $evRu = if ($eventMap.ContainsKey($event.InnerText)) { $eventMap[$event.InnerText] } else { $event.InnerText } + Out "Событие: $evRu" + } + $handler = $props.SelectSingleNode("md:Handler", $ns) + if ($handler -and $handler.InnerText) { + $hName = $handler.InnerText + if ($hName -match '^CommonModule\.(.+)$') { $hName = $Matches[1] } + Out "Обработчик: $hName" + } + $source = $props.SelectSingleNode("md:Source", $ns) + if ($source) { + $srcTypes = @() + foreach ($t in $source.SelectNodes("v8:Type", $ns)) { + $srcTypes += Format-SourceType $t.InnerText + } + if ($srcTypes.Count -gt 0) { + if ($Mode -eq "full") { + Out "Источники ($($srcTypes.Count)):" + foreach ($s in $srcTypes) { Out " $s" } + } else { + Out "Источники ($($srcTypes.Count))" + } + } + } + } + + # HTTPService: show root URL and endpoints + if ($mdType -eq "HTTPService") { + $rootURL = $props.SelectSingleNode("md:RootURL", $ns) + if ($rootURL -and $rootURL.InnerText) { Out "Корневой URL: /$($rootURL.InnerText)" } + if ($childObjs) { + $endpoints = @(Get-HTTPEndpoints $childObjs) + if ($endpoints.Count -gt 0) { + Out "" + Out "Шаблоны URL ($($endpoints.Count)):" + foreach ($ep in $endpoints) { + Out " $($ep.Template)" + foreach ($m in $ep.Methods) { + Out " $($m.HTTPMethod.PadRight(6)) → $($m.Handler)" + } + } + } + } + } + + # WebService: show namespace and operations + if ($mdType -eq "WebService") { + $nsUrl = $props.SelectSingleNode("md:Namespace", $ns) + if ($nsUrl -and $nsUrl.InnerText) { Out "Пространство имён: $($nsUrl.InnerText)" } + if ($childObjs) { + $ops = @(Get-WSOperations $childObjs) + if ($ops.Count -gt 0) { + Out "" + Out "Операции ($($ops.Count)):" + foreach ($op in $ops) { + Out " $($op.Name)($($op.Params)) → $($op.ReturnType)" + } + } + } + } + + # --- Enum values --- + if ($mdType -eq "Enum" -and $childObjs) { + $vals = @() + foreach ($ev in $childObjs.SelectNodes("md:EnumValue", $ns)) { + $ep = $ev.SelectSingleNode("md:Properties", $ns) + $vName = $ep.SelectSingleNode("md:Name", $ns).InnerText + $vSyn = Get-MLText $ep.SelectSingleNode("md:Synonym", $ns) + $vals += @{ Name=$vName; Synonym=$vSyn } + } + if ($vals.Count -gt 0) { + Out "" + Out "Значения ($($vals.Count)):" + $ml = Get-MaxNameLen $vals + foreach ($v in $vals) { + $padded = $v.Name.PadRight($ml) + $synText = if ($v.Synonym -and $v.Synonym -ne $v.Name) { "`"$($v.Synonym)`"" } else { "" } + Out " $padded $synText" + } + } + } + + # --- Dimensions (registers) --- + if ($mdType -match "Register$" -and $childObjs) { + $dims = @(Get-Attributes $childObjs "Dimension" $true) + if ($dims.Count -gt 0) { + Out "" + Out "Измерения ($($dims.Count)):" + $ml = Get-MaxNameLen $dims + foreach ($d in $dims) { Out (Format-AttrLine $d $ml) } + } + } + + # --- Resources (registers) --- + if ($mdType -match "Register$" -and $childObjs) { + $res = @(Get-Attributes $childObjs "Resource") + if ($res.Count -gt 0) { + Out "" + Out "Ресурсы ($($res.Count)):" + $ml = Get-MaxNameLen $res + foreach ($r in $res) { Out (Format-AttrLine $r $ml) } + } + } + + # --- Attributes --- + if ($childObjs -and $mdType -ne "Enum") { + $attrs = @(Get-Attributes $childObjs) + if ($attrs.Count -gt 0) { + Out "" + Out "Реквизиты ($($attrs.Count)):" + $sorted = Sort-AttrsRefFirst $attrs + $ml = Get-MaxNameLen $sorted + foreach ($a in $sorted) { Out (Format-AttrLine $a $ml) } + } + } + + # --- Tabular sections --- + if ($childObjs -and $mdType -ne "Enum") { + $tss = @(Get-TabularSections $childObjs) + if ($tss.Count -gt 0) { + if ($Mode -eq "full") { + foreach ($ts in $tss) { + Out "" + Out "ТЧ $($ts.Name) ($($ts.ColCount) $(Decline-Cols $ts.ColCount)):" + if ($ts.ColCount -gt 0) { + $sortedCols = Sort-AttrsRefFirst $ts.Columns + $ml = Get-MaxNameLen $sortedCols + foreach ($c in $sortedCols) { Out (Format-AttrLine $c $ml) } + } + } + } else { + # overview — just names with column counts + Out "" + $tsParts = $tss | ForEach-Object { "$($_.Name)($($_.ColCount))" } + Out "ТЧ ($($tss.Count)): $($tsParts -join ', ')" + } + } + } + + # Forms/Templates/Commands in overview for Reports & DataProcessors + if ($Mode -eq "overview" -and $childObjs -and ($mdType -eq "Report" -or $mdType -eq "DataProcessor")) { + $forms = @(Get-SimpleChildren $childObjs "Form") + if ($forms.Count -gt 0) { Out "Формы: $($forms -join ', ')" } + $templates = @(Get-SimpleChildren $childObjs "Template") + if ($templates.Count -gt 0) { Out "Макеты: $($templates -join ', ')" } + $commands = @(Get-SimpleChildren $childObjs "Command") + if ($commands.Count -gt 0) { Out "Команды: $($commands -join ', ')" } + } + + # --- Full mode: additional sections --- + if ($Mode -eq "full" -and $childObjs) { + # Register records (documents) + if ($mdType -eq "Document") { + $regRecs = @() + foreach ($item in $props.SelectNodes("md:RegisterRecords/xr:Item", $ns)) { + $raw = $item.InnerText + if ($raw -match '^(\w+)\.(.+)$') { + $prefix = $Matches[1] + $rname = $Matches[2] + $short = if ($regTypeMap.ContainsKey($prefix)) { $regTypeMap[$prefix] } else { $prefix } + $regRecs += "$short.$rname" + } else { + $regRecs += $raw + } + } + if ($regRecs.Count -gt 0) { + Out "" + Out "Движения ($($regRecs.Count)): $($regRecs -join ', ')" + } + + # BasedOn + $basedOn = @() + foreach ($item in $props.SelectNodes("md:BasedOn/xr:Item", $ns)) { + $raw = $item.InnerText + if ($raw -match '^\w+\.(.+)$') { $basedOn += $Matches[1] } else { $basedOn += $raw } + } + if ($basedOn.Count -gt 0) { + Out "Ввод на основании: $($basedOn -join ', ')" + } + } + + # Forms + $forms = @(Get-SimpleChildren $childObjs "Form") + if ($forms.Count -gt 0) { + Out "Формы: $($forms -join ', ')" + } + + # Templates + $templates = @(Get-SimpleChildren $childObjs "Template") + if ($templates.Count -gt 0) { + Out "Макеты: $($templates -join ', ')" + } + + # Commands + $commands = @(Get-SimpleChildren $childObjs "Command") + if ($commands.Count -gt 0) { + Out "Команды: $($commands -join ', ')" + } + } + } +} + +# --- Pagination and output --- +$totalLines = $script:lines.Count +$outLines = $script:lines + +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $outLines = $outLines[$Offset..($totalLines - 1)] +} + +if ($Limit -gt 0 -and $outLines.Count -gt $Limit) { + $shown = $outLines[0..($Limit - 1)] + $remaining = $totalLines - $Offset - $Limit + $shown += "" + $shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения." + $outLines = $shown +} + +if ($OutFile) { + if (-not [System.IO.Path]::IsPathRooted($OutFile)) { + $OutFile = Join-Path (Get-Location).Path $OutFile + } + $utf8 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8) + Write-Host "Output written to $OutFile" +} else { + foreach ($l in $outLines) { Write-Host $l } +} diff --git a/.claude/skills/meta-remove/SKILL.md b/.claude/skills/meta-remove/SKILL.md index 8587c4bc..11a7750e 100644 --- a/.claude/skills/meta-remove/SKILL.md +++ b/.claude/skills/meta-remove/SKILL.md @@ -1,60 +1,60 @@ ---- -name: meta-remove -description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации -argument-hint: -Object -allowed-tools: - - Bash - - Read - - Glob - - AskUserQuestion ---- - -# /meta-remove — удаление объекта метаданных - -Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется. - -## Использование - -``` -/meta-remove -Object -``` - -## Параметры - -| Параметр | Обязательный | Описание | -|------------|:------------:|-------------------------------------------------| -| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) | -| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. | -| DryRun | нет | Только показать что будет удалено, без изменений | -| KeepFiles | нет | Не удалять файлы, только дерегистрировать | -| Force | нет | Удалить несмотря на найденные ссылки | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-remove.ps1" -ConfigDir "<путь>" -Object "Catalog.Товары" -``` - -## Поддерживаемые типы - -Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan, DocumentJournal, Report, DataProcessor, CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService, DefinedType, Role, Subsystem, CommonForm, CommonTemplate, CommonPicture, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, Sequence, FilterCriterion, SettingsStorage, XDTOPackage, WSReference, StyleItem, Language - -## Примеры - -```powershell -# Проверка ссылок + dry run -... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -DryRun - -# Удалить объект без ссылок -... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" - -# Принудительно удалить несмотря на ссылки -... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -Force - -# Только дерегистрировать (файлы оставить) -... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles - -# Удалить общий модуль -... -ConfigDir src -Object "CommonModule.МойМодуль" -``` - +--- +name: meta-remove +description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации +argument-hint: -Object +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /meta-remove — удаление объекта метаданных + +Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется. + +## Использование + +``` +/meta-remove -Object +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|-------------------------------------------------| +| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) | +| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. | +| DryRun | нет | Только показать что будет удалено, без изменений | +| KeepFiles | нет | Не удалять файлы, только дерегистрировать | +| Force | нет | Удалить несмотря на найденные ссылки | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-remove.ps1" -ConfigDir "<путь>" -Object "Catalog.Товары" +``` + +## Поддерживаемые типы + +Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan, DocumentJournal, Report, DataProcessor, CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService, DefinedType, Role, Subsystem, CommonForm, CommonTemplate, CommonPicture, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, Sequence, FilterCriterion, SettingsStorage, XDTOPackage, WSReference, StyleItem, Language + +## Примеры + +```powershell +# Проверка ссылок + dry run +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -DryRun + +# Удалить объект без ссылок +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" + +# Принудительно удалить несмотря на ссылки +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -Force + +# Только дерегистрировать (файлы оставить) +... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles + +# Удалить общий модуль +... -ConfigDir src -Object "CommonModule.МойМодуль" +``` + diff --git a/.claude/skills/meta-validate/SKILL.md b/.claude/skills/meta-validate/SKILL.md index dc682c1d..207efd42 100644 --- a/.claude/skills/meta-validate/SKILL.md +++ b/.claude/skills/meta-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: meta-validate -description: Валидация объекта метаданных 1С. Используй после создания или модификации объекта конфигурации для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] — pipe-separated paths for batch -allowed-tools: - - Bash - - Read - - Glob ---- - -# /meta-validate — валидация объекта метаданных 1С - -Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|------------|:-----:|---------|-------------------------------------------------| -| ObjectPath | да | — | Путь к XML-файлу или каталогу. Через `\|` для batch | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок (per object) | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Банки|Documents/Заказ" -``` +--- +name: meta-validate +description: Валидация объекта метаданных 1С. Используй после создания или модификации объекта конфигурации для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] — pipe-separated paths for batch +allowed-tools: + - Bash + - Read + - Glob +--- + +# /meta-validate — валидация объекта метаданных 1С + +Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ObjectPath | да | — | Путь к XML-файлу или каталогу. Через `\|` для batch | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок (per object) | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Банки|Documents/Заказ" +``` diff --git a/.claude/skills/meta-validate/scripts/meta-validate.ps1 b/.claude/skills/meta-validate/scripts/meta-validate.ps1 index d189ec7b..2881af28 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.ps1 +++ b/.claude/skills/meta-validate/scripts/meta-validate.ps1 @@ -1,1340 +1,1340 @@ -# meta-validate v1.4 — Validate 1C metadata object structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$ObjectPath, - - [switch]$Detailed, - - [int]$MaxErrors = 30, - - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Batch mode: pipe-separated paths (comma reserved by PowerShell) --- - -$pathList = @($ObjectPath -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -if ($pathList.Count -gt 1) { - $batchOk = 0 - $batchFail = 0 - foreach ($singlePath in $pathList) { - $callArgs = @{ ObjectPath = $singlePath; MaxErrors = $MaxErrors; Verbose = $Detailed } - if ($OutFile) { - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutFile) - $ext = [System.IO.Path]::GetExtension($OutFile) - $dir = Split-Path $OutFile - if (-not $dir) { $dir = "." } - $objLeaf = [System.IO.Path]::GetFileNameWithoutExtension($singlePath) - $callArgs.OutFile = Join-Path $dir "$baseName`_$objLeaf$ext" - } - & $PSCommandPath @callArgs - if ($LASTEXITCODE -eq 0) { $batchOk++ } else { $batchFail++ } - } - Write-Host "" - Write-Host "=== Batch: $($pathList.Count) objects, $batchOk passed, $batchFail failed ===" - if ($batchFail -gt 0) { exit 1 } - exit 0 -} - -# --- Resolve path --- - -if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { - $ObjectPath = Join-Path (Get-Location).Path $ObjectPath -} - -if (Test-Path $ObjectPath -PathType Container) { - $dirName = Split-Path $ObjectPath -Leaf - $candidate = Join-Path $ObjectPath "$dirName.xml" - $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" - if (Test-Path $candidate) { - $ObjectPath = $candidate - } elseif (Test-Path $sibling) { - $ObjectPath = $sibling - } else { - $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) - if ($xmlFiles.Count -gt 0) { - $ObjectPath = $xmlFiles[0].FullName - } else { - Write-Host "[ERROR] No XML file found in directory: $ObjectPath" - exit 1 - } - } -} - -# File not found — check Dir/Name/Name.xml → Dir/Name.xml -if (-not (Test-Path $ObjectPath)) { - $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) - $parentDir = Split-Path $ObjectPath - $parentDirName = Split-Path $parentDir -Leaf - if ($fileName -eq $parentDirName) { - $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" - if (Test-Path $candidate) { $ObjectPath = $candidate } - } -} -if (-not (Test-Path $ObjectPath)) { - Write-Host "[ERROR] File not found: $ObjectPath" - exit 1 -} - -$resolvedPath = (Resolve-Path $ObjectPath).Path - -# --- Detect config directory (for cross-object checks) --- - -$script:configDir = $null -$probe = Split-Path $resolvedPath -for ($depth = 0; $depth -lt 4; $depth++) { - if (-not $probe) { break } - if (Test-Path (Join-Path $probe "Configuration.xml")) { - $script:configDir = $probe - break - } - $probe = Split-Path $probe -} - -# --- Output infrastructure --- - -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$finalize = { - $checks = $script:okCount + $script:errors + $script:warnings - if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: $mdType.$objName ($checks checks) ===" - } else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() - } - Write-Host $result - - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" - } -} - -# --- Reference tables --- - -$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' - -$validTypes = @( - "Catalog","Document","Enum","Constant", - "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", - "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", - "BusinessProcess","Task","ExchangePlan","DocumentJournal", - "Report","DataProcessor", - "CommonModule","ScheduledJob","EventSubscription", - "HTTPService","WebService","DefinedType" -) - -# GeneratedType categories by type -$generatedTypeCategories = @{ - "Catalog" = @("Object","Ref","Selection","List","Manager") - "Document" = @("Object","Ref","Selection","List","Manager") - "Enum" = @("Ref","Manager","List") - "Constant" = @("Manager","ValueManager","ValueKey") - "InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager") - "AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey") - "AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions") - "CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs") - "ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow") - "ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic") - "ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow") - "BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef") - "Task" = @("Object","Ref","Selection","List","Manager") - "ExchangePlan" = @("Object","Ref","Selection","List","Manager") - "DocumentJournal" = @("Selection","List","Manager") - "Report" = @("Object","Manager") - "DataProcessor" = @("Object","Manager") - "DefinedType" = @("DefinedType") -} - -# Types that have NO InternalInfo / GeneratedType -$typesWithoutInternalInfo = @("CommonModule","ScheduledJob","EventSubscription") - -# StandardAttributes by type -$standardAttributesByType = @{ - "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") - "Document" = @("Posted","Ref","DeletionMark","Date","Number") - "Enum" = @("Order","Ref") - "InformationRegister" = @("Active","LineNumber","Recorder","Period") - "AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType") - "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") - "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod") - "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") - "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType") - "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") - "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") - "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") - "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") - "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") -} - -# Types that have StandardAttributes block -$typesWithStdAttrs = @( - "Catalog","Document","Enum", - "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", - "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", - "BusinessProcess","Task","ExchangePlan","DocumentJournal" -) - -# ChildObjects rules: what child element types are valid for each metadata type -$childObjectRules = @{ - "Catalog" = @("Attribute","TabularSection","Form","Template","Command") - "Document" = @("Attribute","TabularSection","Form","Template","Command") - "ExchangePlan" = @("Attribute","TabularSection","Form","Template","Command") - "ChartOfAccounts" = @("Attribute","TabularSection","Form","Template","Command","AccountingFlag","ExtDimensionAccountingFlag") - "ChartOfCharacteristicTypes" = @("Attribute","TabularSection","Form","Template","Command") - "ChartOfCalculationTypes" = @("Attribute","TabularSection","Form","Template","Command") - "BusinessProcess" = @("Attribute","TabularSection","Form","Template","Command") - "Task" = @("Attribute","TabularSection","Form","Template","Command","AddressingAttribute") - "Report" = @("Attribute","TabularSection","Form","Template","Command") - "DataProcessor" = @("Attribute","TabularSection","Form","Template","Command") - "Enum" = @("EnumValue","Form","Template","Command") - "InformationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") - "AccumulationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") - "AccountingRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") - "CalculationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command","Recalculation") - "DocumentJournal" = @("Column","Form","Template","Command") - "HTTPService" = @("URLTemplate") - "WebService" = @("Operation") - "Constant" = @("Form") - "DefinedType" = @() - "CommonModule" = @() - "ScheduledJob" = @() - "EventSubscription" = @() -} - -# Valid enum property values -$validPropertyValues = @{ - "CodeType" = @("String","Number") - "CodeAllowedLength" = @("Variable","Fixed") - "NumberType" = @("String","Number") - "NumberAllowedLength" = @("Variable","Fixed") - "Posting" = @("Allow","Deny") - "RealTimePosting" = @("Allow","Deny") - "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") - "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") - "DataLockControlMode" = @("Automatic","Managed") - "FullTextSearch" = @("Use","DontUse") - "DefaultPresentation" = @("AsDescription","AsCode") - "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") - "EditType" = @("InDialog","InList","BothWays") - "WriteMode" = @("Independent","RecorderSubordinate") - "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") - "RegisterType" = @("Balance","Turnovers") - "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") - "ReuseSessions" = @("DontUse","AutoUse") - "FillChecking" = @("DontCheck","ShowError","ShowWarning") - "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") - "DataHistory" = @("Use","DontUse") - "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") -} - -# Properties forbidden per type (would cause LoadConfigFromFiles error) -$forbiddenProperties = @{ - "ChartOfCharacteristicTypes" = @("CodeType") - "ChartOfAccounts" = @("Autonumbering","Hierarchical") - "ChartOfCalculationTypes" = @("CheckUnique","Autonumbering") - "ExchangePlan" = @("CodeType","CheckUnique","Autonumbering") -} - -# --- 1. Parse XML --- - -Out-Line "" - -$xmlDoc = $null -try { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $false - $xmlDoc.Load($resolvedPath) -} catch { - Out-Line "=== Validation: (parse failed) ===" - Out-Line "" - Report-Error "1. XML parse failed: $($_.Exception.Message)" - & $finalize - exit 1 -} - -# --- 2. Register namespaces --- - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") - -$root = $xmlDoc.DocumentElement - -# --- Check 1: Root structure --- - -$check1Ok = $true - -# Root must be MetaDataObject -if ($root.LocalName -ne "MetaDataObject") { - Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" - & $finalize - exit 1 -} - -$expectedNs = "http://v8.1c.ru/8.3/MDClasses" -if ($root.NamespaceURI -ne $expectedNs) { - Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" - $check1Ok = $false -} - -# Version attribute -$version = $root.GetAttribute("version") -if (-not $version) { - Report-Warn "1. Missing version attribute on MetaDataObject" -} elseif ($version -ne "2.17" -and $version -ne "2.20") { - Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" -} - -# Detect type element — exactly one child element in md namespace -$typeNode = $null -$mdType = "" -$childElements = @() -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { - $childElements += $child - } -} - -if ($childElements.Count -eq 0) { - Report-Error "1. No metadata type element found inside MetaDataObject" - & $finalize - exit 1 -} elseif ($childElements.Count -gt 1) { - Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" - $check1Ok = $false -} - -$typeNode = $childElements[0] -$mdType = $typeNode.LocalName - -if ($validTypes -notcontains $mdType) { - Report-Error "1. Unrecognized metadata type: $mdType" - & $finalize - exit 1 -} - -# UUID on type element -$typeUuid = $typeNode.GetAttribute("uuid") -if (-not $typeUuid) { - Report-Error "1. Missing uuid on <$mdType> element" - $check1Ok = $false -} elseif ($typeUuid -notmatch $guidPattern) { - Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" - $check1Ok = $false -} - -# Get object name early for header -$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) -$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } -$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } - -# Now emit header -$script:output.Insert(0, "=== Validation: $mdType.$objName ===$([Environment]::NewLine)") | Out-Null - -if ($check1Ok) { - Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 2: InternalInfo --- - -$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) - -if ($typesWithoutInternalInfo -contains $mdType) { - # These types should NOT have InternalInfo with GeneratedType - if ($internalInfo) { - $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) - if ($genTypes.Count -gt 0) { - Report-Warn "2. InternalInfo: $mdType should not have GeneratedType entries, found $($genTypes.Count)" - } else { - Report-OK "2. InternalInfo: absent or empty (correct for $mdType)" - } - } else { - Report-OK "2. InternalInfo: absent (correct for $mdType)" - } -} elseif ($generatedTypeCategories.ContainsKey($mdType)) { - $expectedCategories = $generatedTypeCategories[$mdType] - if (-not $internalInfo) { - Report-Error "2. InternalInfo: missing (expected $($expectedCategories.Count) GeneratedType)" - } else { - $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) - $check2Ok = $true - $foundCategories = @() - - foreach ($gt in $genTypes) { - $gtName = $gt.GetAttribute("name") - $gtCategory = $gt.GetAttribute("category") - $foundCategories += $gtCategory - - # Validate name format: Prefix.ObjectName - if ($gtName -and $objName -ne "(unknown)") { - if (-not $gtName.EndsWith(".$objName")) { - Report-Error "2. GeneratedType name '$gtName' does not end with '.$objName'" - $check2Ok = $false - } - } - - # Validate category - if ($expectedCategories -notcontains $gtCategory) { - Report-Warn "2. Unexpected GeneratedType category '$gtCategory' for $mdType" - } - - # Validate TypeId and ValueId UUIDs - $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) - $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) - if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid TypeId UUID in GeneratedType '$gtCategory'" - $check2Ok = $false - } - if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { - Report-Error "2. Invalid ValueId UUID in GeneratedType '$gtCategory'" - $check2Ok = $false - } - } - - # ExchangePlan: check for ThisNode - if ($mdType -eq "ExchangePlan") { - $thisNode = $internalInfo.SelectSingleNode("xr:ThisNode", $ns) - if (-not $thisNode) { - Report-Warn "2. ExchangePlan missing xr:ThisNode in InternalInfo" - } elseif ($thisNode.InnerText -notmatch $guidPattern) { - Report-Error "2. ExchangePlan xr:ThisNode has invalid UUID" - $check2Ok = $false - } - } - - # Check count mismatch - $missingCats = @($expectedCategories | Where-Object { $foundCategories -notcontains $_ }) - if ($missingCats.Count -gt 0) { - Report-Warn "2. Missing GeneratedType categories: $($missingCats -join ', ')" - } - - if ($check2Ok) { - $catList = ($foundCategories | Sort-Object) -join ", " - Report-OK "2. InternalInfo: $($genTypes.Count) GeneratedType ($catList)" - } - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 3: Properties — Name, Synonym --- - -if (-not $propsNode) { - Report-Error "3. Properties block missing" -} else { - $check3Ok = $true - - # Name - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "3. Properties: Name is missing or empty" - $check3Ok = $false - } else { - $nameVal = $nameNode.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" - $check3Ok = $false - } - if ($nameVal.Length -gt 80) { - Report-Warn "3. Properties: Name '$nameVal' is longer than 80 characters ($($nameVal.Length))" - } - } - - # Synonym - $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) - $synPresent = $false - if ($synNode) { - $synItem = $synNode.SelectSingleNode("v8:item", $ns) - if ($synItem) { - $synContent = $synItem.SelectSingleNode("v8:content", $ns) - if ($synContent -and $synContent.InnerText) { - $synPresent = $true - } - } - } - - if ($check3Ok) { - $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } - Report-OK "3. Properties: Name=`"$objName`", $synInfo" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 4: Property values — enum properties --- - -if ($propsNode) { - $enumChecked = 0 - $check4Ok = $true - - foreach ($propName in $validPropertyValues.Keys) { - $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) - if ($propNode -and $propNode.InnerText) { - $val = $propNode.InnerText - $allowed = $validPropertyValues[$propName] - if ($allowed -notcontains $val) { - Report-Error "4. Property '$propName' has invalid value '$val' (allowed: $($allowed -join ', '))" - $check4Ok = $false - } - $enumChecked++ - } - } - - if ($check4Ok) { - Report-OK "4. Property values: $enumChecked enum properties checked" - } -} else { - Report-Warn "4. No Properties block to check" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 5: StandardAttributes --- - -if ($typesWithStdAttrs -contains $mdType) { - $stdAttrNode = $propsNode.SelectSingleNode("md:StandardAttributes", $ns) - if (-not $stdAttrNode) { - # StandardAttributes block is optional for some types (e.g. Enum) - Report-OK "5. StandardAttributes: absent (optional for $mdType)" - } else { - $stdAttrs = $stdAttrNode.SelectNodes("xr:StandardAttribute", $ns) - $expectedStdAttrs = $standardAttributesByType[$mdType] - $check5Ok = $true - - $foundNames = @() - foreach ($sa in $stdAttrs) { - $saName = $sa.GetAttribute("name") - if ($saName) { - $foundNames += $saName - if ($expectedStdAttrs -notcontains $saName) { - # AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} and optional PeriodAdjustment - $isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment")) - # CalculationRegister has conditional period attrs - $isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")) - if (-not $isDynamic -and -not $isCalcDynamic) { - Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType" - } - } - } else { - Report-Error "5. StandardAttribute without 'name' attribute" - $check5Ok = $false - } - } - - if ($expectedStdAttrs) { - $missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ }) - if ($missingAttrs.Count -gt 0) { - Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')" - } - } - - if ($check5Ok) { - Report-OK "5. StandardAttributes: $($stdAttrs.Count) entries" - } - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 6: ChildObjects — allowed element types --- - -$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) -$allowedChildren = $childObjectRules[$mdType] - -if ($childObjNode) { - $check6Ok = $true - $childCounts = @{} - - foreach ($child in $childObjNode.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $childTag = $child.LocalName - - if ($allowedChildren -notcontains $childTag) { - Report-Error "6. ChildObjects: disallowed element '$childTag' for $mdType" - $check6Ok = $false - } - - if (-not $childCounts.ContainsKey($childTag)) { - $childCounts[$childTag] = 0 - } - $childCounts[$childTag]++ - } - - if ($check6Ok) { - $summary = ($childCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " - if ($summary) { - Report-OK "6. ChildObjects types: $summary" - } else { - Report-OK "6. ChildObjects: empty (valid for $mdType)" - } - } -} elseif ($allowedChildren.Count -eq 0) { - Report-OK "6. ChildObjects: absent (correct for $mdType)" -} else { - # Some types may have no children — that's OK - Report-OK "6. ChildObjects: absent" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 7: Attributes/Dimensions/Resources/EnumValues/Columns — UUID, Name, Type --- - -function Check-ChildElement { - param( - [System.Xml.XmlNode]$node, - [string]$kind, - [bool]$requireType - ) - - $uuid = $node.GetAttribute("uuid") - if (-not $uuid) { - Report-Error "7. $kind missing uuid" - return $false - } elseif ($uuid -notmatch $guidPattern) { - Report-Error "7. $kind has invalid uuid '$uuid'" - return $false - } - - $elProps = $node.SelectSingleNode("md:Properties", $ns) - if (-not $elProps) { - Report-Error "7. $kind (uuid=$uuid) missing Properties" - return $false - } - - $elName = $elProps.SelectSingleNode("md:Name", $ns) - if (-not $elName -or -not $elName.InnerText) { - Report-Error "7. $kind (uuid=$uuid) missing or empty Name" - return $false - } - - $nameVal = $elName.InnerText - if ($nameVal -notmatch $identPattern) { - Report-Error "7. $kind '$nameVal' has invalid identifier" - return $false - } - - if ($requireType) { - $typeEl = $elProps.SelectSingleNode("md:Type", $ns) - if (-not $typeEl) { - Report-Error "7. $kind '$nameVal' missing Type block" - return $false - } - $v8Types = $typeEl.SelectNodes("v8:Type", $ns) - $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) - if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { - Report-Error "7. $kind '$nameVal' Type block has no v8:Type or v8:TypeSet" - return $false - } - } - - return $true -} - -if ($childObjNode) { - $check7Ok = $true - $check7Count = 0 - $elementKinds = @("Attribute","Dimension","Resource","EnumValue","Column") - - foreach ($kind in $elementKinds) { - $elements = $childObjNode.SelectNodes("md:$kind", $ns) - $requireType = ($kind -ne "EnumValue" -and $kind -ne "Column") - foreach ($el in $elements) { - if ($script:stopped) { break } - $ok = Check-ChildElement -node $el -kind $kind -requireType $requireType - if (-not $ok) { $check7Ok = $false } - $check7Count++ - } - } - - if ($check7Ok -and $check7Count -gt 0) { - Report-OK "7. Child elements: $check7Count items checked (UUID, Name, Type)" - } elseif ($check7Count -eq 0) { - Report-OK "7. Child elements: none to check" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 7b: Reserved attribute names --- - -$reservedAttrNames = @( - "Ref","DeletionMark","Code","Description","Date","Number","Posted","Parent","Owner", - "IsFolder","Predefined","PredefinedDataName","Recorder","Period","LineNumber","Active", - "Order","Type","OffBalance","Started","Completed","HeadTask","Executed","RoutePoint", - "BusinessProcess","ThisNode","SentNo","ReceivedNo","CalculationType","RegistrationPeriod", - "ReversingEntry","Account","ValueType","ActionPeriodIsBasic" -) - -if ($childObjNode) { - $check7bOk = $true - $attrNodes = $childObjNode.SelectNodes("md:Attribute", $ns) - foreach ($attrNode in $attrNodes) { - $attrProps = $attrNode.SelectSingleNode("md:Properties", $ns) - if ($attrProps) { - $attrNameNode = $attrProps.SelectSingleNode("md:Name", $ns) - if ($attrNameNode -and $attrNameNode.InnerText) { - $an = $attrNameNode.InnerText - if ($reservedAttrNames -contains $an) { - Report-Warn "7b. Attribute '$an' conflicts with a standard attribute name" - $check7bOk = $false - } - } - } - } - if ($check7bOk) { - Report-OK "7b. Reserved attribute names: no conflicts" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 8: Name uniqueness --- - -function Check-Uniqueness { - param( - [System.Xml.XmlNodeList]$nodes, - [string]$kind - ) - - $names = @{} - $hasDupes = $false - - foreach ($node in $nodes) { - $elProps = $node.SelectSingleNode("md:Properties", $ns) - if (-not $elProps) { continue } - $elName = $elProps.SelectSingleNode("md:Name", $ns) - if (-not $elName -or -not $elName.InnerText) { continue } - - $nameVal = $elName.InnerText - if ($names.ContainsKey($nameVal)) { - Report-Error "8. Duplicate $kind name: '$nameVal'" - $hasDupes = $true - } else { - $names[$nameVal] = $true - } - } - - return (-not $hasDupes) -} - -if ($childObjNode) { - $check8Ok = $true - - # Attributes - $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) - if ($attrs.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $attrs -kind "Attribute")) { $check8Ok = $false } - } - - # TabularSections - $tss = $childObjNode.SelectNodes("md:TabularSection", $ns) - if ($tss.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $tss -kind "TabularSection")) { $check8Ok = $false } - } - - # Dimensions - $dims = $childObjNode.SelectNodes("md:Dimension", $ns) - if ($dims.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $dims -kind "Dimension")) { $check8Ok = $false } - } - - # Resources - $ress = $childObjNode.SelectNodes("md:Resource", $ns) - if ($ress.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $ress -kind "Resource")) { $check8Ok = $false } - } - - # EnumValues - $evs = $childObjNode.SelectNodes("md:EnumValue", $ns) - if ($evs.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $evs -kind "EnumValue")) { $check8Ok = $false } - } - - # Columns (DocumentJournal) - $cols = $childObjNode.SelectNodes("md:Column", $ns) - if ($cols.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $cols -kind "Column")) { $check8Ok = $false } - } - - # URLTemplates (HTTPService) - $urlTs = $childObjNode.SelectNodes("md:URLTemplate", $ns) - if ($urlTs.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $urlTs -kind "URLTemplate")) { $check8Ok = $false } - } - - # Operations (WebService) - $ops = $childObjNode.SelectNodes("md:Operation", $ns) - if ($ops.Count -gt 0) { - if (-not (Check-Uniqueness -nodes $ops -kind "Operation")) { $check8Ok = $false } - } - - if ($check8Ok) { - Report-OK "8. Name uniqueness: all names unique" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 9: TabularSections — internal structure --- - -if ($childObjNode) { - $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) - if ($tsSections.Count -gt 0) { - $check9Ok = $true - $tsCount = 0 - - foreach ($ts in $tsSections) { - if ($script:stopped) { break } - $tsCount++ - - # UUID - $tsUuid = $ts.GetAttribute("uuid") - if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { - Report-Error "9. TabularSection #${tsCount}: invalid or missing uuid" - $check9Ok = $false - } - - # Name - $tsProps = $ts.SelectSingleNode("md:Properties", $ns) - $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } - $tsName = if ($tsNameNode) { $tsNameNode.InnerText } else { "(unnamed)" } - - if (-not $tsNameNode -or -not $tsNameNode.InnerText) { - Report-Error "9. TabularSection #${tsCount}: missing or empty Name" - $check9Ok = $false - } - - # InternalInfo with 2 GeneratedType (TabularSection + TabularSectionRow) - $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) - if ($tsIntInfo) { - $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) - if ($tsGens.Count -lt 2) { - Report-Warn "9. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" - } - } - - # Attributes inside TS - $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) - if ($tsChildObj) { - $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) - $tsAttrNames = @{} - foreach ($ta in $tsAttrs) { - $taOk = Check-ChildElement -node $ta -kind "TabularSection '$tsName'.Attribute" -requireType $true - if (-not $taOk) { $check9Ok = $false } - - # Check name uniqueness within TS - $taProps = $ta.SelectSingleNode("md:Properties", $ns) - $taName = if ($taProps) { $taProps.SelectSingleNode("md:Name", $ns) } else { $null } - if ($taName -and $taName.InnerText) { - if ($tsAttrNames.ContainsKey($taName.InnerText)) { - Report-Error "9. Duplicate attribute '$($taName.InnerText)' in TabularSection '$tsName'" - $check9Ok = $false - } else { - $tsAttrNames[$taName.InnerText] = $true - } - } - } - - # StandardAttributes of TS: expect LineNumber - $tsStdAttr = $tsProps.SelectSingleNode("md:StandardAttributes", $ns) - if ($tsStdAttr) { - $tsStdAttrs = $tsStdAttr.SelectNodes("xr:StandardAttribute", $ns) - $hasLineNumber = $false - foreach ($tsa in $tsStdAttrs) { - if ($tsa.GetAttribute("name") -eq "LineNumber") { $hasLineNumber = $true } - } - if (-not $hasLineNumber) { - Report-Warn "9. TabularSection '$tsName': missing LineNumber StandardAttribute" - } - } - } - } - - if ($check9Ok) { - Report-OK "9. TabularSections: $tsCount sections, structure valid" - } - } else { - Report-OK "9. TabularSections: none present" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 10: Cross-property consistency --- - -$check10Ok = $true -$check10Issues = 0 - -if ($propsNode) { - # HierarchyType set but Hierarchical = false - $hierarchical = $propsNode.SelectSingleNode("md:Hierarchical", $ns) - $hierarchyType = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - if ($hierarchical -and $hierarchyType -and $hierarchical.InnerText -eq "false" -and $hierarchyType.InnerText) { - Report-Warn "10. HierarchyType='$($hierarchyType.InnerText)' but Hierarchical=false" - $check10Issues++ - } - - # CommonModule: no context enabled - if ($mdType -eq "CommonModule") { - $contexts = @("Server","ClientManagedApplication","ClientOrdinaryApplication","ExternalConnection","ServerCall","Global") - $anyEnabled = $false - foreach ($ctx in $contexts) { - $ctxNode = $propsNode.SelectSingleNode("md:$ctx", $ns) - if ($ctxNode -and $ctxNode.InnerText -eq "true") { - $anyEnabled = $true - break - } - } - if (-not $anyEnabled) { - Report-Warn "10. CommonModule: no execution context enabled" - $check10Issues++ - } - } - - # EventSubscription: empty Handler - if ($mdType -eq "EventSubscription") { - $handler = $propsNode.SelectSingleNode("md:Handler", $ns) - if (-not $handler -or -not $handler.InnerText.Trim()) { - Report-Error "10. EventSubscription: empty Handler" - $check10Ok = $false - $check10Issues++ - } - - # Empty Source - $source = $propsNode.SelectSingleNode("md:Source", $ns) - $hasSource = $false - if ($source) { - $sourceTypes = $source.SelectNodes("v8:Type", $ns) - if ($sourceTypes.Count -gt 0) { $hasSource = $true } - } - if (-not $hasSource) { - Report-Warn "10. EventSubscription: no Source types specified" - $check10Issues++ - } - } - - # ScheduledJob: empty MethodName - if ($mdType -eq "ScheduledJob") { - $method = $propsNode.SelectSingleNode("md:MethodName", $ns) - if (-not $method -or -not $method.InnerText.Trim()) { - Report-Error "10. ScheduledJob: empty MethodName" - $check10Ok = $false - $check10Issues++ - } - } - - # AccountingRegister: ChartOfAccounts must not be empty - if ($mdType -eq "AccountingRegister") { - $coa = $propsNode.SelectSingleNode("md:ChartOfAccounts", $ns) - if (-not $coa -or -not $coa.InnerText.Trim()) { - Report-Error "10. AccountingRegister: empty ChartOfAccounts" - $check10Ok = $false - $check10Issues++ - Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" - } - } - - # CalculationRegister: ChartOfCalculationTypes must not be empty - if ($mdType -eq "CalculationRegister") { - $coct = $propsNode.SelectSingleNode("md:ChartOfCalculationTypes", $ns) - if (-not $coct -or -not $coct.InnerText.Trim()) { - Report-Error "10. CalculationRegister: empty ChartOfCalculationTypes" - $check10Ok = $false - $check10Issues++ - Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`"" - } - } - - # BusinessProcess: Task should not be empty - if ($mdType -eq "BusinessProcess") { - $taskProp = $propsNode.SelectSingleNode("md:Task", $ns) - if (-not $taskProp -or -not $taskProp.InnerText.Trim()) { - Report-Warn "10. BusinessProcess: empty Task reference" - $check10Issues++ - Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"Task=Task.XXX`"" - } - } - - # CalculationRegister: ActionPeriod=true requires non-empty Schedule - if ($mdType -eq "CalculationRegister") { - $actionPeriod = $propsNode.SelectSingleNode("md:ActionPeriod", $ns) - if ($actionPeriod -and $actionPeriod.InnerText -eq "true") { - $schedule = $propsNode.SelectSingleNode("md:Schedule", $ns) - if (-not $schedule -or -not $schedule.InnerText.Trim()) { - Report-Warn "10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register" - $check10Issues++ - } - } - } - - # DocumentJournal: RegisteredDocuments should not be empty - if ($mdType -eq "DocumentJournal") { - $regDocs = $propsNode.SelectSingleNode("md:RegisteredDocuments", $ns) - $hasRegDocs = $false - if ($regDocs) { - $items = $regDocs.SelectNodes("v8:Type", $ns) - if ($items.Count -gt 0) { $hasRegDocs = $true } - } - if (-not $hasRegDocs) { - Report-Warn "10. DocumentJournal: no RegisteredDocuments specified" - $check10Issues++ - } - } - - # ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0 - if ($mdType -eq "ChartOfAccounts") { - $maxExtDim = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) - if ($maxExtDim -and [int]$maxExtDim.InnerText -gt 0) { - $edt = $propsNode.SelectSingleNode("md:ExtDimensionTypes", $ns) - if (-not $edt -or -not $edt.InnerText.Trim()) { - Report-Warn "10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty" - $check10Issues++ - Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`"" - } - } - } - - # Register: must have at least one Dimension or Resource (platform rejects empty registers) - $regTypesAll = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") - if ($regTypesAll -contains $mdType -and $childObjNode) { - $dims = $childObjNode.SelectNodes("md:Dimension", $ns).Count - $ress = $childObjNode.SelectNodes("md:Resource", $ns).Count - $attrs = $childObjNode.SelectNodes("md:Attribute", $ns).Count - if (($dims + $ress + $attrs) -eq 0) { - Report-Warn "10. $mdType`: no Dimensions, Resources, or Attributes — platform will reject" - $check10Issues++ - } - } - - # Document: RegisterRecords references should point to existing objects in config - if ($mdType -eq "Document" -and $script:configDir) { - $regRecords = $propsNode.SelectSingleNode("md:RegisterRecords", $ns) - if ($regRecords) { - $items = $regRecords.SelectNodes("xr:Item", $ns) - foreach ($item in $items) { - $refVal = $item.InnerText.Trim() - if (-not $refVal) { continue } - # Parse "AccumulationRegister.Name" → dir AccumulationRegisters/Name - $parts = $refVal -split '\.',2 - if ($parts.Count -eq 2) { - $refType = $parts[0]; $refName = $parts[1] - $dirMap = @{ - "AccumulationRegister"="AccumulationRegisters"; "InformationRegister"="InformationRegisters" - "AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters" - } - $refDir = $dirMap[$refType] - if ($refDir) { - $refPath = Join-Path $script:configDir "$refDir/$refName" - $refXml = Join-Path $script:configDir "$refDir/$refName.xml" - if (-not (Test-Path $refPath) -and -not (Test-Path $refXml)) { - Report-Warn "10. Document.RegisterRecords references '$refVal' but object not found in config" - $check10Issues++ - } - } - } - } - } - } - - # Register: must have at least one registrar document - $registerTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") - if ($registerTypes -contains $mdType -and $script:configDir -and $objName -ne "(unknown)") { - $needsRegistrar = $true - # InformationRegister with WriteMode=Independent does not need a registrar - if ($mdType -eq "InformationRegister") { - $writeMode = $propsNode.SelectSingleNode("md:WriteMode", $ns) - if (-not $writeMode -or $writeMode.InnerText -ne "RecorderSubordinate") { - $needsRegistrar = $false - } - } - if ($needsRegistrar) { - $regRef = "$mdType.$objName" - $docsDir = Join-Path $script:configDir "Documents" - $hasRegistrar = $false - if (Test-Path $docsDir) { - $docXmls = Get-ChildItem $docsDir -Filter "*.xml" -File -ErrorAction SilentlyContinue - foreach ($docXml in $docXmls) { - $content = [System.IO.File]::ReadAllText($docXml.FullName, [System.Text.Encoding]::UTF8) - if ($content.Contains($regRef)) { - $hasRegistrar = $true - break - } - } - } - if (-not $hasRegistrar) { - Report-Warn "10. $mdType`: no registrar document found (none references '$regRef' in RegisterRecords)" - $check10Issues++ - } - } - } -} - -if ($check10Ok -and $check10Issues -eq 0) { - Report-OK "10. Cross-property consistency" -} elseif ($check10Ok) { - # Had warnings but no errors — already reported -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 11: HTTPService/WebService nested structure --- - -if ($mdType -eq "HTTPService" -and $childObjNode) { - $urlTemplates = $childObjNode.SelectNodes("md:URLTemplate", $ns) - $check11Ok = $true - $methodCount = 0 - - $validHTTPMethods = @("GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","MERGE","CONNECT") - - foreach ($ut in $urlTemplates) { - if ($script:stopped) { break } - - $utProps = $ut.SelectSingleNode("md:Properties", $ns) - $utNameNode = if ($utProps) { $utProps.SelectSingleNode("md:Name", $ns) } else { $null } - $utName = if ($utNameNode) { $utNameNode.InnerText } else { "(unnamed)" } - - # Template property - $tpl = if ($utProps) { $utProps.SelectSingleNode("md:Template", $ns) } else { $null } - if (-not $tpl -or -not $tpl.InnerText.Trim()) { - Report-Error "11. HTTPService URLTemplate '$utName': empty Template" - $check11Ok = $false - } - - # Methods inside URLTemplate - $utChildObj = $ut.SelectSingleNode("md:ChildObjects", $ns) - if ($utChildObj) { - $methods = $utChildObj.SelectNodes("md:Method", $ns) - foreach ($m in $methods) { - $methodCount++ - $mProps = $m.SelectSingleNode("md:Properties", $ns) - if ($mProps) { - $httpMethod = $mProps.SelectSingleNode("md:HTTPMethod", $ns) - if ($httpMethod -and $httpMethod.InnerText) { - if ($validHTTPMethods -notcontains $httpMethod.InnerText) { - Report-Error "11. HTTPService URLTemplate '$utName': invalid HTTPMethod '$($httpMethod.InnerText)'" - $check11Ok = $false - } - } else { - Report-Error "11. HTTPService URLTemplate '$utName': Method missing HTTPMethod" - $check11Ok = $false - } - } - } - } - } - - if ($check11Ok) { - Report-OK "11. HTTPService: $($urlTemplates.Count) URLTemplate(s), $methodCount method(s)" - } -} elseif ($mdType -eq "WebService" -and $childObjNode) { - $operations = $childObjNode.SelectNodes("md:Operation", $ns) - $check11Ok = $true - $paramCount = 0 - - $validDirections = @("In","Out","InOut") - - foreach ($op in $operations) { - if ($script:stopped) { break } - - $opProps = $op.SelectSingleNode("md:Properties", $ns) - $opNameNode = if ($opProps) { $opProps.SelectSingleNode("md:Name", $ns) } else { $null } - $opName = if ($opNameNode) { $opNameNode.InnerText } else { "(unnamed)" } - - # ReturnType — XDTOReturningValueType - $retType = if ($opProps) { $opProps.SelectSingleNode("md:XDTOReturningValueType", $ns) } else { $null } - if (-not $retType -or -not $retType.InnerText.Trim()) { - Report-Warn "11. WebService Operation '$opName': no XDTOReturningValueType" - } - - # Parameters inside Operation - $opChildObj = $op.SelectSingleNode("md:ChildObjects", $ns) - if ($opChildObj) { - $params = $opChildObj.SelectNodes("md:Parameter", $ns) - foreach ($p in $params) { - $paramCount++ - $pProps = $p.SelectSingleNode("md:Properties", $ns) - if ($pProps) { - $dir = $pProps.SelectSingleNode("md:TransferDirection", $ns) - if ($dir -and $dir.InnerText -and $validDirections -notcontains $dir.InnerText) { - Report-Error "11. WebService Operation '$opName': Parameter has invalid TransferDirection '$($dir.InnerText)'" - $check11Ok = $false - } - } - } - } - } - - if ($check11Ok) { - Report-OK "11. WebService: $($operations.Count) operation(s), $paramCount parameter(s)" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 12: Forbidden properties per type --- - -if ($propsNode -and $forbiddenProperties.ContainsKey($mdType)) { - $forbidden = $forbiddenProperties[$mdType] - $check12Ok = $true - foreach ($fp in $forbidden) { - $fpNode = $propsNode.SelectSingleNode("md:$fp", $ns) - if ($fpNode) { - Report-Error "12. Forbidden property '$fp' present in $mdType (will fail on LoadConfigFromFiles)" - $check12Ok = $false - } - } - if ($check12Ok) { - Report-OK "12. Forbidden properties: none found" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 13: Method reference validation (EventSubscription.Handler, ScheduledJob.MethodName) --- - -if ($propsNode -and $mdType -in @("EventSubscription","ScheduledJob") -and $script:configDir) { - $check13Ok = $true - $methodRef = $null - $propLabel = $null - - if ($mdType -eq "EventSubscription") { - $hNode = $propsNode.SelectSingleNode("md:Handler", $ns) - if ($hNode) { $methodRef = $hNode.InnerText.Trim() } - $propLabel = "Handler" - } elseif ($mdType -eq "ScheduledJob") { - $mNode = $propsNode.SelectSingleNode("md:MethodName", $ns) - if ($mNode) { $methodRef = $mNode.InnerText.Trim() } - $propLabel = "MethodName" - } - - if ($methodRef) { - $parts = $methodRef.Split('.') - # Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy) - if ($parts.Count -eq 3 -and $parts[0] -eq "CommonModule") { - $cmName = $parts[1] - $procName = $parts[2] - } elseif ($parts.Count -eq 2) { - $cmName = $parts[0] - $procName = $parts[1] - } else { - Report-Error "13. ${mdType}.${propLabel} = '$methodRef': expected format 'CommonModule.ModuleName.ProcedureName'" - $check13Ok = $false - $cmName = $null - $procName = $null - } - if ($cmName) { - $cmXml = Join-Path (Join-Path $script:configDir "CommonModules") "$cmName.xml" - if (-not (Test-Path $cmXml)) { - Report-Error "13. ${mdType}.${propLabel}: CommonModule '$cmName' not found (expected $cmXml)" - $check13Ok = $false - } else { - # Check BSL file for exported procedure - $bslPath = Join-Path (Join-Path (Join-Path $script:configDir "CommonModules") $cmName) "Ext/Module.bsl" - if (Test-Path $bslPath) { - $bslContent = [System.IO.File]::ReadAllText($bslPath, [System.Text.Encoding]::UTF8) - # Match: Procedure/Function ProcName(...) Export or Процедура/Функция ProcName(...) Экспорт - $exportPattern = "(?mi)^[\s]*(Procedure|Function|Процедура|Функция)\s+$([regex]::Escape($procName))\s*\(.*\)\s+(Export|Экспорт)" - if (-not [regex]::IsMatch($bslContent, $exportPattern)) { - Report-Warn "13. ${mdType}.${propLabel}: procedure '$procName' not found as exported in CommonModule '$cmName'" - $check13Ok = $false - } - } else { - Report-Warn "13. ${mdType}.${propLabel}: BSL file not found ($bslPath), cannot verify procedure" - } - } - } - } - - if ($check13Ok) { - Report-OK "13. Method reference: $propLabel = '$methodRef'" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Check 14: DocumentJournal Column content --- - -if ($mdType -eq "DocumentJournal" -and $childObjNode) { - $columns = $childObjNode.SelectNodes("md:Column", $ns) - $check14Ok = $true - $colCount = 0 - $emptyRefCount = 0 - - foreach ($col in $columns) { - $colCount++ - $colProps = $col.SelectSingleNode("md:Properties", $ns) - $colNameNode = if ($colProps) { $colProps.SelectSingleNode("md:Name", $ns) } else { $null } - $colName = if ($colNameNode) { $colNameNode.InnerText } else { "(unnamed)" } - - $refs = if ($colProps) { $colProps.SelectSingleNode("md:References", $ns) } else { $null } - $hasItems = $false - if ($refs) { - $items = $refs.SelectNodes("xr:Item", $ns) - if ($items.Count -gt 0) { $hasItems = $true } - } - if (-not $hasItems) { - Report-Error "14. DocumentJournal Column '$colName': empty References (will fail on LoadConfigFromFiles)" - $check14Ok = $false - $emptyRefCount++ - } - } - - if ($check14Ok -and $colCount -gt 0) { - Report-OK "14. DocumentJournal Columns: $colCount column(s), all have References" - } elseif ($colCount -eq 0) { - Report-OK "14. DocumentJournal Columns: none" - } -} - -# --- Final output --- - -& $finalize - -if ($script:errors -gt 0) { - exit 1 -} -exit 0 +# meta-validate v1.4 — Validate 1C metadata object structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ObjectPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Batch mode: pipe-separated paths (comma reserved by PowerShell) --- + +$pathList = @($ObjectPath -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +if ($pathList.Count -gt 1) { + $batchOk = 0 + $batchFail = 0 + foreach ($singlePath in $pathList) { + $callArgs = @{ ObjectPath = $singlePath; MaxErrors = $MaxErrors; Verbose = $Detailed } + if ($OutFile) { + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutFile) + $ext = [System.IO.Path]::GetExtension($OutFile) + $dir = Split-Path $OutFile + if (-not $dir) { $dir = "." } + $objLeaf = [System.IO.Path]::GetFileNameWithoutExtension($singlePath) + $callArgs.OutFile = Join-Path $dir "$baseName`_$objLeaf$ext" + } + & $PSCommandPath @callArgs + if ($LASTEXITCODE -eq 0) { $batchOk++ } else { $batchFail++ } + } + Write-Host "" + Write-Host "=== Batch: $($pathList.Count) objects, $batchOk passed, $batchFail failed ===" + if ($batchFail -gt 0) { exit 1 } + exit 0 +} + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } elseif (Test-Path $sibling) { + $ObjectPath = $sibling + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $ObjectPath)) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) + $parentDir = Split-Path $ObjectPath + $parentDirName = Split-Path $parentDir -Leaf + if ($fileName -eq $parentDirName) { + $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ObjectPath).Path + +# --- Detect config directory (for cross-object checks) --- + +$script:configDir = $null +$probe = Split-Path $resolvedPath +for ($depth = 0; $depth -lt 4; $depth++) { + if (-not $probe) { break } + if (Test-Path (Join-Path $probe "Configuration.xml")) { + $script:configDir = $probe + break + } + $probe = Split-Path $probe +} + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: $mdType.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- + +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +$validTypes = @( + "Catalog","Document","Enum","Constant", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", + "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","ExchangePlan","DocumentJournal", + "Report","DataProcessor", + "CommonModule","ScheduledJob","EventSubscription", + "HTTPService","WebService","DefinedType" +) + +# GeneratedType categories by type +$generatedTypeCategories = @{ + "Catalog" = @("Object","Ref","Selection","List","Manager") + "Document" = @("Object","Ref","Selection","List","Manager") + "Enum" = @("Ref","Manager","List") + "Constant" = @("Manager","ValueManager","ValueKey") + "InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager") + "AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey") + "AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions") + "CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs") + "ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow") + "ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic") + "ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow") + "BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef") + "Task" = @("Object","Ref","Selection","List","Manager") + "ExchangePlan" = @("Object","Ref","Selection","List","Manager") + "DocumentJournal" = @("Selection","List","Manager") + "Report" = @("Object","Manager") + "DataProcessor" = @("Object","Manager") + "DefinedType" = @("DefinedType") +} + +# Types that have NO InternalInfo / GeneratedType +$typesWithoutInternalInfo = @("CommonModule","ScheduledJob","EventSubscription") + +# StandardAttributes by type +$standardAttributesByType = @{ + "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") + "Document" = @("Posted","Ref","DeletionMark","Date","Number") + "Enum" = @("Order","Ref") + "InformationRegister" = @("Active","LineNumber","Recorder","Period") + "AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType") + "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") + "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod") + "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") + "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType") + "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") + "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") + "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") + "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") + "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") +} + +# Types that have StandardAttributes block +$typesWithStdAttrs = @( + "Catalog","Document","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister", + "ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","ExchangePlan","DocumentJournal" +) + +# ChildObjects rules: what child element types are valid for each metadata type +$childObjectRules = @{ + "Catalog" = @("Attribute","TabularSection","Form","Template","Command") + "Document" = @("Attribute","TabularSection","Form","Template","Command") + "ExchangePlan" = @("Attribute","TabularSection","Form","Template","Command") + "ChartOfAccounts" = @("Attribute","TabularSection","Form","Template","Command","AccountingFlag","ExtDimensionAccountingFlag") + "ChartOfCharacteristicTypes" = @("Attribute","TabularSection","Form","Template","Command") + "ChartOfCalculationTypes" = @("Attribute","TabularSection","Form","Template","Command") + "BusinessProcess" = @("Attribute","TabularSection","Form","Template","Command") + "Task" = @("Attribute","TabularSection","Form","Template","Command","AddressingAttribute") + "Report" = @("Attribute","TabularSection","Form","Template","Command") + "DataProcessor" = @("Attribute","TabularSection","Form","Template","Command") + "Enum" = @("EnumValue","Form","Template","Command") + "InformationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "AccumulationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "AccountingRegister" = @("Dimension","Resource","Attribute","Form","Template","Command") + "CalculationRegister" = @("Dimension","Resource","Attribute","Form","Template","Command","Recalculation") + "DocumentJournal" = @("Column","Form","Template","Command") + "HTTPService" = @("URLTemplate") + "WebService" = @("Operation") + "Constant" = @("Form") + "DefinedType" = @() + "CommonModule" = @() + "ScheduledJob" = @() + "EventSubscription" = @() +} + +# Valid enum property values +$validPropertyValues = @{ + "CodeType" = @("String","Number") + "CodeAllowedLength" = @("Variable","Fixed") + "NumberType" = @("String","Number") + "NumberAllowedLength" = @("Variable","Fixed") + "Posting" = @("Allow","Deny") + "RealTimePosting" = @("Allow","Deny") + "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") + "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") + "DataLockControlMode" = @("Automatic","Managed") + "FullTextSearch" = @("Use","DontUse") + "DefaultPresentation" = @("AsDescription","AsCode") + "HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly") + "EditType" = @("InDialog","InList","BothWays") + "WriteMode" = @("Independent","RecorderSubordinate") + "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") + "RegisterType" = @("Balance","Turnovers") + "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") + "ReuseSessions" = @("DontUse","AutoUse") + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") + "DataHistory" = @("Use","DontUse") + "DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod") +} + +# Properties forbidden per type (would cause LoadConfigFromFiles error) +$forbiddenProperties = @{ + "ChartOfCharacteristicTypes" = @("CodeType") + "ChartOfAccounts" = @("Autonumbering","Hierarchical") + "ChartOfCalculationTypes" = @("CheckUnique","Autonumbering") + "ExchangePlan" = @("CodeType","CheckUnique","Autonumbering") +} + +# --- 1. Parse XML --- + +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- 2. Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- + +$check1Ok = $true + +# Root must be MetaDataObject +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +# Version attribute +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20") { + Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" +} + +# Detect type element — exactly one child element in md namespace +$typeNode = $null +$mdType = "" +$childElements = @() +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { + $childElements += $child + } +} + +if ($childElements.Count -eq 0) { + Report-Error "1. No metadata type element found inside MetaDataObject" + & $finalize + exit 1 +} elseif ($childElements.Count -gt 1) { + Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" + $check1Ok = $false +} + +$typeNode = $childElements[0] +$mdType = $typeNode.LocalName + +if ($validTypes -notcontains $mdType) { + Report-Error "1. Unrecognized metadata type: $mdType" + & $finalize + exit 1 +} + +# UUID on type element +$typeUuid = $typeNode.GetAttribute("uuid") +if (-not $typeUuid) { + Report-Error "1. Missing uuid on <$mdType> element" + $check1Ok = $false +} elseif ($typeUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" + $check1Ok = $false +} + +# Get object name early for header +$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +# Now emit header +$script:output.Insert(0, "=== Validation: $mdType.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- + +$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) + +if ($typesWithoutInternalInfo -contains $mdType) { + # These types should NOT have InternalInfo with GeneratedType + if ($internalInfo) { + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + if ($genTypes.Count -gt 0) { + Report-Warn "2. InternalInfo: $mdType should not have GeneratedType entries, found $($genTypes.Count)" + } else { + Report-OK "2. InternalInfo: absent or empty (correct for $mdType)" + } + } else { + Report-OK "2. InternalInfo: absent (correct for $mdType)" + } +} elseif ($generatedTypeCategories.ContainsKey($mdType)) { + $expectedCategories = $generatedTypeCategories[$mdType] + if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing (expected $($expectedCategories.Count) GeneratedType)" + } else { + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + $check2Ok = $true + $foundCategories = @() + + foreach ($gt in $genTypes) { + $gtName = $gt.GetAttribute("name") + $gtCategory = $gt.GetAttribute("category") + $foundCategories += $gtCategory + + # Validate name format: Prefix.ObjectName + if ($gtName -and $objName -ne "(unknown)") { + if (-not $gtName.EndsWith(".$objName")) { + Report-Error "2. GeneratedType name '$gtName' does not end with '.$objName'" + $check2Ok = $false + } + } + + # Validate category + if ($expectedCategories -notcontains $gtCategory) { + Report-Warn "2. Unexpected GeneratedType category '$gtCategory' for $mdType" + } + + # Validate TypeId and ValueId UUIDs + $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) + $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) + if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid TypeId UUID in GeneratedType '$gtCategory'" + $check2Ok = $false + } + if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ValueId UUID in GeneratedType '$gtCategory'" + $check2Ok = $false + } + } + + # ExchangePlan: check for ThisNode + if ($mdType -eq "ExchangePlan") { + $thisNode = $internalInfo.SelectSingleNode("xr:ThisNode", $ns) + if (-not $thisNode) { + Report-Warn "2. ExchangePlan missing xr:ThisNode in InternalInfo" + } elseif ($thisNode.InnerText -notmatch $guidPattern) { + Report-Error "2. ExchangePlan xr:ThisNode has invalid UUID" + $check2Ok = $false + } + } + + # Check count mismatch + $missingCats = @($expectedCategories | Where-Object { $foundCategories -notcontains $_ }) + if ($missingCats.Count -gt 0) { + Report-Warn "2. Missing GeneratedType categories: $($missingCats -join ', ')" + } + + if ($check2Ok) { + $catList = ($foundCategories | Sort-Object) -join ", " + Report-OK "2. InternalInfo: $($genTypes.Count) GeneratedType ($catList)" + } + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties — Name, Synonym --- + +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + if ($nameVal.Length -gt 80) { + Report-Warn "3. Properties: Name '$nameVal' is longer than 80 characters ($($nameVal.Length))" + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { + $synPresent = $true + } + } + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Property values — enum properties --- + +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validPropertyValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validPropertyValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val' (allowed: $($allowed -join ', '))" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: StandardAttributes --- + +if ($typesWithStdAttrs -contains $mdType) { + $stdAttrNode = $propsNode.SelectSingleNode("md:StandardAttributes", $ns) + if (-not $stdAttrNode) { + # StandardAttributes block is optional for some types (e.g. Enum) + Report-OK "5. StandardAttributes: absent (optional for $mdType)" + } else { + $stdAttrs = $stdAttrNode.SelectNodes("xr:StandardAttribute", $ns) + $expectedStdAttrs = $standardAttributesByType[$mdType] + $check5Ok = $true + + $foundNames = @() + foreach ($sa in $stdAttrs) { + $saName = $sa.GetAttribute("name") + if ($saName) { + $foundNames += $saName + if ($expectedStdAttrs -notcontains $saName) { + # AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} and optional PeriodAdjustment + $isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment")) + # CalculationRegister has conditional period attrs + $isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")) + if (-not $isDynamic -and -not $isCalcDynamic) { + Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType" + } + } + } else { + Report-Error "5. StandardAttribute without 'name' attribute" + $check5Ok = $false + } + } + + if ($expectedStdAttrs) { + $missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ }) + if ($missingAttrs.Count -gt 0) { + Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')" + } + } + + if ($check5Ok) { + Report-OK "5. StandardAttributes: $($stdAttrs.Count) entries" + } + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: ChildObjects — allowed element types --- + +$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$allowedChildren = $childObjectRules[$mdType] + +if ($childObjNode) { + $check6Ok = $true + $childCounts = @{} + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTag = $child.LocalName + + if ($allowedChildren -notcontains $childTag) { + Report-Error "6. ChildObjects: disallowed element '$childTag' for $mdType" + $check6Ok = $false + } + + if (-not $childCounts.ContainsKey($childTag)) { + $childCounts[$childTag] = 0 + } + $childCounts[$childTag]++ + } + + if ($check6Ok) { + $summary = ($childCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " + if ($summary) { + Report-OK "6. ChildObjects types: $summary" + } else { + Report-OK "6. ChildObjects: empty (valid for $mdType)" + } + } +} elseif ($allowedChildren.Count -eq 0) { + Report-OK "6. ChildObjects: absent (correct for $mdType)" +} else { + # Some types may have no children — that's OK + Report-OK "6. ChildObjects: absent" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Attributes/Dimensions/Resources/EnumValues/Columns — UUID, Name, Type --- + +function Check-ChildElement { + param( + [System.Xml.XmlNode]$node, + [string]$kind, + [bool]$requireType + ) + + $uuid = $node.GetAttribute("uuid") + if (-not $uuid) { + Report-Error "7. $kind missing uuid" + return $false + } elseif ($uuid -notmatch $guidPattern) { + Report-Error "7. $kind has invalid uuid '$uuid'" + return $false + } + + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { + Report-Error "7. $kind (uuid=$uuid) missing Properties" + return $false + } + + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { + Report-Error "7. $kind (uuid=$uuid) missing or empty Name" + return $false + } + + $nameVal = $elName.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "7. $kind '$nameVal' has invalid identifier" + return $false + } + + if ($requireType) { + $typeEl = $elProps.SelectSingleNode("md:Type", $ns) + if (-not $typeEl) { + Report-Error "7. $kind '$nameVal' missing Type block" + return $false + } + $v8Types = $typeEl.SelectNodes("v8:Type", $ns) + $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) + if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { + Report-Error "7. $kind '$nameVal' Type block has no v8:Type or v8:TypeSet" + return $false + } + } + + return $true +} + +if ($childObjNode) { + $check7Ok = $true + $check7Count = 0 + $elementKinds = @("Attribute","Dimension","Resource","EnumValue","Column") + + foreach ($kind in $elementKinds) { + $elements = $childObjNode.SelectNodes("md:$kind", $ns) + $requireType = ($kind -ne "EnumValue" -and $kind -ne "Column") + foreach ($el in $elements) { + if ($script:stopped) { break } + $ok = Check-ChildElement -node $el -kind $kind -requireType $requireType + if (-not $ok) { $check7Ok = $false } + $check7Count++ + } + } + + if ($check7Ok -and $check7Count -gt 0) { + Report-OK "7. Child elements: $check7Count items checked (UUID, Name, Type)" + } elseif ($check7Count -eq 0) { + Report-OK "7. Child elements: none to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7b: Reserved attribute names --- + +$reservedAttrNames = @( + "Ref","DeletionMark","Code","Description","Date","Number","Posted","Parent","Owner", + "IsFolder","Predefined","PredefinedDataName","Recorder","Period","LineNumber","Active", + "Order","Type","OffBalance","Started","Completed","HeadTask","Executed","RoutePoint", + "BusinessProcess","ThisNode","SentNo","ReceivedNo","CalculationType","RegistrationPeriod", + "ReversingEntry","Account","ValueType","ActionPeriodIsBasic" +) + +if ($childObjNode) { + $check7bOk = $true + $attrNodes = $childObjNode.SelectNodes("md:Attribute", $ns) + foreach ($attrNode in $attrNodes) { + $attrProps = $attrNode.SelectSingleNode("md:Properties", $ns) + if ($attrProps) { + $attrNameNode = $attrProps.SelectSingleNode("md:Name", $ns) + if ($attrNameNode -and $attrNameNode.InnerText) { + $an = $attrNameNode.InnerText + if ($reservedAttrNames -contains $an) { + Report-Warn "7b. Attribute '$an' conflicts with a standard attribute name" + $check7bOk = $false + } + } + } + } + if ($check7bOk) { + Report-OK "7b. Reserved attribute names: no conflicts" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Name uniqueness --- + +function Check-Uniqueness { + param( + [System.Xml.XmlNodeList]$nodes, + [string]$kind + ) + + $names = @{} + $hasDupes = $false + + foreach ($node in $nodes) { + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { continue } + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { continue } + + $nameVal = $elName.InnerText + if ($names.ContainsKey($nameVal)) { + Report-Error "8. Duplicate $kind name: '$nameVal'" + $hasDupes = $true + } else { + $names[$nameVal] = $true + } + } + + return (-not $hasDupes) +} + +if ($childObjNode) { + $check8Ok = $true + + # Attributes + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) + if ($attrs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $attrs -kind "Attribute")) { $check8Ok = $false } + } + + # TabularSections + $tss = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tss.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $tss -kind "TabularSection")) { $check8Ok = $false } + } + + # Dimensions + $dims = $childObjNode.SelectNodes("md:Dimension", $ns) + if ($dims.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $dims -kind "Dimension")) { $check8Ok = $false } + } + + # Resources + $ress = $childObjNode.SelectNodes("md:Resource", $ns) + if ($ress.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $ress -kind "Resource")) { $check8Ok = $false } + } + + # EnumValues + $evs = $childObjNode.SelectNodes("md:EnumValue", $ns) + if ($evs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $evs -kind "EnumValue")) { $check8Ok = $false } + } + + # Columns (DocumentJournal) + $cols = $childObjNode.SelectNodes("md:Column", $ns) + if ($cols.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $cols -kind "Column")) { $check8Ok = $false } + } + + # URLTemplates (HTTPService) + $urlTs = $childObjNode.SelectNodes("md:URLTemplate", $ns) + if ($urlTs.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $urlTs -kind "URLTemplate")) { $check8Ok = $false } + } + + # Operations (WebService) + $ops = $childObjNode.SelectNodes("md:Operation", $ns) + if ($ops.Count -gt 0) { + if (-not (Check-Uniqueness -nodes $ops -kind "Operation")) { $check8Ok = $false } + } + + if ($check8Ok) { + Report-OK "8. Name uniqueness: all names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: TabularSections — internal structure --- + +if ($childObjNode) { + $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tsSections.Count -gt 0) { + $check9Ok = $true + $tsCount = 0 + + foreach ($ts in $tsSections) { + if ($script:stopped) { break } + $tsCount++ + + # UUID + $tsUuid = $ts.GetAttribute("uuid") + if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { + Report-Error "9. TabularSection #${tsCount}: invalid or missing uuid" + $check9Ok = $false + } + + # Name + $tsProps = $ts.SelectSingleNode("md:Properties", $ns) + $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } + $tsName = if ($tsNameNode) { $tsNameNode.InnerText } else { "(unnamed)" } + + if (-not $tsNameNode -or -not $tsNameNode.InnerText) { + Report-Error "9. TabularSection #${tsCount}: missing or empty Name" + $check9Ok = $false + } + + # InternalInfo with 2 GeneratedType (TabularSection + TabularSectionRow) + $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) + if ($tsIntInfo) { + $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) + if ($tsGens.Count -lt 2) { + Report-Warn "9. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" + } + } + + # Attributes inside TS + $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) + if ($tsChildObj) { + $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) + $tsAttrNames = @{} + foreach ($ta in $tsAttrs) { + $taOk = Check-ChildElement -node $ta -kind "TabularSection '$tsName'.Attribute" -requireType $true + if (-not $taOk) { $check9Ok = $false } + + # Check name uniqueness within TS + $taProps = $ta.SelectSingleNode("md:Properties", $ns) + $taName = if ($taProps) { $taProps.SelectSingleNode("md:Name", $ns) } else { $null } + if ($taName -and $taName.InnerText) { + if ($tsAttrNames.ContainsKey($taName.InnerText)) { + Report-Error "9. Duplicate attribute '$($taName.InnerText)' in TabularSection '$tsName'" + $check9Ok = $false + } else { + $tsAttrNames[$taName.InnerText] = $true + } + } + } + + # StandardAttributes of TS: expect LineNumber + $tsStdAttr = $tsProps.SelectSingleNode("md:StandardAttributes", $ns) + if ($tsStdAttr) { + $tsStdAttrs = $tsStdAttr.SelectNodes("xr:StandardAttribute", $ns) + $hasLineNumber = $false + foreach ($tsa in $tsStdAttrs) { + if ($tsa.GetAttribute("name") -eq "LineNumber") { $hasLineNumber = $true } + } + if (-not $hasLineNumber) { + Report-Warn "9. TabularSection '$tsName': missing LineNumber StandardAttribute" + } + } + } + } + + if ($check9Ok) { + Report-OK "9. TabularSections: $tsCount sections, structure valid" + } + } else { + Report-OK "9. TabularSections: none present" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 10: Cross-property consistency --- + +$check10Ok = $true +$check10Issues = 0 + +if ($propsNode) { + # HierarchyType set but Hierarchical = false + $hierarchical = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $hierarchyType = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + if ($hierarchical -and $hierarchyType -and $hierarchical.InnerText -eq "false" -and $hierarchyType.InnerText) { + Report-Warn "10. HierarchyType='$($hierarchyType.InnerText)' but Hierarchical=false" + $check10Issues++ + } + + # CommonModule: no context enabled + if ($mdType -eq "CommonModule") { + $contexts = @("Server","ClientManagedApplication","ClientOrdinaryApplication","ExternalConnection","ServerCall","Global") + $anyEnabled = $false + foreach ($ctx in $contexts) { + $ctxNode = $propsNode.SelectSingleNode("md:$ctx", $ns) + if ($ctxNode -and $ctxNode.InnerText -eq "true") { + $anyEnabled = $true + break + } + } + if (-not $anyEnabled) { + Report-Warn "10. CommonModule: no execution context enabled" + $check10Issues++ + } + } + + # EventSubscription: empty Handler + if ($mdType -eq "EventSubscription") { + $handler = $propsNode.SelectSingleNode("md:Handler", $ns) + if (-not $handler -or -not $handler.InnerText.Trim()) { + Report-Error "10. EventSubscription: empty Handler" + $check10Ok = $false + $check10Issues++ + } + + # Empty Source + $source = $propsNode.SelectSingleNode("md:Source", $ns) + $hasSource = $false + if ($source) { + $sourceTypes = $source.SelectNodes("v8:Type", $ns) + if ($sourceTypes.Count -gt 0) { $hasSource = $true } + } + if (-not $hasSource) { + Report-Warn "10. EventSubscription: no Source types specified" + $check10Issues++ + } + } + + # ScheduledJob: empty MethodName + if ($mdType -eq "ScheduledJob") { + $method = $propsNode.SelectSingleNode("md:MethodName", $ns) + if (-not $method -or -not $method.InnerText.Trim()) { + Report-Error "10. ScheduledJob: empty MethodName" + $check10Ok = $false + $check10Issues++ + } + } + + # AccountingRegister: ChartOfAccounts must not be empty + if ($mdType -eq "AccountingRegister") { + $coa = $propsNode.SelectSingleNode("md:ChartOfAccounts", $ns) + if (-not $coa -or -not $coa.InnerText.Trim()) { + Report-Error "10. AccountingRegister: empty ChartOfAccounts" + $check10Ok = $false + $check10Issues++ + Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" + } + } + + # CalculationRegister: ChartOfCalculationTypes must not be empty + if ($mdType -eq "CalculationRegister") { + $coct = $propsNode.SelectSingleNode("md:ChartOfCalculationTypes", $ns) + if (-not $coct -or -not $coct.InnerText.Trim()) { + Report-Error "10. CalculationRegister: empty ChartOfCalculationTypes" + $check10Ok = $false + $check10Issues++ + Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`"" + } + } + + # BusinessProcess: Task should not be empty + if ($mdType -eq "BusinessProcess") { + $taskProp = $propsNode.SelectSingleNode("md:Task", $ns) + if (-not $taskProp -or -not $taskProp.InnerText.Trim()) { + Report-Warn "10. BusinessProcess: empty Task reference" + $check10Issues++ + Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"Task=Task.XXX`"" + } + } + + # CalculationRegister: ActionPeriod=true requires non-empty Schedule + if ($mdType -eq "CalculationRegister") { + $actionPeriod = $propsNode.SelectSingleNode("md:ActionPeriod", $ns) + if ($actionPeriod -and $actionPeriod.InnerText -eq "true") { + $schedule = $propsNode.SelectSingleNode("md:Schedule", $ns) + if (-not $schedule -or -not $schedule.InnerText.Trim()) { + Report-Warn "10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register" + $check10Issues++ + } + } + } + + # DocumentJournal: RegisteredDocuments should not be empty + if ($mdType -eq "DocumentJournal") { + $regDocs = $propsNode.SelectSingleNode("md:RegisteredDocuments", $ns) + $hasRegDocs = $false + if ($regDocs) { + $items = $regDocs.SelectNodes("v8:Type", $ns) + if ($items.Count -gt 0) { $hasRegDocs = $true } + } + if (-not $hasRegDocs) { + Report-Warn "10. DocumentJournal: no RegisteredDocuments specified" + $check10Issues++ + } + } + + # ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0 + if ($mdType -eq "ChartOfAccounts") { + $maxExtDim = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) + if ($maxExtDim -and [int]$maxExtDim.InnerText -gt 0) { + $edt = $propsNode.SelectSingleNode("md:ExtDimensionTypes", $ns) + if (-not $edt -or -not $edt.InnerText.Trim()) { + Report-Warn "10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty" + $check10Issues++ + Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`"" + } + } + } + + # Register: must have at least one Dimension or Resource (platform rejects empty registers) + $regTypesAll = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") + if ($regTypesAll -contains $mdType -and $childObjNode) { + $dims = $childObjNode.SelectNodes("md:Dimension", $ns).Count + $ress = $childObjNode.SelectNodes("md:Resource", $ns).Count + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns).Count + if (($dims + $ress + $attrs) -eq 0) { + Report-Warn "10. $mdType`: no Dimensions, Resources, or Attributes — platform will reject" + $check10Issues++ + } + } + + # Document: RegisterRecords references should point to existing objects in config + if ($mdType -eq "Document" -and $script:configDir) { + $regRecords = $propsNode.SelectSingleNode("md:RegisterRecords", $ns) + if ($regRecords) { + $items = $regRecords.SelectNodes("xr:Item", $ns) + foreach ($item in $items) { + $refVal = $item.InnerText.Trim() + if (-not $refVal) { continue } + # Parse "AccumulationRegister.Name" → dir AccumulationRegisters/Name + $parts = $refVal -split '\.',2 + if ($parts.Count -eq 2) { + $refType = $parts[0]; $refName = $parts[1] + $dirMap = @{ + "AccumulationRegister"="AccumulationRegisters"; "InformationRegister"="InformationRegisters" + "AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters" + } + $refDir = $dirMap[$refType] + if ($refDir) { + $refPath = Join-Path $script:configDir "$refDir/$refName" + $refXml = Join-Path $script:configDir "$refDir/$refName.xml" + if (-not (Test-Path $refPath) -and -not (Test-Path $refXml)) { + Report-Warn "10. Document.RegisterRecords references '$refVal' but object not found in config" + $check10Issues++ + } + } + } + } + } + } + + # Register: must have at least one registrar document + $registerTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister") + if ($registerTypes -contains $mdType -and $script:configDir -and $objName -ne "(unknown)") { + $needsRegistrar = $true + # InformationRegister with WriteMode=Independent does not need a registrar + if ($mdType -eq "InformationRegister") { + $writeMode = $propsNode.SelectSingleNode("md:WriteMode", $ns) + if (-not $writeMode -or $writeMode.InnerText -ne "RecorderSubordinate") { + $needsRegistrar = $false + } + } + if ($needsRegistrar) { + $regRef = "$mdType.$objName" + $docsDir = Join-Path $script:configDir "Documents" + $hasRegistrar = $false + if (Test-Path $docsDir) { + $docXmls = Get-ChildItem $docsDir -Filter "*.xml" -File -ErrorAction SilentlyContinue + foreach ($docXml in $docXmls) { + $content = [System.IO.File]::ReadAllText($docXml.FullName, [System.Text.Encoding]::UTF8) + if ($content.Contains($regRef)) { + $hasRegistrar = $true + break + } + } + } + if (-not $hasRegistrar) { + Report-Warn "10. $mdType`: no registrar document found (none references '$regRef' in RegisterRecords)" + $check10Issues++ + } + } + } +} + +if ($check10Ok -and $check10Issues -eq 0) { + Report-OK "10. Cross-property consistency" +} elseif ($check10Ok) { + # Had warnings but no errors — already reported +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: HTTPService/WebService nested structure --- + +if ($mdType -eq "HTTPService" -and $childObjNode) { + $urlTemplates = $childObjNode.SelectNodes("md:URLTemplate", $ns) + $check11Ok = $true + $methodCount = 0 + + $validHTTPMethods = @("GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","MERGE","CONNECT") + + foreach ($ut in $urlTemplates) { + if ($script:stopped) { break } + + $utProps = $ut.SelectSingleNode("md:Properties", $ns) + $utNameNode = if ($utProps) { $utProps.SelectSingleNode("md:Name", $ns) } else { $null } + $utName = if ($utNameNode) { $utNameNode.InnerText } else { "(unnamed)" } + + # Template property + $tpl = if ($utProps) { $utProps.SelectSingleNode("md:Template", $ns) } else { $null } + if (-not $tpl -or -not $tpl.InnerText.Trim()) { + Report-Error "11. HTTPService URLTemplate '$utName': empty Template" + $check11Ok = $false + } + + # Methods inside URLTemplate + $utChildObj = $ut.SelectSingleNode("md:ChildObjects", $ns) + if ($utChildObj) { + $methods = $utChildObj.SelectNodes("md:Method", $ns) + foreach ($m in $methods) { + $methodCount++ + $mProps = $m.SelectSingleNode("md:Properties", $ns) + if ($mProps) { + $httpMethod = $mProps.SelectSingleNode("md:HTTPMethod", $ns) + if ($httpMethod -and $httpMethod.InnerText) { + if ($validHTTPMethods -notcontains $httpMethod.InnerText) { + Report-Error "11. HTTPService URLTemplate '$utName': invalid HTTPMethod '$($httpMethod.InnerText)'" + $check11Ok = $false + } + } else { + Report-Error "11. HTTPService URLTemplate '$utName': Method missing HTTPMethod" + $check11Ok = $false + } + } + } + } + } + + if ($check11Ok) { + Report-OK "11. HTTPService: $($urlTemplates.Count) URLTemplate(s), $methodCount method(s)" + } +} elseif ($mdType -eq "WebService" -and $childObjNode) { + $operations = $childObjNode.SelectNodes("md:Operation", $ns) + $check11Ok = $true + $paramCount = 0 + + $validDirections = @("In","Out","InOut") + + foreach ($op in $operations) { + if ($script:stopped) { break } + + $opProps = $op.SelectSingleNode("md:Properties", $ns) + $opNameNode = if ($opProps) { $opProps.SelectSingleNode("md:Name", $ns) } else { $null } + $opName = if ($opNameNode) { $opNameNode.InnerText } else { "(unnamed)" } + + # ReturnType — XDTOReturningValueType + $retType = if ($opProps) { $opProps.SelectSingleNode("md:XDTOReturningValueType", $ns) } else { $null } + if (-not $retType -or -not $retType.InnerText.Trim()) { + Report-Warn "11. WebService Operation '$opName': no XDTOReturningValueType" + } + + # Parameters inside Operation + $opChildObj = $op.SelectSingleNode("md:ChildObjects", $ns) + if ($opChildObj) { + $params = $opChildObj.SelectNodes("md:Parameter", $ns) + foreach ($p in $params) { + $paramCount++ + $pProps = $p.SelectSingleNode("md:Properties", $ns) + if ($pProps) { + $dir = $pProps.SelectSingleNode("md:TransferDirection", $ns) + if ($dir -and $dir.InnerText -and $validDirections -notcontains $dir.InnerText) { + Report-Error "11. WebService Operation '$opName': Parameter has invalid TransferDirection '$($dir.InnerText)'" + $check11Ok = $false + } + } + } + } + } + + if ($check11Ok) { + Report-OK "11. WebService: $($operations.Count) operation(s), $paramCount parameter(s)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Forbidden properties per type --- + +if ($propsNode -and $forbiddenProperties.ContainsKey($mdType)) { + $forbidden = $forbiddenProperties[$mdType] + $check12Ok = $true + foreach ($fp in $forbidden) { + $fpNode = $propsNode.SelectSingleNode("md:$fp", $ns) + if ($fpNode) { + Report-Error "12. Forbidden property '$fp' present in $mdType (will fail on LoadConfigFromFiles)" + $check12Ok = $false + } + } + if ($check12Ok) { + Report-OK "12. Forbidden properties: none found" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: Method reference validation (EventSubscription.Handler, ScheduledJob.MethodName) --- + +if ($propsNode -and $mdType -in @("EventSubscription","ScheduledJob") -and $script:configDir) { + $check13Ok = $true + $methodRef = $null + $propLabel = $null + + if ($mdType -eq "EventSubscription") { + $hNode = $propsNode.SelectSingleNode("md:Handler", $ns) + if ($hNode) { $methodRef = $hNode.InnerText.Trim() } + $propLabel = "Handler" + } elseif ($mdType -eq "ScheduledJob") { + $mNode = $propsNode.SelectSingleNode("md:MethodName", $ns) + if ($mNode) { $methodRef = $mNode.InnerText.Trim() } + $propLabel = "MethodName" + } + + if ($methodRef) { + $parts = $methodRef.Split('.') + # Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy) + if ($parts.Count -eq 3 -and $parts[0] -eq "CommonModule") { + $cmName = $parts[1] + $procName = $parts[2] + } elseif ($parts.Count -eq 2) { + $cmName = $parts[0] + $procName = $parts[1] + } else { + Report-Error "13. ${mdType}.${propLabel} = '$methodRef': expected format 'CommonModule.ModuleName.ProcedureName'" + $check13Ok = $false + $cmName = $null + $procName = $null + } + if ($cmName) { + $cmXml = Join-Path (Join-Path $script:configDir "CommonModules") "$cmName.xml" + if (-not (Test-Path $cmXml)) { + Report-Error "13. ${mdType}.${propLabel}: CommonModule '$cmName' not found (expected $cmXml)" + $check13Ok = $false + } else { + # Check BSL file for exported procedure + $bslPath = Join-Path (Join-Path (Join-Path $script:configDir "CommonModules") $cmName) "Ext/Module.bsl" + if (Test-Path $bslPath) { + $bslContent = [System.IO.File]::ReadAllText($bslPath, [System.Text.Encoding]::UTF8) + # Match: Procedure/Function ProcName(...) Export or Процедура/Функция ProcName(...) Экспорт + $exportPattern = "(?mi)^[\s]*(Procedure|Function|Процедура|Функция)\s+$([regex]::Escape($procName))\s*\(.*\)\s+(Export|Экспорт)" + if (-not [regex]::IsMatch($bslContent, $exportPattern)) { + Report-Warn "13. ${mdType}.${propLabel}: procedure '$procName' not found as exported in CommonModule '$cmName'" + $check13Ok = $false + } + } else { + Report-Warn "13. ${mdType}.${propLabel}: BSL file not found ($bslPath), cannot verify procedure" + } + } + } + } + + if ($check13Ok) { + Report-OK "13. Method reference: $propLabel = '$methodRef'" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 14: DocumentJournal Column content --- + +if ($mdType -eq "DocumentJournal" -and $childObjNode) { + $columns = $childObjNode.SelectNodes("md:Column", $ns) + $check14Ok = $true + $colCount = 0 + $emptyRefCount = 0 + + foreach ($col in $columns) { + $colCount++ + $colProps = $col.SelectSingleNode("md:Properties", $ns) + $colNameNode = if ($colProps) { $colProps.SelectSingleNode("md:Name", $ns) } else { $null } + $colName = if ($colNameNode) { $colNameNode.InnerText } else { "(unnamed)" } + + $refs = if ($colProps) { $colProps.SelectSingleNode("md:References", $ns) } else { $null } + $hasItems = $false + if ($refs) { + $items = $refs.SelectNodes("xr:Item", $ns) + if ($items.Count -gt 0) { $hasItems = $true } + } + if (-not $hasItems) { + Report-Error "14. DocumentJournal Column '$colName': empty References (will fail on LoadConfigFromFiles)" + $check14Ok = $false + $emptyRefCount++ + } + } + + if ($check14Ok -and $colCount -gt 0) { + Report-OK "14. DocumentJournal Columns: $colCount column(s), all have References" + } elseif ($colCount -eq 0) { + Report-OK "14. DocumentJournal Columns: none" + } +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index 66c6101a..e7de18f1 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -1,65 +1,65 @@ ---- -name: mxl-compile -description: Компиляция табличного документа (MXL) из JSON-определения. Используй когда нужно создать макет печатной формы -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /mxl-compile — Компилятор макета из DSL - -Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace). - -## Использование - -``` -/mxl-compile -``` - -## Параметры - -| Параметр | Обязательный | Описание | -|------------|:------------:|------------------------------------| -| JsonPath | да | Путь к JSON-определению макета | -| OutputPath | да | Путь для генерации Template.xml | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml" -``` - -## Рабочий процесс - -1. Claude пишет JSON-определение (Write tool) → файл `.json` -2. Claude вызывает `/mxl-compile` для генерации Template.xml -3. Claude вызывает `/mxl-validate` для проверки корректности -4. Claude вызывает `/mxl-info` для верификации структуры - -**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров. - -## JSON-схема DSL - -Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON). - -Краткая структура: - -``` -{ columns, page, defaultWidth, columnWidths, - fonts: { name: { face, size, bold, italic, underline, strikeout } }, - styles: { name: { font, align, valign, border, borderWidth, wrap, format } }, - areas: [{ name, rows: [{ height, rowStyle, cells: [ - { col, span, rowspan, style, param, detail, text, template } - ]}]}] -} -``` - -Ключевые правила: -- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` -- `col` — 1-based позиция колонки -- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) -- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template -- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) +--- +name: mxl-compile +description: Компиляция табличного документа (MXL) из JSON-определения. Используй когда нужно создать макет печатной формы +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /mxl-compile — Компилятор макета из DSL + +Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace). + +## Использование + +``` +/mxl-compile +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|------------------------------------| +| JsonPath | да | Путь к JSON-определению макета | +| OutputPath | да | Путь для генерации Template.xml | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml" +``` + +## Рабочий процесс + +1. Claude пишет JSON-определение (Write tool) → файл `.json` +2. Claude вызывает `/mxl-compile` для генерации Template.xml +3. Claude вызывает `/mxl-validate` для проверки корректности +4. Claude вызывает `/mxl-info` для верификации структуры + +**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров. + +## JSON-схема DSL + +Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON). + +Краткая структура: + +``` +{ columns, page, defaultWidth, columnWidths, + fonts: { name: { face, size, bold, italic, underline, strikeout } }, + styles: { name: { font, align, valign, border, borderWidth, wrap, format } }, + areas: [{ name, rows: [{ height, rowStyle, cells: [ + { col, span, rowspan, style, param, detail, text, template } + ]}]}] +} +``` + +Ключевые правила: +- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` +- `col` — 1-based позиция колонки +- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) +- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template +- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) diff --git a/.claude/skills/mxl-decompile/SKILL.md b/.claude/skills/mxl-decompile/SKILL.md index 75c088f0..946177e0 100644 --- a/.claude/skills/mxl-decompile/SKILL.md +++ b/.claude/skills/mxl-decompile/SKILL.md @@ -1,57 +1,57 @@ ---- -name: mxl-decompile -description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета -argument-hint: [OutputPath] -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /mxl-decompile — Декомпилятор макета в DSL - -Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`. - -## Использование - -``` -/mxl-decompile [OutputPath] -``` - -## Параметры - -| Параметр | Обязательный | Описание | -|--------------|:------------:|-----------------------------------------| -| TemplatePath | да | Путь к Template.xml | -| OutputPath | нет | Путь для JSON (если не указан — stdout) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"] -``` - -## Рабочий процесс - -Декомпиляция существующего макета для анализа или доработки: - -1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml -2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили) -3. Claude вызывает `/mxl-compile` для генерации нового Template.xml -4. Claude вызывает `/mxl-validate` для проверки - -## JSON-схема DSL - -Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool). - -## Генерация имён - -Скрипт автоматически генерирует осмысленные имена: - -- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам -- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств - -## Детектирование `rowStyle` - -Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода. +--- +name: mxl-decompile +description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета +argument-hint: [OutputPath] +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /mxl-decompile — Декомпилятор макета в DSL + +Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`. + +## Использование + +``` +/mxl-decompile [OutputPath] +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|--------------|:------------:|-----------------------------------------| +| TemplatePath | да | Путь к Template.xml | +| OutputPath | нет | Путь для JSON (если не указан — stdout) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"] +``` + +## Рабочий процесс + +Декомпиляция существующего макета для анализа или доработки: + +1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml +2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили) +3. Claude вызывает `/mxl-compile` для генерации нового Template.xml +4. Claude вызывает `/mxl-validate` для проверки + +## JSON-схема DSL + +Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool). + +## Генерация имён + +Скрипт автоматически генерирует осмысленные имена: + +- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам +- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств + +## Детектирование `rowStyle` + +Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода. diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 376af68e..f1c66696 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -1,646 +1,646 @@ -# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$TemplatePath, - - [string]$OutputPath -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- 1. Load and parse XML --- - -if (-not (Test-Path $TemplatePath)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load((Resolve-Path $TemplatePath).Path) - -$root = $xmlDoc.DocumentElement -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - -# --- 2. Extract font palette --- - -$rawFonts = @() -foreach ($fNode in $root.SelectNodes("d:font", $ns)) { - $rawFonts += @{ - Face = $fNode.GetAttribute("faceName") - Size = [int]$fNode.GetAttribute("height") - Bold = $fNode.GetAttribute("bold") -eq "true" - Italic = $fNode.GetAttribute("italic") -eq "true" - Underline = $fNode.GetAttribute("underline") -eq "true" - Strikeout = $fNode.GetAttribute("strikeout") -eq "true" - } -} - -# --- 3. Extract line palette --- - -$rawLines = @() -foreach ($lNode in $root.SelectNodes("d:line", $ns)) { - $rawLines += @{ Width = [int]$lNode.GetAttribute("width") } -} - -# --- 4. Extract format palette --- - -$rawFormats = @() -foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { - $fmt = @{ - FontIdx = -1 - LB = -1; TB = -1; RB = -1; BB = -1 - Width = 0; Height = 0 - HA = ""; VA = "" - Wrap = $false; FillType = ""; DataFormat = "" - } - - $n = $fmtNode.SelectSingleNode("d:font", $ns) - if ($n) { $fmt.FontIdx = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:leftBorder", $ns) - if ($n) { $fmt.LB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:topBorder", $ns) - if ($n) { $fmt.TB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:rightBorder", $ns) - if ($n) { $fmt.RB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns) - if ($n) { $fmt.BB = [int]$n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:width", $ns) - if ($n) { $fmt.Width = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:height", $ns) - if ($n) { $fmt.Height = [int]$n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns) - if ($n) { $fmt.HA = $n.InnerText } - $n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns) - if ($n) { $fmt.VA = $n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:textPlacement", $ns) - if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true } - - $n = $fmtNode.SelectSingleNode("d:fillType", $ns) - if ($n) { $fmt.FillType = $n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns) - if ($n) { $fmt.DataFormat = $n.InnerText } - - $rawFormats += $fmt -} - -function Get-Format { - param([int]$idx) - if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null } - return $rawFormats[$idx - 1] -} - -# --- 5. Extract columns and default width --- - -$colNode = $root.SelectSingleNode("d:columns", $ns) -$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText - -$colFormatIndices = @{} -foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) { - $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText - $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText - $colFormatIndices[$colIdx] = $fmtIdx -} - -$defaultFmtIdx = 0 -$n = $root.SelectSingleNode("d:defaultFormatIndex", $ns) -if ($n) { $defaultFmtIdx = [int]$n.InnerText } - -$defaultWidth = 10 -if ($defaultFmtIdx -gt 0) { - $defFmt = Get-Format $defaultFmtIdx - if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width } -} - -# Build column width map (1-based col → width), only non-default -$colWidthMap = [ordered]@{} -foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) { - $fmt = Get-Format $colFormatIndices[$col0] - if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { - $col1 = [string]($col0 + 1) - $colWidthMap.Add($col1, $fmt.Width) - } -} - -# --- 6. Extract merges --- - -$mergeMap = @{} -foreach ($mNode in $root.SelectNodes("d:merge", $ns)) { - $r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText - $c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText - $w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText - $hNode = $mNode.SelectSingleNode("d:h", $ns) - $h = if ($hNode) { [int]$hNode.InnerText } else { 0 } - $mergeMap["$r,$c"] = @{ W = $w; H = $h } -} - -# --- 7. Extract named items --- - -$namedAreas = @() -foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) { - $xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -ne "NamedItemCells") { continue } - - $areaNode = $niNode.SelectSingleNode("d:area", $ns) - $areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText - if ($areaType -ne "Rows") { continue } - - $namedAreas += @{ - Name = $niNode.SelectSingleNode("d:name", $ns).InnerText - BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText - EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText - } -} - -# --- 8. Extract rows --- - -$rowData = @{} -foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { - $rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText - $rowNode = $riNode.SelectSingleNode("d:row", $ns) - - $indexTo = $rowIdx - $itNode = $riNode.SelectSingleNode("d:indexTo", $ns) - if ($itNode) { $indexTo = [int]$itNode.InnerText } - - $rowFmtIdx = 0 - $fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns) - if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText } - - $isEmpty = $false - $emptyNode = $rowNode.SelectSingleNode("d:empty", $ns) - if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true } - - $cells = @() - if (-not $isEmpty) { - $col = -1 - foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) { - $iNode = $cGroup.SelectSingleNode("d:i", $ns) - if ($iNode) { $col = [int]$iNode.InnerText } - else { $col++ } - - $cContent = $cGroup.SelectSingleNode("d:c", $ns) - if (-not $cContent) { continue } - - $cellFmtIdx = 0 - $fNode = $cContent.SelectSingleNode("d:f", $ns) - if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText } - - $param = $null - $pNode = $cContent.SelectSingleNode("d:parameter", $ns) - if ($pNode) { $param = $pNode.InnerText } - - $detail = $null - $dNode = $cContent.SelectSingleNode("d:detailParameter", $ns) - if ($dNode) { $detail = $dNode.InnerText } - - $text = $null - $tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns) - if ($tNode) { $text = $tNode.InnerText } - - $cells += @{ - Col = $col - FormatIdx = $cellFmtIdx - Param = $param - Detail = $detail - Text = $text - } - } - } - - for ($r = $rowIdx; $r -le $indexTo; $r++) { - $rowData[$r] = @{ - FormatIdx = $rowFmtIdx - Cells = $cells - Empty = $isEmpty - } - } -} - -# --- 9. Build style key (ignoring fillType) --- - -function Get-BorderDesc { - param($fmt) - if (-not $fmt) { return @{ Border = "none"; Thick = $false } } - - $lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0 - $rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0 - - if (-not $lb -and -not $tb -and -not $rb -and -not $bb) { - return @{ Border = "none"; Thick = $false } - } - - $thick = $false - foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) { - if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) { - $thick = $true; break - } - } - - if ($lb -and $tb -and $rb -and $bb) { - return @{ Border = "all"; Thick = $thick } - } - - $sides = @() - if ($tb) { $sides += "top" } - if ($bb) { $sides += "bottom" } - if ($lb) { $sides += "left" } - if ($rb) { $sides += "right" } - - return @{ Border = ($sides -join ","); Thick = $thick } -} - -function Get-StyleKey { - param($fmt) - if (-not $fmt) { return "empty" } - $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } - $bd = Get-BorderDesc $fmt - return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)" -} - -# --- 10. Name fonts --- - -$fontNames = @{} -$fontDefs = [ordered]@{} - -if ($rawFonts.Count -gt 0) { - $fontNames[0] = "default" - $fontDefs["default"] = $rawFonts[0] -} - -function Get-FontKey { - param($f) - return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)" -} - -$fontKeyMap = @{} -$fontKeyMap[(Get-FontKey $rawFonts[0])] = "default" - -for ($i = 1; $i -lt $rawFonts.Count; $i++) { - $f = $rawFonts[$i] - $df = $rawFonts[0] - - # Dedup: if identical font already named, reuse - $fKey = Get-FontKey $f - if ($fontKeyMap.ContainsKey($fKey)) { - $fontNames[$i] = $fontKeyMap[$fKey] - continue - } - - $name = $null - - if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) { - if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) { - $name = "bold" - } elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) { - $name = "italic" - } elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) { - $name = "underline" - } - } elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) { - $name = "header" - } elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) { - $name = "small" - } - - if (-not $name) { - $parts = @() - if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() } - $parts += "$($f.Size)" - if ($f.Bold) { $parts += "bold" } - if ($f.Italic) { $parts += "italic" } - if ($f.Underline) { $parts += "underline" } - if ($f.Strikeout) { $parts += "strikeout" } - $name = $parts -join "-" - } - - $baseName = $name; $suffix = 2 - while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } - - $fontNames[$i] = $name - $fontDefs[$name] = $f - $fontKeyMap[$fKey] = $name -} - -# --- 11. Collect and name styles --- - -$styleKeys = [ordered]@{} -$formatToStyleKey = @{} - -foreach ($r in $rowData.Values) { - foreach ($cell in $r.Cells) { - $fmt = Get-Format $cell.FormatIdx - if (-not $fmt) { continue } - $key = Get-StyleKey $fmt - if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } - $formatToStyleKey[$cell.FormatIdx] = $key - } -} - -function Name-Style { - param($fmt) - if (-not $fmt) { return "default" } - $parts = @() - - $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } - if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { - $parts += $fontNames[$fi] - } - - $bd = Get-BorderDesc $fmt - if ($bd.Border -ne "none") { - if ($bd.Border -eq "all") { $parts += "bordered" } - else { $parts += "border-$($bd.Border)" } - } - - if ($fmt.HA -eq "Center") { $parts += "center" } - elseif ($fmt.HA -eq "Right") { $parts += "right" } - if ($fmt.VA -eq "Center") { $parts += "vcenter" } - elseif ($fmt.VA -eq "Top") { $parts += "vtop" } - if ($fmt.Wrap) { $parts += "wrap" } - if ($fmt.DataFormat) { $parts += "fmt" } - - if ($parts.Count -eq 0) { return "default" } - return ($parts -join "-") -} - -$styleNames = [ordered]@{} -$styleDefs = [ordered]@{} - -foreach ($key in $styleKeys.Keys) { - $fmt = $styleKeys[$key] - $name = Name-Style $fmt - - $baseName = $name; $suffix = 2 - while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } - - $styleNames[$key] = $name - - $sDef = [ordered]@{} - $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } - if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { - $sDef["font"] = $fontNames[$fi] - } - if ($fmt.HA) { - $a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } } - if ($a) { $sDef["align"] = $a } - } - if ($fmt.VA) { - $a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } } - if ($a) { $sDef["valign"] = $a } - } - $bd = Get-BorderDesc $fmt - if ($bd.Border -ne "none") { - $sDef["border"] = $bd.Border - if ($bd.Thick) { $sDef["borderWidth"] = "thick" } - } - if ($fmt.Wrap) { $sDef["wrap"] = $true } - if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat } - - $styleDefs[$name] = $sDef -} - -function Get-StyleName { - param([int]$fmtIdx) - $key = $formatToStyleKey[$fmtIdx] - if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] } - return "default" -} - -# --- 12. Build areas --- - -$dslAreas = @() - -foreach ($area in $namedAreas) { - $areaRows = @() - - for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) { - $rd = $rowData[$globalRow] - - if (-not $rd -or $rd.Empty) { - $areaRows += [ordered]@{} - continue - } - - $dslRow = [ordered]@{} - - # Row height - if ($rd.FormatIdx -gt 0) { - $rowFmt = Get-Format $rd.FormatIdx - if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } - } - - # Separate content cells from gap-fill cells - $contentCells = @() - $gapCells = @() - - foreach ($cell in $rd.Cells) { - $hasContent = $cell.Param -or $cell.Text - $hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)") - - if ($hasContent -or $hasMerge) { - $contentCells += $cell - } else { - $gapCells += $cell - } - } - - # Detect rowStyle - $rowStyleName = $null - $rowStyleKey = $null - - if ($gapCells.Count -gt 0) { - $gapKeys = @{} - foreach ($gc in $gapCells) { - $fmt = Get-Format $gc.FormatIdx - $gapKeys[(Get-StyleKey $fmt)] = $true - } - - if ($gapKeys.Count -eq 1) { - $rowStyleKey = @($gapKeys.Keys)[0] - if ($styleNames.Contains($rowStyleKey)) { - $rowStyleName = $styleNames[$rowStyleKey] - } - } - } - - if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName } - - # Build cell list - $dslCells = @() - - foreach ($cell in ($contentCells | Sort-Object { $_.Col })) { - $dslCell = [ordered]@{ col = $cell.Col + 1 } - - # Span/rowspan from merge - $mk = "$globalRow,$($cell.Col)" - if ($mergeMap.ContainsKey($mk)) { - $m = $mergeMap[$mk] - if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 } - if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 } - } - - # Style - $cellFmt = Get-Format $cell.FormatIdx - $cellStyleKey = Get-StyleKey $cellFmt - - if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) { - # Inherits rowStyle - } else { - $sn = Get-StyleName $cell.FormatIdx - if ($sn -ne "default" -or -not $rowStyleName) { - $dslCell["style"] = $sn - } - } - - # Content - $fillType = if ($cellFmt) { $cellFmt.FillType } else { "" } - - if ($cell.Param) { - $dslCell["param"] = $cell.Param - if ($cell.Detail) { $dslCell["detail"] = $cell.Detail } - } elseif ($fillType -eq "Template" -and $cell.Text) { - $dslCell["template"] = $cell.Text - } elseif ($cell.Text) { - $dslCell["text"] = $cell.Text - } - - $dslCells += $dslCell - } - - if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells } - $areaRows += $dslRow - } - - # Compress consecutive empty rows ({}) into { empty = N } - $compressedRows = @() - $emptyRun = 0 - foreach ($r in $areaRows) { - if ($r.Count -eq 0) { - $emptyRun++ - } else { - if ($emptyRun -gt 0) { - if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } - else { $compressedRows += [ordered]@{ empty = $emptyRun } } - $emptyRun = 0 - } - $compressedRows += $r - } - } - if ($emptyRun -gt 0) { - if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } - else { $compressedRows += [ordered]@{ empty = $emptyRun } } - } - - $dslAreas += [ordered]@{ - name = $area.Name - rows = [array]$compressedRows - } -} - -# --- 13. Compress columnWidths --- - -$compressedWidths = [ordered]@{} -if ($colWidthMap.Count -gt 0) { - $grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] } - foreach ($g in $grouped) { - $width = [int]$g.Name - $cols = @($g.Group | Sort-Object { [int]$_ }) - - $ranges = @() - $rangeStart = $cols[0]; $rangePrev = $cols[0] - - for ($i = 1; $i -lt $cols.Count; $i++) { - if ([int]$cols[$i] -eq [int]$rangePrev + 1) { - $rangePrev = $cols[$i] - } else { - if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } - else { $ranges += "$rangeStart-$rangePrev" } - $rangeStart = $cols[$i]; $rangePrev = $cols[$i] - } - } - if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } - else { $ranges += "$rangeStart-$rangePrev" } - - foreach ($range in $ranges) { $compressedWidths[$range] = $width } - } -} - -# --- 14. Build fonts output --- - -$fontsOut = [ordered]@{} -foreach ($name in $fontDefs.Keys) { - $f = $fontDefs[$name] - $fOut = [ordered]@{ face = $f.Face; size = $f.Size } - if ($f.Bold) { $fOut["bold"] = $true } - if ($f.Italic) { $fOut["italic"] = $true } - if ($f.Underline) { $fOut["underline"] = $true } - if ($f.Strikeout) { $fOut["strikeout"] = $true } - $fontsOut[$name] = $fOut -} - -# --- 15. Assemble result --- - -$result = [ordered]@{ - columns = $totalColumns - defaultWidth = $defaultWidth -} -if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths } -# Remove empty "default" style -if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) { - $styleDefs.Remove("default") -} - -# Remove unused styles -$usedStyles = @{} -foreach ($a in $dslAreas) { - foreach ($r in $a.rows) { - if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true } - if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } } - } -} -$toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) -foreach ($s in $toRemove) { $styleDefs.Remove($s) -} - -$result["fonts"] = $fontsOut -$result["styles"] = $styleDefs -$result["areas"] = [array]$dslAreas - -# --- 16. Convert to JSON and fix Unicode --- - -$json = $result | ConvertTo-Json -Depth 10 - -# PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8 -$json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', { - param($m) - [char][int]("0x" + $m.Groups[1].Value) -}) - -# --- 17. Output --- - -if ($OutputPath) { - $enc = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::WriteAllText( - (Join-Path (Get-Location) $OutputPath), - $json, - $enc - ) - Write-Host "[OK] Decompiled: $OutputPath" -} else { - Write-Output $json -} - -Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray -Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray +# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$TemplatePath, + + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Load and parse XML --- + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load((Resolve-Path $TemplatePath).Path) + +$root = $xmlDoc.DocumentElement +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + +# --- 2. Extract font palette --- + +$rawFonts = @() +foreach ($fNode in $root.SelectNodes("d:font", $ns)) { + $rawFonts += @{ + Face = $fNode.GetAttribute("faceName") + Size = [int]$fNode.GetAttribute("height") + Bold = $fNode.GetAttribute("bold") -eq "true" + Italic = $fNode.GetAttribute("italic") -eq "true" + Underline = $fNode.GetAttribute("underline") -eq "true" + Strikeout = $fNode.GetAttribute("strikeout") -eq "true" + } +} + +# --- 3. Extract line palette --- + +$rawLines = @() +foreach ($lNode in $root.SelectNodes("d:line", $ns)) { + $rawLines += @{ Width = [int]$lNode.GetAttribute("width") } +} + +# --- 4. Extract format palette --- + +$rawFormats = @() +foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { + $fmt = @{ + FontIdx = -1 + LB = -1; TB = -1; RB = -1; BB = -1 + Width = 0; Height = 0 + HA = ""; VA = "" + Wrap = $false; FillType = ""; DataFormat = "" + } + + $n = $fmtNode.SelectSingleNode("d:font", $ns) + if ($n) { $fmt.FontIdx = [int]$n.InnerText } + $n = $fmtNode.SelectSingleNode("d:leftBorder", $ns) + if ($n) { $fmt.LB = [int]$n.InnerText } + $n = $fmtNode.SelectSingleNode("d:topBorder", $ns) + if ($n) { $fmt.TB = [int]$n.InnerText } + $n = $fmtNode.SelectSingleNode("d:rightBorder", $ns) + if ($n) { $fmt.RB = [int]$n.InnerText } + $n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns) + if ($n) { $fmt.BB = [int]$n.InnerText } + + $n = $fmtNode.SelectSingleNode("d:width", $ns) + if ($n) { $fmt.Width = [int]$n.InnerText } + $n = $fmtNode.SelectSingleNode("d:height", $ns) + if ($n) { $fmt.Height = [int]$n.InnerText } + + $n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns) + if ($n) { $fmt.HA = $n.InnerText } + $n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns) + if ($n) { $fmt.VA = $n.InnerText } + + $n = $fmtNode.SelectSingleNode("d:textPlacement", $ns) + if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true } + + $n = $fmtNode.SelectSingleNode("d:fillType", $ns) + if ($n) { $fmt.FillType = $n.InnerText } + + $n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns) + if ($n) { $fmt.DataFormat = $n.InnerText } + + $rawFormats += $fmt +} + +function Get-Format { + param([int]$idx) + if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null } + return $rawFormats[$idx - 1] +} + +# --- 5. Extract columns and default width --- + +$colNode = $root.SelectSingleNode("d:columns", $ns) +$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText + +$colFormatIndices = @{} +foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) { + $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText + $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText + $colFormatIndices[$colIdx] = $fmtIdx +} + +$defaultFmtIdx = 0 +$n = $root.SelectSingleNode("d:defaultFormatIndex", $ns) +if ($n) { $defaultFmtIdx = [int]$n.InnerText } + +$defaultWidth = 10 +if ($defaultFmtIdx -gt 0) { + $defFmt = Get-Format $defaultFmtIdx + if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width } +} + +# Build column width map (1-based col → width), only non-default +$colWidthMap = [ordered]@{} +foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) { + $fmt = Get-Format $colFormatIndices[$col0] + if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { + $col1 = [string]($col0 + 1) + $colWidthMap.Add($col1, $fmt.Width) + } +} + +# --- 6. Extract merges --- + +$mergeMap = @{} +foreach ($mNode in $root.SelectNodes("d:merge", $ns)) { + $r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText + $c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText + $w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText + $hNode = $mNode.SelectSingleNode("d:h", $ns) + $h = if ($hNode) { [int]$hNode.InnerText } else { 0 } + $mergeMap["$r,$c"] = @{ W = $w; H = $h } +} + +# --- 7. Extract named items --- + +$namedAreas = @() +foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) { + $xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -ne "NamedItemCells") { continue } + + $areaNode = $niNode.SelectSingleNode("d:area", $ns) + $areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText + if ($areaType -ne "Rows") { continue } + + $namedAreas += @{ + Name = $niNode.SelectSingleNode("d:name", $ns).InnerText + BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText + EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText + } +} + +# --- 8. Extract rows --- + +$rowData = @{} +foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { + $rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText + $rowNode = $riNode.SelectSingleNode("d:row", $ns) + + $indexTo = $rowIdx + $itNode = $riNode.SelectSingleNode("d:indexTo", $ns) + if ($itNode) { $indexTo = [int]$itNode.InnerText } + + $rowFmtIdx = 0 + $fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns) + if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText } + + $isEmpty = $false + $emptyNode = $rowNode.SelectSingleNode("d:empty", $ns) + if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true } + + $cells = @() + if (-not $isEmpty) { + $col = -1 + foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) { + $iNode = $cGroup.SelectSingleNode("d:i", $ns) + if ($iNode) { $col = [int]$iNode.InnerText } + else { $col++ } + + $cContent = $cGroup.SelectSingleNode("d:c", $ns) + if (-not $cContent) { continue } + + $cellFmtIdx = 0 + $fNode = $cContent.SelectSingleNode("d:f", $ns) + if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText } + + $param = $null + $pNode = $cContent.SelectSingleNode("d:parameter", $ns) + if ($pNode) { $param = $pNode.InnerText } + + $detail = $null + $dNode = $cContent.SelectSingleNode("d:detailParameter", $ns) + if ($dNode) { $detail = $dNode.InnerText } + + $text = $null + $tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns) + if ($tNode) { $text = $tNode.InnerText } + + $cells += @{ + Col = $col + FormatIdx = $cellFmtIdx + Param = $param + Detail = $detail + Text = $text + } + } + } + + for ($r = $rowIdx; $r -le $indexTo; $r++) { + $rowData[$r] = @{ + FormatIdx = $rowFmtIdx + Cells = $cells + Empty = $isEmpty + } + } +} + +# --- 9. Build style key (ignoring fillType) --- + +function Get-BorderDesc { + param($fmt) + if (-not $fmt) { return @{ Border = "none"; Thick = $false } } + + $lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0 + $rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0 + + if (-not $lb -and -not $tb -and -not $rb -and -not $bb) { + return @{ Border = "none"; Thick = $false } + } + + $thick = $false + foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) { + if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) { + $thick = $true; break + } + } + + if ($lb -and $tb -and $rb -and $bb) { + return @{ Border = "all"; Thick = $thick } + } + + $sides = @() + if ($tb) { $sides += "top" } + if ($bb) { $sides += "bottom" } + if ($lb) { $sides += "left" } + if ($rb) { $sides += "right" } + + return @{ Border = ($sides -join ","); Thick = $thick } +} + +function Get-StyleKey { + param($fmt) + if (-not $fmt) { return "empty" } + $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } + $bd = Get-BorderDesc $fmt + return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)" +} + +# --- 10. Name fonts --- + +$fontNames = @{} +$fontDefs = [ordered]@{} + +if ($rawFonts.Count -gt 0) { + $fontNames[0] = "default" + $fontDefs["default"] = $rawFonts[0] +} + +function Get-FontKey { + param($f) + return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)" +} + +$fontKeyMap = @{} +$fontKeyMap[(Get-FontKey $rawFonts[0])] = "default" + +for ($i = 1; $i -lt $rawFonts.Count; $i++) { + $f = $rawFonts[$i] + $df = $rawFonts[0] + + # Dedup: if identical font already named, reuse + $fKey = Get-FontKey $f + if ($fontKeyMap.ContainsKey($fKey)) { + $fontNames[$i] = $fontKeyMap[$fKey] + continue + } + + $name = $null + + if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) { + if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) { + $name = "bold" + } elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) { + $name = "italic" + } elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) { + $name = "underline" + } + } elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) { + $name = "header" + } elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) { + $name = "small" + } + + if (-not $name) { + $parts = @() + if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() } + $parts += "$($f.Size)" + if ($f.Bold) { $parts += "bold" } + if ($f.Italic) { $parts += "italic" } + if ($f.Underline) { $parts += "underline" } + if ($f.Strikeout) { $parts += "strikeout" } + $name = $parts -join "-" + } + + $baseName = $name; $suffix = 2 + while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } + + $fontNames[$i] = $name + $fontDefs[$name] = $f + $fontKeyMap[$fKey] = $name +} + +# --- 11. Collect and name styles --- + +$styleKeys = [ordered]@{} +$formatToStyleKey = @{} + +foreach ($r in $rowData.Values) { + foreach ($cell in $r.Cells) { + $fmt = Get-Format $cell.FormatIdx + if (-not $fmt) { continue } + $key = Get-StyleKey $fmt + if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } + $formatToStyleKey[$cell.FormatIdx] = $key + } +} + +function Name-Style { + param($fmt) + if (-not $fmt) { return "default" } + $parts = @() + + $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } + if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { + $parts += $fontNames[$fi] + } + + $bd = Get-BorderDesc $fmt + if ($bd.Border -ne "none") { + if ($bd.Border -eq "all") { $parts += "bordered" } + else { $parts += "border-$($bd.Border)" } + } + + if ($fmt.HA -eq "Center") { $parts += "center" } + elseif ($fmt.HA -eq "Right") { $parts += "right" } + if ($fmt.VA -eq "Center") { $parts += "vcenter" } + elseif ($fmt.VA -eq "Top") { $parts += "vtop" } + if ($fmt.Wrap) { $parts += "wrap" } + if ($fmt.DataFormat) { $parts += "fmt" } + + if ($parts.Count -eq 0) { return "default" } + return ($parts -join "-") +} + +$styleNames = [ordered]@{} +$styleDefs = [ordered]@{} + +foreach ($key in $styleKeys.Keys) { + $fmt = $styleKeys[$key] + $name = Name-Style $fmt + + $baseName = $name; $suffix = 2 + while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } + + $styleNames[$key] = $name + + $sDef = [ordered]@{} + $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } + if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { + $sDef["font"] = $fontNames[$fi] + } + if ($fmt.HA) { + $a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } } + if ($a) { $sDef["align"] = $a } + } + if ($fmt.VA) { + $a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } } + if ($a) { $sDef["valign"] = $a } + } + $bd = Get-BorderDesc $fmt + if ($bd.Border -ne "none") { + $sDef["border"] = $bd.Border + if ($bd.Thick) { $sDef["borderWidth"] = "thick" } + } + if ($fmt.Wrap) { $sDef["wrap"] = $true } + if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat } + + $styleDefs[$name] = $sDef +} + +function Get-StyleName { + param([int]$fmtIdx) + $key = $formatToStyleKey[$fmtIdx] + if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] } + return "default" +} + +# --- 12. Build areas --- + +$dslAreas = @() + +foreach ($area in $namedAreas) { + $areaRows = @() + + for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) { + $rd = $rowData[$globalRow] + + if (-not $rd -or $rd.Empty) { + $areaRows += [ordered]@{} + continue + } + + $dslRow = [ordered]@{} + + # Row height + if ($rd.FormatIdx -gt 0) { + $rowFmt = Get-Format $rd.FormatIdx + if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } + } + + # Separate content cells from gap-fill cells + $contentCells = @() + $gapCells = @() + + foreach ($cell in $rd.Cells) { + $hasContent = $cell.Param -or $cell.Text + $hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)") + + if ($hasContent -or $hasMerge) { + $contentCells += $cell + } else { + $gapCells += $cell + } + } + + # Detect rowStyle + $rowStyleName = $null + $rowStyleKey = $null + + if ($gapCells.Count -gt 0) { + $gapKeys = @{} + foreach ($gc in $gapCells) { + $fmt = Get-Format $gc.FormatIdx + $gapKeys[(Get-StyleKey $fmt)] = $true + } + + if ($gapKeys.Count -eq 1) { + $rowStyleKey = @($gapKeys.Keys)[0] + if ($styleNames.Contains($rowStyleKey)) { + $rowStyleName = $styleNames[$rowStyleKey] + } + } + } + + if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName } + + # Build cell list + $dslCells = @() + + foreach ($cell in ($contentCells | Sort-Object { $_.Col })) { + $dslCell = [ordered]@{ col = $cell.Col + 1 } + + # Span/rowspan from merge + $mk = "$globalRow,$($cell.Col)" + if ($mergeMap.ContainsKey($mk)) { + $m = $mergeMap[$mk] + if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 } + if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 } + } + + # Style + $cellFmt = Get-Format $cell.FormatIdx + $cellStyleKey = Get-StyleKey $cellFmt + + if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) { + # Inherits rowStyle + } else { + $sn = Get-StyleName $cell.FormatIdx + if ($sn -ne "default" -or -not $rowStyleName) { + $dslCell["style"] = $sn + } + } + + # Content + $fillType = if ($cellFmt) { $cellFmt.FillType } else { "" } + + if ($cell.Param) { + $dslCell["param"] = $cell.Param + if ($cell.Detail) { $dslCell["detail"] = $cell.Detail } + } elseif ($fillType -eq "Template" -and $cell.Text) { + $dslCell["template"] = $cell.Text + } elseif ($cell.Text) { + $dslCell["text"] = $cell.Text + } + + $dslCells += $dslCell + } + + if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells } + $areaRows += $dslRow + } + + # Compress consecutive empty rows ({}) into { empty = N } + $compressedRows = @() + $emptyRun = 0 + foreach ($r in $areaRows) { + if ($r.Count -eq 0) { + $emptyRun++ + } else { + if ($emptyRun -gt 0) { + if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } + else { $compressedRows += [ordered]@{ empty = $emptyRun } } + $emptyRun = 0 + } + $compressedRows += $r + } + } + if ($emptyRun -gt 0) { + if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } + else { $compressedRows += [ordered]@{ empty = $emptyRun } } + } + + $dslAreas += [ordered]@{ + name = $area.Name + rows = [array]$compressedRows + } +} + +# --- 13. Compress columnWidths --- + +$compressedWidths = [ordered]@{} +if ($colWidthMap.Count -gt 0) { + $grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] } + foreach ($g in $grouped) { + $width = [int]$g.Name + $cols = @($g.Group | Sort-Object { [int]$_ }) + + $ranges = @() + $rangeStart = $cols[0]; $rangePrev = $cols[0] + + for ($i = 1; $i -lt $cols.Count; $i++) { + if ([int]$cols[$i] -eq [int]$rangePrev + 1) { + $rangePrev = $cols[$i] + } else { + if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } + else { $ranges += "$rangeStart-$rangePrev" } + $rangeStart = $cols[$i]; $rangePrev = $cols[$i] + } + } + if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } + else { $ranges += "$rangeStart-$rangePrev" } + + foreach ($range in $ranges) { $compressedWidths[$range] = $width } + } +} + +# --- 14. Build fonts output --- + +$fontsOut = [ordered]@{} +foreach ($name in $fontDefs.Keys) { + $f = $fontDefs[$name] + $fOut = [ordered]@{ face = $f.Face; size = $f.Size } + if ($f.Bold) { $fOut["bold"] = $true } + if ($f.Italic) { $fOut["italic"] = $true } + if ($f.Underline) { $fOut["underline"] = $true } + if ($f.Strikeout) { $fOut["strikeout"] = $true } + $fontsOut[$name] = $fOut +} + +# --- 15. Assemble result --- + +$result = [ordered]@{ + columns = $totalColumns + defaultWidth = $defaultWidth +} +if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths } +# Remove empty "default" style +if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) { + $styleDefs.Remove("default") +} + +# Remove unused styles +$usedStyles = @{} +foreach ($a in $dslAreas) { + foreach ($r in $a.rows) { + if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true } + if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } } + } +} +$toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) +foreach ($s in $toRemove) { $styleDefs.Remove($s) +} + +$result["fonts"] = $fontsOut +$result["styles"] = $styleDefs +$result["areas"] = [array]$dslAreas + +# --- 16. Convert to JSON and fix Unicode --- + +$json = $result | ConvertTo-Json -Depth 10 + +# PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8 +$json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', { + param($m) + [char][int]("0x" + $m.Groups[1].Value) +}) + +# --- 17. Output --- + +if ($OutputPath) { + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText( + (Join-Path (Get-Location) $OutputPath), + $json, + $enc + ) + Write-Host "[OK] Decompiled: $OutputPath" +} else { + Write-Output $json +} + +Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray +Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray diff --git a/.claude/skills/mxl-info/SKILL.md b/.claude/skills/mxl-info/SKILL.md index 4a1969b5..4481857d 100644 --- a/.claude/skills/mxl-info/SKILL.md +++ b/.claude/skills/mxl-info/SKILL.md @@ -1,132 +1,132 @@ ---- -name: mxl-info -description: Анализ структуры макета табличного документа (MXL) — области, параметры, наборы колонок. Используй при разработке печати — получить области и заполняемые параметры макета -argument-hint: или -allowed-tools: - - Bash - - Read - - Glob ---- - -# /mxl-info — Анализ структуры макета - -Читает Template.xml табличного документа и выводит компактную сводку: именованные области, параметры, наборы колонок. Заменяет необходимость читать тысячи строк XML. - -## Использование - -``` -/mxl-info -/mxl-info -``` - -## Параметры - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|--------------|------------------------------------------| -| TemplatePath | нет | — | Прямой путь к Template.xml | -| ProcessorName | нет | — | Имя обработки (альтернатива пути) | -| TemplateName | нет | — | Имя макета (альтернатива пути) | -| SrcDir | нет | `src` | Каталог исходников | -| Format | нет | `text` | Формат вывода: `text` или `json` | -| WithText | нет | false | Включить статический текст и шаблоны | -| MaxParams | нет | 10 | Макс. параметров в списке на область | -| Limit | нет | 150 | Макс. строк вывода (защита от переполнения) | -| Offset | нет | 0 | Пропустить N строк (для пагинации) | - -Укажите либо `-TemplatePath`, либо оба `-ProcessorName` и `-TemplateName`. - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -TemplatePath "<путь>" -``` - -Или по имени обработки/макета: -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"] -``` - -Дополнительные флаги: -```powershell -... -WithText # включить текстовое содержимое ячеек -... -Format json # JSON-вывод для программной обработки -... -MaxParams 20 # показать больше параметров на область -... -Offset 150 # пагинация: пропустить первые 150 строк -``` - -## Чтение вывода - -### Области — сортировка сверху вниз - -Области перечислены в порядке документа (по позиции строки), а не по алфавиту. Это соответствует порядку вывода областей в коде заполнения — сверху вниз. - -``` ---- Named areas --- - Заголовок Rows rows 1-4 (1 params) - Поставщик Rows rows 5-6 (1 params) - Строка Rows rows 14-14 (8 params) - Итого Rows rows 16-17 (1 params) -``` - -Типы областей: -- **Rows** — горизонтальная область (диапазон строк). Получение: `Макет.ПолучитьОбласть("Имя")` -- **Columns** — вертикальная область (диапазон колонок). Получение: `Макет.ПолучитьОбласть("Имя")` -- **Rectangle** — фиксированная область (строки + колонки). Обычно использует отдельный набор колонок. -- **Drawing** — именованный рисунок/штрихкод. - -### Пересечения - -Когда есть области и Rows, и Columns (этикетки, ценники), скрипт выводит пары пересечений: - -``` ---- Intersections (use with GetArea) --- - ВысотаЭтикетки|ШиринаЭтикетки -``` - -В BSL: `Макет.ПолучитьОбласть("ВысотаЭтикетки|ШиринаЭтикетки")` - -### Параметры и detailParameter - -Параметры перечислены по областям. Если у параметра есть `detailParameter` (расшифровка), он показан ниже: - -``` ---- Parameters by area --- - Поставщик: ПредставлениеПоставщика - detail: ПредставлениеПоставщика->Поставщик - Строка: НомерСтроки, Товар, Количество, Цена, Сумма, ... (+3) - detail: Товар->Номенклатура -``` - -Это означает: параметр `Товар` отображает значение, а при клике открывает `Номенклатура` (объект расшифровки). - -### Параметры из шаблонов (суффикс `[tpl]`) - -Некоторые параметры встроены в шаблонный текст: `"Инв № [ИнвентарныйНомер]"`. Они заполняются через fillType=Template, а не fillType=Parameter. Скрипт всегда извлекает их и помечает суффиксом `[tpl]`: - -``` - НумерацияЛистов: Номер [tpl], Дата [tpl], НомерЛиста [tpl] -``` - -В BSL шаблонные параметры заполняются так же, как обычные: -```bsl -Область.Параметры.Номер = НомерДокумента; -Область.Параметры.Дата = ДатаДокумента; -``` - -Числовые подстановки вроде `[5]`, `[6]` (ссылки на сноски в официальных формах) игнорируются. - -### Текстовое содержимое (`-WithText`) - -Показывает статический текст (надписи, заголовки) и шаблонные строки с подстановками `[Параметр]`: - -``` ---- Text content --- - ШапкаТаблицы: - Text: "№", "Товар", "Ед. изм.", "Кол-во", "Цена", "Сумма" - Строка: - Templates: "Инв № [ИнвентарныйНомер]" -``` - -- **Text** — статические надписи (fillType=Text). Полезно для понимания назначения колонок. -- **Templates** — текст с подстановками `[ИмяПараметра]` (fillType=Template). Параметр внутри `[]` заполняется программно. - +--- +name: mxl-info +description: Анализ структуры макета табличного документа (MXL) — области, параметры, наборы колонок. Используй при разработке печати — получить области и заполняемые параметры макета +argument-hint: или +allowed-tools: + - Bash + - Read + - Glob +--- + +# /mxl-info — Анализ структуры макета + +Читает Template.xml табличного документа и выводит компактную сводку: именованные области, параметры, наборы колонок. Заменяет необходимость читать тысячи строк XML. + +## Использование + +``` +/mxl-info +/mxl-info +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|--------------|------------------------------------------| +| TemplatePath | нет | — | Прямой путь к Template.xml | +| ProcessorName | нет | — | Имя обработки (альтернатива пути) | +| TemplateName | нет | — | Имя макета (альтернатива пути) | +| SrcDir | нет | `src` | Каталог исходников | +| Format | нет | `text` | Формат вывода: `text` или `json` | +| WithText | нет | false | Включить статический текст и шаблоны | +| MaxParams | нет | 10 | Макс. параметров в списке на область | +| Limit | нет | 150 | Макс. строк вывода (защита от переполнения) | +| Offset | нет | 0 | Пропустить N строк (для пагинации) | + +Укажите либо `-TemplatePath`, либо оба `-ProcessorName` и `-TemplateName`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -TemplatePath "<путь>" +``` + +Или по имени обработки/макета: +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"] +``` + +Дополнительные флаги: +```powershell +... -WithText # включить текстовое содержимое ячеек +... -Format json # JSON-вывод для программной обработки +... -MaxParams 20 # показать больше параметров на область +... -Offset 150 # пагинация: пропустить первые 150 строк +``` + +## Чтение вывода + +### Области — сортировка сверху вниз + +Области перечислены в порядке документа (по позиции строки), а не по алфавиту. Это соответствует порядку вывода областей в коде заполнения — сверху вниз. + +``` +--- Named areas --- + Заголовок Rows rows 1-4 (1 params) + Поставщик Rows rows 5-6 (1 params) + Строка Rows rows 14-14 (8 params) + Итого Rows rows 16-17 (1 params) +``` + +Типы областей: +- **Rows** — горизонтальная область (диапазон строк). Получение: `Макет.ПолучитьОбласть("Имя")` +- **Columns** — вертикальная область (диапазон колонок). Получение: `Макет.ПолучитьОбласть("Имя")` +- **Rectangle** — фиксированная область (строки + колонки). Обычно использует отдельный набор колонок. +- **Drawing** — именованный рисунок/штрихкод. + +### Пересечения + +Когда есть области и Rows, и Columns (этикетки, ценники), скрипт выводит пары пересечений: + +``` +--- Intersections (use with GetArea) --- + ВысотаЭтикетки|ШиринаЭтикетки +``` + +В BSL: `Макет.ПолучитьОбласть("ВысотаЭтикетки|ШиринаЭтикетки")` + +### Параметры и detailParameter + +Параметры перечислены по областям. Если у параметра есть `detailParameter` (расшифровка), он показан ниже: + +``` +--- Parameters by area --- + Поставщик: ПредставлениеПоставщика + detail: ПредставлениеПоставщика->Поставщик + Строка: НомерСтроки, Товар, Количество, Цена, Сумма, ... (+3) + detail: Товар->Номенклатура +``` + +Это означает: параметр `Товар` отображает значение, а при клике открывает `Номенклатура` (объект расшифровки). + +### Параметры из шаблонов (суффикс `[tpl]`) + +Некоторые параметры встроены в шаблонный текст: `"Инв № [ИнвентарныйНомер]"`. Они заполняются через fillType=Template, а не fillType=Parameter. Скрипт всегда извлекает их и помечает суффиксом `[tpl]`: + +``` + НумерацияЛистов: Номер [tpl], Дата [tpl], НомерЛиста [tpl] +``` + +В BSL шаблонные параметры заполняются так же, как обычные: +```bsl +Область.Параметры.Номер = НомерДокумента; +Область.Параметры.Дата = ДатаДокумента; +``` + +Числовые подстановки вроде `[5]`, `[6]` (ссылки на сноски в официальных формах) игнорируются. + +### Текстовое содержимое (`-WithText`) + +Показывает статический текст (надписи, заголовки) и шаблонные строки с подстановками `[Параметр]`: + +``` +--- Text content --- + ШапкаТаблицы: + Text: "№", "Товар", "Ед. изм.", "Кол-во", "Цена", "Сумма" + Строка: + Templates: "Инв № [ИнвентарныйНомер]" +``` + +- **Text** — статические надписи (fillType=Text). Полезно для понимания назначения колонок. +- **Templates** — текст с подстановками `[ИмяПараметра]` (fillType=Template). Параметр внутри `[]` заполняется программно. + diff --git a/.claude/skills/mxl-info/scripts/mxl-info.ps1 b/.claude/skills/mxl-info/scripts/mxl-info.ps1 index 44ea6bb8..ce65dd96 100644 --- a/.claude/skills/mxl-info/scripts/mxl-info.ps1 +++ b/.claude/skills/mxl-info/scripts/mxl-info.ps1 @@ -1,544 +1,544 @@ -# mxl-info v1.1 — Analyze 1C spreadsheet structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Alias('Path')] - [string]$TemplatePath, - [string]$ProcessorName, - [string]$TemplateName, - [string]$SrcDir = "src", - [ValidateSet("text", "json")] - [string]$Format = "text", - [switch]$WithText, - [int]$MaxParams = 10, - [int]$Limit = 150, - [int]$Offset = 0 -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve template path --- - -if (-not $TemplatePath) { - if (-not $ProcessorName -or -not $TemplateName) { - Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName" - exit 1 - } - $TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml" -} - -if (-not (Test-Path $TemplatePath)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -# --- Load XML --- - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load((Resolve-Path $TemplatePath).Path) - -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") -$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - -$root = $xmlDoc.DocumentElement - -# --- Column sets --- - -$columnSets = @() -$defaultColCount = 0 - -foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { - $sizeNode = $cols.SelectSingleNode("d:size", $nsMgr) - $idNode = $cols.SelectSingleNode("d:id", $nsMgr) - $size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } - - if ($idNode) { - $columnSets += @{ Id = $idNode.InnerText; Size = $size } - } else { - $defaultColCount = $size - } -} - -# --- Rows: collect row data --- - -$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr) -$totalRows = $rowNodes.Count - -$heightNode = $root.SelectSingleNode("d:height", $nsMgr) -$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { $totalRows } - -# --- Named items --- - -$namedAreas = @() -$namedDrawings = @() - -foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) { - $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - $name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText - - if ($niType -like "*NamedItemCells*") { - $area = $ni.SelectSingleNode("d:area", $nsMgr) - $areaType = $area.SelectSingleNode("d:type", $nsMgr).InnerText - $beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText - $endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText - $beginCol = [int]$area.SelectSingleNode("d:beginColumn", $nsMgr).InnerText - $endCol = [int]$area.SelectSingleNode("d:endColumn", $nsMgr).InnerText - $colsId = $null - $colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr) - if ($colsIdNode) { $colsId = $colsIdNode.InnerText } - - $namedAreas += @{ - Name = $name - AreaType = $areaType - BeginRow = $beginRow - EndRow = $endRow - BeginCol = $beginCol - EndCol = $endCol - ColumnsID = $colsId - } - } elseif ($niType -like "*NamedItemDrawing*") { - $drawId = $ni.SelectSingleNode("d:drawingID", $nsMgr).InnerText - $namedDrawings += @{ Name = $name; DrawingID = $drawId } - } -} - -# --- Scan rows for parameters and text --- - -# Build row index map: rowIndex -> XmlNode -$rowMap = @{} -foreach ($ri in $rowNodes) { - $idx = [int]$ri.SelectSingleNode("d:index", $nsMgr).InnerText - $rowMap[$idx] = $ri -} - -function Get-CellData { - param($rowNode, [System.Xml.XmlNamespaceManager]$ns, [bool]$includeText) - - $row = $rowNode.SelectSingleNode("d:row", $ns) - if (-not $row) { return @() } - - $results = @() - foreach ($cGroup in $row.SelectNodes("d:c", $ns)) { - $cell = $cGroup.SelectSingleNode("d:c", $ns) - if (-not $cell) { continue } - - $param = $cell.SelectSingleNode("d:parameter", $ns) - $detail = $cell.SelectSingleNode("d:detailParameter", $ns) - $tl = $cell.SelectSingleNode("d:tl", $ns) - - if ($param) { - $entry = @{ Kind = "Parameter"; Value = $param.InnerText } - if ($detail) { $entry.Detail = $detail.InnerText } - $results += $entry - } - - if ($tl) { - $content = $tl.SelectSingleNode("v8:item/v8:content", $ns) - if ($content -and $content.InnerText) { - $text = $content.InnerText - $isTemplate = $text -match '\[.+\]' - - if ($isTemplate) { - # Always extract parameter names from [Param] placeholders - # Skip numeric-only like [5] — these are footnote refs in legal forms - foreach ($m in [regex]::Matches($text, '\[([^\]]+)\]')) { - $val = $m.Groups[1].Value - if ($val -notmatch '^\d+$') { - $results += @{ Kind = "TemplateParam"; Value = $val } - } - } - # Full template text only with -WithText - if ($includeText) { - $results += @{ Kind = "Template"; Value = $text } - } - } elseif ($includeText) { - $results += @{ Kind = "Text"; Value = $text } - } - } - } - } - return $results -} - -function Get-AreaCellData { - param( - [hashtable]$area, - [hashtable]$rowMap, - [System.Xml.XmlNamespaceManager]$ns, - [bool]$includeText - ) - - $params = @() - $details = @() - $texts = @() - $templates = @() - - $startRow = $area.BeginRow - $endRow = $area.EndRow - if ($startRow -eq -1) { $startRow = 0 } - if ($endRow -eq -1) { $endRow = $docHeight - 1 } - - for ($r = $startRow; $r -le $endRow; $r++) { - if ($rowMap.ContainsKey($r)) { - $cells = Get-CellData -rowNode $rowMap[$r] -ns $ns -includeText $includeText - foreach ($c in $cells) { - switch ($c.Kind) { - "Parameter" { - $params += $c.Value - if ($c.Detail) { $details += "$($c.Value)->$($c.Detail)" } - } - "TemplateParam" { $params += "$($c.Value) [tpl]" } - "Text" { $texts += $c.Value } - "Template" { $templates += $c.Value } - } - } - } - } - - return @{ Params = $params; Details = $details; Texts = $texts; Templates = $templates } -} - -# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow -$namedAreas = $namedAreas | Sort-Object { - if ($_.AreaType -eq "Columns") { $_.BeginCol } else { $_.BeginRow } -}, { $_.Name } - -# Collect data for each area -$areaData = @() -$coveredRows = @{} - -foreach ($area in $namedAreas) { - $data = Get-AreaCellData -area $area -rowMap $rowMap -ns $nsMgr -includeText $WithText - $areaData += @{ - Area = $area - Params = $data.Params - Details = $data.Details - Texts = $data.Texts - Templates = $data.Templates - } - - # Track covered rows - $sr = $area.BeginRow - $er = $area.EndRow - if ($sr -ne -1 -and $er -ne -1) { - for ($r = $sr; $r -le $er; $r++) { - $coveredRows[$r] = $true - } - } -} - -# Find parameters outside named areas -$outsideParams = @() -$outsideDetails = @() -$outsideTexts = @() -$outsideTemplates = @() - -foreach ($r in $rowMap.Keys | Sort-Object) { - if (-not $coveredRows.ContainsKey($r)) { - $cells = Get-CellData -rowNode $rowMap[$r] -ns $nsMgr -includeText $WithText - foreach ($c in $cells) { - switch ($c.Kind) { - "Parameter" { - $outsideParams += $c.Value - if ($c.Detail) { $outsideDetails += "$($c.Value)->$($c.Detail)" } - } - "TemplateParam" { $outsideParams += "$($c.Value) [tpl]" } - "Text" { $outsideTexts += $c.Value } - "Template" { $outsideTemplates += $c.Value } - } - } - } -} - -# --- Counts --- - -$mergeCount = $root.SelectNodes("d:merge", $nsMgr).Count -$drawingNodes = $root.SelectNodes("d:drawing", $nsMgr) -$drawingCount = $drawingNodes.Count - -# --- Output --- - -function Truncate-List { - param([string[]]$items, [int]$max) - if ($items.Count -le $max) { - return ($items -join ", ") - } - $shown = ($items[0..($max - 1)] -join ", ") - $remaining = $items.Count - $max - return "$shown, ... (+$remaining)" -} - -# Determine template name from path -$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath))) - -if ($Format -eq "json") { - $result = @{ - name = $templateName - rows = $docHeight - columns = $defaultColCount - columnSets = @($columnSets) - areas = @() - outsideParams = @($outsideParams) - mergeCount = $mergeCount - drawingCount = $drawingCount - } - - foreach ($ad in $areaData) { - $areaObj = @{ - name = $ad.Area.Name - type = $ad.Area.AreaType - beginRow = $ad.Area.BeginRow - endRow = $ad.Area.EndRow - beginCol = $ad.Area.BeginCol - endCol = $ad.Area.EndCol - params = @($ad.Params) - } - if ($ad.Area.ColumnsID) { $areaObj.columnsID = $ad.Area.ColumnsID } - if ($WithText) { - $areaObj.texts = @($ad.Texts) - $areaObj.templates = @($ad.Templates) - } - $result.areas += $areaObj - } - - if ($WithText) { - $result.outsideTexts = @($outsideTexts) - $result.outsideTemplates = @($outsideTemplates) - } - - foreach ($nd in $namedDrawings) { - $result.areas += @{ - name = $nd.Name - type = "Drawing" - drawingID = $nd.DrawingID - } - } - - $result | ConvertTo-Json -Depth 5 - exit 0 -} - -function Get-SupportStatusForPath([string]$targetPath) { - try { - $rp = (Resolve-Path $targetPath).Path - $elemUuid = $null - $binPath = $null - # Reads the uuid of the first metadata element in an .xml file (or $null). - function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null - } - # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). - $elemUuid = Get-RootUuid $rp - $d = [System.IO.Path]::GetDirectoryName($rp) - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $binPath) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } - } - if ($elemUuid -and $binPath) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return "не на поддержке" } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return "снято с поддержки (правки свободны)" } - if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } - if (-not $elemUuid) { return "не на поддержке" } - $u = [regex]::Escape($elemUuid.ToLower()) - $best = $null - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - if ($null -eq $best) { return "не на поддержке" } - switch ($best) { - 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } - 1 { return "редактируется с сохранением поддержки" } - 2 { return "снято с поддержки (правки свободны)" } - } - return "не на поддержке" - } catch { return "не на поддержке" } -} - -# --- Text format output --- - -$lines = @() - -$lines += "=== $templateName ===" -$lines += "Поддержка: $(Get-SupportStatusForPath $TemplatePath)" -$lines += " Rows: $docHeight, Columns: $defaultColCount" - -if ($columnSets.Count -eq 0) { - $lines += " Column sets: 1 (default only)" -} else { - $lines += " Column sets: $($columnSets.Count + 1) (default=$defaultColCount cols + $($columnSets.Count) additional)" - foreach ($cs in $columnSets) { - $lines += " $($cs.Id.Substring(0,8))...: $($cs.Size) cols" - } -} - -$lines += "" -$lines += "--- Named areas ---" - -foreach ($ad in $areaData) { - $a = $ad.Area - $paramCount = $ad.Params.Count - $rowRange = "" - - switch ($a.AreaType) { - "Rows" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow)" } - "Columns" { $rowRange = "cols $($a.BeginCol)-$($a.EndCol)" } - "Rectangle" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow), cols $($a.BeginCol)-$($a.EndCol)" } - } - - $colsInfo = "" - if ($a.ColumnsID) { - $csSize = "" - foreach ($cs in $columnSets) { - if ($cs.Id -eq $a.ColumnsID) { $csSize = " $($cs.Size)cols"; break } - } - $colsInfo = " [colset$csSize]" - } - - $paramInfo = "($paramCount params)" - $nameStr = $a.Name.PadRight(25) - $typeStr = $a.AreaType.PadRight(12) - $lines += " $nameStr $typeStr $rowRange $paramInfo$colsInfo" -} - -foreach ($nd in $namedDrawings) { - $nameStr = $nd.Name.PadRight(25) - $lines += " $nameStr Drawing drawingID=$($nd.DrawingID)" -} - -# Detect intersection pairs (Rows + Columns areas that overlap) -$rowsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Rows" } -$colsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Columns" } -$intersections = @() -if ($rowsAreas -and $colsAreas) { - foreach ($ra in $rowsAreas) { - foreach ($ca in $colsAreas) { - $intersections += "$($ra.Area.Name)|$($ca.Area.Name)" - } - } -} - -if ($intersections.Count -gt 0) { - $lines += "" - $lines += "--- Intersections (use with GetArea) ---" - foreach ($pair in $intersections) { - $lines += " $pair" - } -} - -# Parameters by area -$hasParams = ($areaData | Where-Object { $_.Params.Count -gt 0 }) -or ($outsideParams.Count -gt 0) - -if ($hasParams) { - $lines += "" - $lines += "--- Parameters by area ---" - foreach ($ad in $areaData) { - if ($ad.Params.Count -gt 0) { - $paramStr = Truncate-List -items $ad.Params -max $MaxParams - $lines += " $($ad.Area.Name): $paramStr" - # Show detailParameters if any - if ($ad.Details.Count -gt 0) { - $detailStr = Truncate-List -items $ad.Details -max $MaxParams - $lines += " detail: $detailStr" - } - } - } - if ($outsideParams.Count -gt 0) { - $paramStr = Truncate-List -items $outsideParams -max $MaxParams - $lines += " (outside areas): $paramStr" - if ($outsideDetails.Count -gt 0) { - $detailStr = Truncate-List -items $outsideDetails -max $MaxParams - $lines += " detail: $detailStr" - } - } -} - -# WithText sections -if ($WithText) { - $hasText = ($areaData | Where-Object { $_.Texts.Count -gt 0 -or $_.Templates.Count -gt 0 }) -or ($outsideTexts.Count -gt 0) -or ($outsideTemplates.Count -gt 0) - - if ($hasText) { - $lines += "" - $lines += "--- Text content ---" - foreach ($ad in $areaData) { - if ($ad.Texts.Count -gt 0 -or $ad.Templates.Count -gt 0) { - $lines += " $($ad.Area.Name):" - if ($ad.Texts.Count -gt 0) { - $textItems = $ad.Texts | ForEach-Object { "`"$_`"" } - $textStr = Truncate-List -items $textItems -max $MaxParams - $lines += " Text: $textStr" - } - if ($ad.Templates.Count -gt 0) { - $tplItems = $ad.Templates | ForEach-Object { "`"$_`"" } - $tplStr = Truncate-List -items $tplItems -max $MaxParams - $lines += " Templates: $tplStr" - } - } - } - if ($outsideTexts.Count -gt 0 -or $outsideTemplates.Count -gt 0) { - $lines += " (outside areas):" - if ($outsideTexts.Count -gt 0) { - $textItems = $outsideTexts | ForEach-Object { "`"$_`"" } - $textStr = Truncate-List -items $textItems -max $MaxParams - $lines += " Text: $textStr" - } - if ($outsideTemplates.Count -gt 0) { - $tplItems = $outsideTemplates | ForEach-Object { "`"$_`"" } - $tplStr = Truncate-List -items $tplItems -max $MaxParams - $lines += " Templates: $tplStr" - } - } - } -} - -$lines += "" -$lines += "--- Stats ---" -$lines += " Merges: $mergeCount" -$lines += " Drawings: $drawingCount" - -# --- Truncation protection --- - -$totalLines = $lines.Count - -if ($Offset -gt 0) { - if ($Offset -ge $totalLines) { - Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." - exit 0 - } - $lines = $lines[$Offset..($totalLines - 1)] -} - -if ($lines.Count -gt $Limit) { - $shown = $lines[0..($Limit - 1)] - foreach ($l in $shown) { Write-Host $l } - $remaining = $totalLines - $Offset - $Limit - Write-Host "" - Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." -} else { - foreach ($l in $lines) { Write-Host $l } -} +# mxl-info v1.1 — Analyze 1C spreadsheet structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Alias('Path')] + [string]$TemplatePath, + [string]$ProcessorName, + [string]$TemplateName, + [string]$SrcDir = "src", + [ValidateSet("text", "json")] + [string]$Format = "text", + [switch]$WithText, + [int]$MaxParams = 10, + [int]$Limit = 150, + [int]$Offset = 0 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve template path --- + +if (-not $TemplatePath) { + if (-not $ProcessorName -or -not $TemplateName) { + Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName" + exit 1 + } + $TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml" +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +# --- Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load((Resolve-Path $TemplatePath).Path) + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") +$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + +$root = $xmlDoc.DocumentElement + +# --- Column sets --- + +$columnSets = @() +$defaultColCount = 0 + +foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { + $sizeNode = $cols.SelectSingleNode("d:size", $nsMgr) + $idNode = $cols.SelectSingleNode("d:id", $nsMgr) + $size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } + + if ($idNode) { + $columnSets += @{ Id = $idNode.InnerText; Size = $size } + } else { + $defaultColCount = $size + } +} + +# --- Rows: collect row data --- + +$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr) +$totalRows = $rowNodes.Count + +$heightNode = $root.SelectSingleNode("d:height", $nsMgr) +$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { $totalRows } + +# --- Named items --- + +$namedAreas = @() +$namedDrawings = @() + +foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) { + $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText + + if ($niType -like "*NamedItemCells*") { + $area = $ni.SelectSingleNode("d:area", $nsMgr) + $areaType = $area.SelectSingleNode("d:type", $nsMgr).InnerText + $beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText + $endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText + $beginCol = [int]$area.SelectSingleNode("d:beginColumn", $nsMgr).InnerText + $endCol = [int]$area.SelectSingleNode("d:endColumn", $nsMgr).InnerText + $colsId = $null + $colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr) + if ($colsIdNode) { $colsId = $colsIdNode.InnerText } + + $namedAreas += @{ + Name = $name + AreaType = $areaType + BeginRow = $beginRow + EndRow = $endRow + BeginCol = $beginCol + EndCol = $endCol + ColumnsID = $colsId + } + } elseif ($niType -like "*NamedItemDrawing*") { + $drawId = $ni.SelectSingleNode("d:drawingID", $nsMgr).InnerText + $namedDrawings += @{ Name = $name; DrawingID = $drawId } + } +} + +# --- Scan rows for parameters and text --- + +# Build row index map: rowIndex -> XmlNode +$rowMap = @{} +foreach ($ri in $rowNodes) { + $idx = [int]$ri.SelectSingleNode("d:index", $nsMgr).InnerText + $rowMap[$idx] = $ri +} + +function Get-CellData { + param($rowNode, [System.Xml.XmlNamespaceManager]$ns, [bool]$includeText) + + $row = $rowNode.SelectSingleNode("d:row", $ns) + if (-not $row) { return @() } + + $results = @() + foreach ($cGroup in $row.SelectNodes("d:c", $ns)) { + $cell = $cGroup.SelectSingleNode("d:c", $ns) + if (-not $cell) { continue } + + $param = $cell.SelectSingleNode("d:parameter", $ns) + $detail = $cell.SelectSingleNode("d:detailParameter", $ns) + $tl = $cell.SelectSingleNode("d:tl", $ns) + + if ($param) { + $entry = @{ Kind = "Parameter"; Value = $param.InnerText } + if ($detail) { $entry.Detail = $detail.InnerText } + $results += $entry + } + + if ($tl) { + $content = $tl.SelectSingleNode("v8:item/v8:content", $ns) + if ($content -and $content.InnerText) { + $text = $content.InnerText + $isTemplate = $text -match '\[.+\]' + + if ($isTemplate) { + # Always extract parameter names from [Param] placeholders + # Skip numeric-only like [5] — these are footnote refs in legal forms + foreach ($m in [regex]::Matches($text, '\[([^\]]+)\]')) { + $val = $m.Groups[1].Value + if ($val -notmatch '^\d+$') { + $results += @{ Kind = "TemplateParam"; Value = $val } + } + } + # Full template text only with -WithText + if ($includeText) { + $results += @{ Kind = "Template"; Value = $text } + } + } elseif ($includeText) { + $results += @{ Kind = "Text"; Value = $text } + } + } + } + } + return $results +} + +function Get-AreaCellData { + param( + [hashtable]$area, + [hashtable]$rowMap, + [System.Xml.XmlNamespaceManager]$ns, + [bool]$includeText + ) + + $params = @() + $details = @() + $texts = @() + $templates = @() + + $startRow = $area.BeginRow + $endRow = $area.EndRow + if ($startRow -eq -1) { $startRow = 0 } + if ($endRow -eq -1) { $endRow = $docHeight - 1 } + + for ($r = $startRow; $r -le $endRow; $r++) { + if ($rowMap.ContainsKey($r)) { + $cells = Get-CellData -rowNode $rowMap[$r] -ns $ns -includeText $includeText + foreach ($c in $cells) { + switch ($c.Kind) { + "Parameter" { + $params += $c.Value + if ($c.Detail) { $details += "$($c.Value)->$($c.Detail)" } + } + "TemplateParam" { $params += "$($c.Value) [tpl]" } + "Text" { $texts += $c.Value } + "Template" { $templates += $c.Value } + } + } + } + } + + return @{ Params = $params; Details = $details; Texts = $texts; Templates = $templates } +} + +# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow +$namedAreas = $namedAreas | Sort-Object { + if ($_.AreaType -eq "Columns") { $_.BeginCol } else { $_.BeginRow } +}, { $_.Name } + +# Collect data for each area +$areaData = @() +$coveredRows = @{} + +foreach ($area in $namedAreas) { + $data = Get-AreaCellData -area $area -rowMap $rowMap -ns $nsMgr -includeText $WithText + $areaData += @{ + Area = $area + Params = $data.Params + Details = $data.Details + Texts = $data.Texts + Templates = $data.Templates + } + + # Track covered rows + $sr = $area.BeginRow + $er = $area.EndRow + if ($sr -ne -1 -and $er -ne -1) { + for ($r = $sr; $r -le $er; $r++) { + $coveredRows[$r] = $true + } + } +} + +# Find parameters outside named areas +$outsideParams = @() +$outsideDetails = @() +$outsideTexts = @() +$outsideTemplates = @() + +foreach ($r in $rowMap.Keys | Sort-Object) { + if (-not $coveredRows.ContainsKey($r)) { + $cells = Get-CellData -rowNode $rowMap[$r] -ns $nsMgr -includeText $WithText + foreach ($c in $cells) { + switch ($c.Kind) { + "Parameter" { + $outsideParams += $c.Value + if ($c.Detail) { $outsideDetails += "$($c.Value)->$($c.Detail)" } + } + "TemplateParam" { $outsideParams += "$($c.Value) [tpl]" } + "Text" { $outsideTexts += $c.Value } + "Template" { $outsideTemplates += $c.Value } + } + } + } +} + +# --- Counts --- + +$mergeCount = $root.SelectNodes("d:merge", $nsMgr).Count +$drawingNodes = $root.SelectNodes("d:drawing", $nsMgr) +$drawingCount = $drawingNodes.Count + +# --- Output --- + +function Truncate-List { + param([string[]]$items, [int]$max) + if ($items.Count -le $max) { + return ($items -join ", ") + } + $shown = ($items[0..($max - 1)] -join ", ") + $remaining = $items.Count - $max + return "$shown, ... (+$remaining)" +} + +# Determine template name from path +$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath))) + +if ($Format -eq "json") { + $result = @{ + name = $templateName + rows = $docHeight + columns = $defaultColCount + columnSets = @($columnSets) + areas = @() + outsideParams = @($outsideParams) + mergeCount = $mergeCount + drawingCount = $drawingCount + } + + foreach ($ad in $areaData) { + $areaObj = @{ + name = $ad.Area.Name + type = $ad.Area.AreaType + beginRow = $ad.Area.BeginRow + endRow = $ad.Area.EndRow + beginCol = $ad.Area.BeginCol + endCol = $ad.Area.EndCol + params = @($ad.Params) + } + if ($ad.Area.ColumnsID) { $areaObj.columnsID = $ad.Area.ColumnsID } + if ($WithText) { + $areaObj.texts = @($ad.Texts) + $areaObj.templates = @($ad.Templates) + } + $result.areas += $areaObj + } + + if ($WithText) { + $result.outsideTexts = @($outsideTexts) + $result.outsideTemplates = @($outsideTemplates) + } + + foreach ($nd in $namedDrawings) { + $result.areas += @{ + name = $nd.Name + type = "Drawing" + drawingID = $nd.DrawingID + } + } + + $result | ConvertTo-Json -Depth 5 + exit 0 +} + +function Get-SupportStatusForPath([string]$targetPath) { + try { + $rp = (Resolve-Path $targetPath).Path + $elemUuid = $null + $binPath = $null + # Reads the uuid of the first metadata element in an .xml file (or $null). + function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null + } + # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). + $elemUuid = Get-RootUuid $rp + $d = [System.IO.Path]::GetDirectoryName($rp) + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $binPath) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } + } + if ($elemUuid -and $binPath) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $elemUuid) { return "не на поддержке" } + $u = [regex]::Escape($elemUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + +# --- Text format output --- + +$lines = @() + +$lines += "=== $templateName ===" +$lines += "Поддержка: $(Get-SupportStatusForPath $TemplatePath)" +$lines += " Rows: $docHeight, Columns: $defaultColCount" + +if ($columnSets.Count -eq 0) { + $lines += " Column sets: 1 (default only)" +} else { + $lines += " Column sets: $($columnSets.Count + 1) (default=$defaultColCount cols + $($columnSets.Count) additional)" + foreach ($cs in $columnSets) { + $lines += " $($cs.Id.Substring(0,8))...: $($cs.Size) cols" + } +} + +$lines += "" +$lines += "--- Named areas ---" + +foreach ($ad in $areaData) { + $a = $ad.Area + $paramCount = $ad.Params.Count + $rowRange = "" + + switch ($a.AreaType) { + "Rows" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow)" } + "Columns" { $rowRange = "cols $($a.BeginCol)-$($a.EndCol)" } + "Rectangle" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow), cols $($a.BeginCol)-$($a.EndCol)" } + } + + $colsInfo = "" + if ($a.ColumnsID) { + $csSize = "" + foreach ($cs in $columnSets) { + if ($cs.Id -eq $a.ColumnsID) { $csSize = " $($cs.Size)cols"; break } + } + $colsInfo = " [colset$csSize]" + } + + $paramInfo = "($paramCount params)" + $nameStr = $a.Name.PadRight(25) + $typeStr = $a.AreaType.PadRight(12) + $lines += " $nameStr $typeStr $rowRange $paramInfo$colsInfo" +} + +foreach ($nd in $namedDrawings) { + $nameStr = $nd.Name.PadRight(25) + $lines += " $nameStr Drawing drawingID=$($nd.DrawingID)" +} + +# Detect intersection pairs (Rows + Columns areas that overlap) +$rowsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Rows" } +$colsAreas = $areaData | Where-Object { $_.Area.AreaType -eq "Columns" } +$intersections = @() +if ($rowsAreas -and $colsAreas) { + foreach ($ra in $rowsAreas) { + foreach ($ca in $colsAreas) { + $intersections += "$($ra.Area.Name)|$($ca.Area.Name)" + } + } +} + +if ($intersections.Count -gt 0) { + $lines += "" + $lines += "--- Intersections (use with GetArea) ---" + foreach ($pair in $intersections) { + $lines += " $pair" + } +} + +# Parameters by area +$hasParams = ($areaData | Where-Object { $_.Params.Count -gt 0 }) -or ($outsideParams.Count -gt 0) + +if ($hasParams) { + $lines += "" + $lines += "--- Parameters by area ---" + foreach ($ad in $areaData) { + if ($ad.Params.Count -gt 0) { + $paramStr = Truncate-List -items $ad.Params -max $MaxParams + $lines += " $($ad.Area.Name): $paramStr" + # Show detailParameters if any + if ($ad.Details.Count -gt 0) { + $detailStr = Truncate-List -items $ad.Details -max $MaxParams + $lines += " detail: $detailStr" + } + } + } + if ($outsideParams.Count -gt 0) { + $paramStr = Truncate-List -items $outsideParams -max $MaxParams + $lines += " (outside areas): $paramStr" + if ($outsideDetails.Count -gt 0) { + $detailStr = Truncate-List -items $outsideDetails -max $MaxParams + $lines += " detail: $detailStr" + } + } +} + +# WithText sections +if ($WithText) { + $hasText = ($areaData | Where-Object { $_.Texts.Count -gt 0 -or $_.Templates.Count -gt 0 }) -or ($outsideTexts.Count -gt 0) -or ($outsideTemplates.Count -gt 0) + + if ($hasText) { + $lines += "" + $lines += "--- Text content ---" + foreach ($ad in $areaData) { + if ($ad.Texts.Count -gt 0 -or $ad.Templates.Count -gt 0) { + $lines += " $($ad.Area.Name):" + if ($ad.Texts.Count -gt 0) { + $textItems = $ad.Texts | ForEach-Object { "`"$_`"" } + $textStr = Truncate-List -items $textItems -max $MaxParams + $lines += " Text: $textStr" + } + if ($ad.Templates.Count -gt 0) { + $tplItems = $ad.Templates | ForEach-Object { "`"$_`"" } + $tplStr = Truncate-List -items $tplItems -max $MaxParams + $lines += " Templates: $tplStr" + } + } + } + if ($outsideTexts.Count -gt 0 -or $outsideTemplates.Count -gt 0) { + $lines += " (outside areas):" + if ($outsideTexts.Count -gt 0) { + $textItems = $outsideTexts | ForEach-Object { "`"$_`"" } + $textStr = Truncate-List -items $textItems -max $MaxParams + $lines += " Text: $textStr" + } + if ($outsideTemplates.Count -gt 0) { + $tplItems = $outsideTemplates | ForEach-Object { "`"$_`"" } + $tplStr = Truncate-List -items $tplItems -max $MaxParams + $lines += " Templates: $tplStr" + } + } + } +} + +$lines += "" +$lines += "--- Stats ---" +$lines += " Merges: $mergeCount" +$lines += " Drawings: $drawingCount" + +# --- Truncation protection --- + +$totalLines = $lines.Count + +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $lines = $lines[$Offset..($totalLines - 1)] +} + +if ($lines.Count -gt $Limit) { + $shown = $lines[0..($Limit - 1)] + foreach ($l in $shown) { Write-Host $l } + $remaining = $totalLines - $Offset - $Limit + Write-Host "" + Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." +} else { + foreach ($l in $lines) { Write-Host $l } +} diff --git a/.claude/skills/mxl-validate/SKILL.md b/.claude/skills/mxl-validate/SKILL.md index 94cf949d..e817607d 100644 --- a/.claude/skills/mxl-validate/SKILL.md +++ b/.claude/skills/mxl-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: mxl-validate -description: Валидация макета табличного документа (MXL). Используй после создания или модификации макета для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 20] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /mxl-validate — валидация макета табличного документа (MXL) - -Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|---------------|:-----:|---------|--------------------------------------------| -| TemplatePath | да | — | Путь к макету (директория или Template.xml) | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 20 | Остановиться после N ошибок | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/Макет" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма" -``` - +--- +name: mxl-validate +description: Валидация макета табличного документа (MXL). Используй после создания или модификации макета для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 20] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /mxl-validate — валидация макета табличного документа (MXL) + +Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|---------------|:-----:|---------|--------------------------------------------| +| TemplatePath | да | — | Путь к макету (директория или Template.xml) | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 20 | Остановиться после N ошибок | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/Макет" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма" +``` + diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index 86271ea7..b12cb5ef 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 @@ -1,423 +1,423 @@ -# mxl-validate v1.1 — Validate 1C spreadsheet -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Alias('Path')] - [string]$TemplatePath, - [string]$ProcessorName, - [string]$TemplateName, - [string]$SrcDir = "src", - [switch]$Detailed, - [int]$MaxErrors = 20 -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve template path --- - -if (-not $TemplatePath) { - if (-not $ProcessorName -or -not $TemplateName) { - Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName" - exit 1 - } - $TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml" -} - -# A: Directory → Ext/Template.xml -if (Test-Path $TemplatePath -PathType Container) { - $TemplatePath = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" -} -# B1: Missing Ext/ (e.g. Templates/Макет/Template.xml → Templates/Макет/Ext/Template.xml) -if (-not (Test-Path $TemplatePath)) { - $fn = [System.IO.Path]::GetFileName($TemplatePath) - if ($fn -eq "Template.xml") { - $c = Join-Path (Join-Path (Split-Path $TemplatePath) "Ext") $fn - if (Test-Path $c) { $TemplatePath = $c } - } -} -# B2: Descriptor (Templates/Макет.xml → Templates/Макет/Ext/Template.xml) -if (-not (Test-Path $TemplatePath) -and $TemplatePath.EndsWith(".xml")) { - $stem = [System.IO.Path]::GetFileNameWithoutExtension($TemplatePath) - $dir = Split-Path $TemplatePath - $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Template.xml" - if (Test-Path $c) { $TemplatePath = $c } -} - -if (-not (Test-Path $TemplatePath)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -# --- Load XML --- - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load((Resolve-Path $TemplatePath).Path) - -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") -$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - -$root = $xmlDoc.DocumentElement - -# --- Counters --- - -$errors = 0 -$warnings = 0 -$stopped = $false -$script:okCount = 0 - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Write-Host "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Write-Host "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Write-Host "[WARN] $msg" -} - -$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath))) -if ($Detailed) { - Write-Host "=== Validation: $templateName ===" - Write-Host "" -} - -# --- Collect palettes --- - -$lineNodes = $root.SelectNodes("d:line", $nsMgr) -$lineCount = $lineNodes.Count - -$fontNodes = @() -foreach ($node in $root.ChildNodes) { - if ($node.LocalName -eq "font") { $fontNodes += $node } -} -$fontCount = $fontNodes.Count - -$formatNodes = @() -foreach ($node in $root.ChildNodes) { - if ($node.LocalName -eq "format") { $formatNodes += $node } -} -$formatCount = $formatNodes.Count - -$pictureNodes = $root.SelectNodes("d:picture", $nsMgr) -$pictureCount = $pictureNodes.Count - -# --- Collect column sets --- - -$columnSets = @{} # id -> size -$defaultColCount = 0 - -foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { - $sizeNode = $cols.SelectSingleNode("d:size", $nsMgr) - $idNode = $cols.SelectSingleNode("d:id", $nsMgr) - $size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } - - if ($idNode) { - $columnSets[$idNode.InnerText] = $size - } else { - $defaultColCount = $size - } -} - -# --- Check 1: height vs actual rows --- - -$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr) -$heightNode = $root.SelectSingleNode("d:height", $nsMgr) -$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { 0 } - -# Find max row index (not all rows have rowsItem - implicit rows are skipped) -$maxRowIndex = -1 -foreach ($ri in $rowNodes) { - $idxNode = $ri.SelectSingleNode("d:index", $nsMgr) - if ($idxNode) { - $idx = [int]$idxNode.InnerText - if ($idx -gt $maxRowIndex) { $maxRowIndex = $idx } - } -} - -$expectedMinHeight = $maxRowIndex + 1 -if ($docHeight -ge $expectedMinHeight) { - Report-OK "height ($docHeight) >= max row index + 1 ($expectedMinHeight), rowsItem count=$($rowNodes.Count)" -} else { - Report-Error "height=$docHeight but max row index=$maxRowIndex (need at least $expectedMinHeight)" -} -# --- Check 2: vgRows <= height --- - -$vgRowsNode = $root.SelectSingleNode("d:vgRows", $nsMgr) -if ($vgRowsNode) { - $vgRows = [int]$vgRowsNode.InnerText - if ($vgRows -le $docHeight) { - Report-OK "vgRows ($vgRows) <= height ($docHeight)" - } else { - Report-Warn "vgRows ($vgRows) > height ($docHeight)" - } -} - -# --- Build row data for checks --- - -$maxFormatRef = 0 -$maxFontRef = 0 -$maxLineRef = 0 - -# Check format palette references in formats (font, border indices) -foreach ($fmt in $formatNodes) { - $fontIdx = $fmt.SelectSingleNode("d:font", $nsMgr) - if ($fontIdx) { - $val = [int]$fontIdx.InnerText - if ($val -gt $maxFontRef) { $maxFontRef = $val } - } - - foreach ($border in @("d:leftBorder", "d:topBorder", "d:rightBorder", "d:bottomBorder", "d:drawingBorder")) { - $borderNode = $fmt.SelectSingleNode($border, $nsMgr) - if ($borderNode) { - $val = [int]$borderNode.InnerText - if ($val -gt $maxLineRef) { $maxLineRef = $val } - } - } -} - -# --- Check 10: font indices in formats --- - -if ($fontCount -gt 0) { - if ($maxFontRef -lt $fontCount) { - Report-OK "Font refs: max=$maxFontRef, palette size=$fontCount" - } else { - Report-Error "Font index $maxFontRef exceeds palette size ($fontCount)" - } -} elseif ($maxFontRef -gt 0) { - Report-Error "Font index $maxFontRef referenced but no fonts defined" -} else { - Report-OK "No font references" -} - -# --- Check 11: line/border indices in formats --- - -if ($lineCount -gt 0) { - if ($maxLineRef -lt $lineCount) { - Report-OK "Line/border refs: max=$maxLineRef, palette size=$lineCount" - } else { - Report-Error "Line index $maxLineRef exceeds palette size ($lineCount)" - } -} elseif ($maxLineRef -gt 0) { - Report-Error "Line index $maxLineRef referenced but no lines defined" -} else { - Report-OK "No line/border references" -} - -# --- Check 3, 4, 5, 6: row/cell checks --- - -$maxCellFormatRef = 0 -$maxRowFormatRef = 0 -$maxDefaultColIdx = 0 -$rowIndex = 0 - -foreach ($ri in $rowNodes) { - if ($stopped) { break } - - $idxNode = $ri.SelectSingleNode("d:index", $nsMgr) - $rowIndex = if ($idxNode) { [int]$idxNode.InnerText } else { $rowIndex } - - $row = $ri.SelectSingleNode("d:row", $nsMgr) - if (-not $row) { continue } - - # Row formatIndex - $rowFmtNode = $row.SelectSingleNode("d:formatIndex", $nsMgr) - if ($rowFmtNode) { - $val = [int]$rowFmtNode.InnerText - if ($val -gt $maxRowFormatRef) { $maxRowFormatRef = $val } - if ($val -gt $formatCount) { - Report-Error "Row ${rowIndex}: formatIndex=$val > format palette size ($formatCount)" - } - } - - # Check columnsID - $rowColsId = $null - $colsIdNode = $row.SelectSingleNode("d:columnsID", $nsMgr) - if ($colsIdNode) { - $rowColsId = $colsIdNode.InnerText - if (-not $columnSets.ContainsKey($rowColsId)) { - Report-Error "Row ${rowIndex}: columnsID '$($rowColsId.Substring(0,8))...' not found in column sets" - } - } - - # Determine column count for this row - $rowColCount = $defaultColCount - if ($rowColsId -and $columnSets.ContainsKey($rowColsId)) { - $rowColCount = $columnSets[$rowColsId] - } - - # Cell checks - foreach ($cGroup in $row.SelectNodes("d:c", $nsMgr)) { - $iNode = $cGroup.SelectSingleNode("d:i", $nsMgr) - if ($iNode) { - $colIdx = [int]$iNode.InnerText - # Track max index for default column set only - if (-not $rowColsId -and $colIdx -gt $maxDefaultColIdx) { - $maxDefaultColIdx = $colIdx - } - # Check against row's column count - if ($rowColCount -gt 0 -and $colIdx -ge $rowColCount) { - Report-Error "Row ${rowIndex}: column index $colIdx >= column count ($rowColCount)" - } - } - - $cell = $cGroup.SelectSingleNode("d:c", $nsMgr) - if ($cell) { - $fNode = $cell.SelectSingleNode("d:f", $nsMgr) - if ($fNode) { - $val = [int]$fNode.InnerText - if ($val -gt $maxCellFormatRef) { $maxCellFormatRef = $val } - if ($val -gt $formatCount) { - Report-Error "Row ${rowIndex}: cell format index $val > format palette size ($formatCount)" - } - } - } - } - - $rowIndex++ -} - -# Summary checks for format refs -if (-not $stopped) { - if ($maxCellFormatRef -le $formatCount -and $maxRowFormatRef -le $formatCount) { - Report-OK "Format refs: max cell=$maxCellFormatRef, max row=$maxRowFormatRef, palette size=$formatCount" - } -} - -# Check column format indices -foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { - if ($stopped) { break } - foreach ($ci in $cols.SelectNodes("d:columnsItem", $nsMgr)) { - $col = $ci.SelectSingleNode("d:column", $nsMgr) - if ($col) { - $fmtNode = $col.SelectSingleNode("d:formatIndex", $nsMgr) - if ($fmtNode) { - $val = [int]$fmtNode.InnerText - if ($val -gt $formatCount) { - $colIdx = $ci.SelectSingleNode("d:index", $nsMgr).InnerText - Report-Error "Column ${colIdx}: formatIndex=$val > format palette size ($formatCount)" - } - } - } - } -} - -# --- Check 5: column index summary --- - -if (-not $stopped) { - Report-OK "Column indices: max in default set=$maxDefaultColIdx, default column count=$defaultColCount" -} - -# --- Check 7, 8: named areas --- - -foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) { - if ($stopped) { break } - - $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - $name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText - - if ($niType -like "*NamedItemCells*") { - $area = $ni.SelectSingleNode("d:area", $nsMgr) - $beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText - $endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText - - # Check row bounds (skip -1 which means "all") - if ($beginRow -ne -1 -and $beginRow -ge $docHeight) { - Report-Error "Area '$name': beginRow=$beginRow >= height=$docHeight" - } - if ($endRow -ne -1 -and $endRow -ge $docHeight) { - Report-Error "Area '$name': endRow=$endRow >= height=$docHeight" - } - - # Check columnsID reference - $colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr) - if ($colsIdNode) { - $colsId = $colsIdNode.InnerText - if (-not $columnSets.ContainsKey($colsId)) { - Report-Error "Area '$name': columnsID '$($colsId.Substring(0,8))...' not found" - } - } - } -} - -# --- Check 9: merge bounds --- - -foreach ($merge in $root.SelectNodes("d:merge", $nsMgr)) { - if ($stopped) { break } - - $r = [int]$merge.SelectSingleNode("d:r", $nsMgr).InnerText - $c = [int]$merge.SelectSingleNode("d:c", $nsMgr).InnerText - $wNode = $merge.SelectSingleNode("d:w", $nsMgr) - $hNode = $merge.SelectSingleNode("d:h", $nsMgr) - - # r=-1 means all rows, skip bound check - if ($r -ne -1 -and $r -ge $docHeight) { - Report-Error "Merge at row=${r}, col=${c}: row >= height ($docHeight)" - } - - if ($hNode -and $r -ne -1) { - $h = [int]$hNode.InnerText - if (($r + $h) -ge $docHeight) { - Report-Error "Merge at row=${r}: extends to row $($r + $h) >= height ($docHeight)" - } - } - - # Check columnsID in merge - $colsIdNode = $merge.SelectSingleNode("d:columnsID", $nsMgr) - if ($colsIdNode) { - $colsId = $colsIdNode.InnerText - if (-not $columnSets.ContainsKey($colsId)) { - Report-Error "Merge at row=${r}, col=${c}: columnsID '$($colsId.Substring(0,8))...' not found" - } - } -} - -# --- Check 12: drawing picture indices --- - -foreach ($drawing in $root.SelectNodes("d:drawing", $nsMgr)) { - if ($stopped) { break } - - $picIdxNode = $drawing.SelectSingleNode("d:pictureIndex", $nsMgr) - if ($picIdxNode) { - $picIdx = [int]$picIdxNode.InnerText - if ($picIdx -gt $pictureCount) { - $drawId = $drawing.SelectSingleNode("d:id", $nsMgr).InnerText - Report-Error "Drawing id=${drawId}: pictureIndex=$picIdx > picture count ($pictureCount)" - } - } -} - -# --- Summary --- - -$checks = $script:okCount + $errors + $warnings - -if ($errors -eq 0 -and $warnings -eq 0 -and -not $Detailed) { - Write-Host "=== Validation OK: Template.$templateName ($checks checks) ===" -} else { - Write-Host "" - - if ($stopped) { - Write-Host "Stopped after $MaxErrors errors. Fix and re-run." - } - - Write-Host "=== Result: $errors errors, $warnings warnings ($checks checks) ===" -} - -if ($errors -gt 0) { - exit 1 -} else { - exit 0 -} +# mxl-validate v1.1 — Validate 1C spreadsheet +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Alias('Path')] + [string]$TemplatePath, + [string]$ProcessorName, + [string]$TemplateName, + [string]$SrcDir = "src", + [switch]$Detailed, + [int]$MaxErrors = 20 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve template path --- + +if (-not $TemplatePath) { + if (-not $ProcessorName -or -not $TemplateName) { + Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName" + exit 1 + } + $TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml" +} + +# A: Directory → Ext/Template.xml +if (Test-Path $TemplatePath -PathType Container) { + $TemplatePath = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" +} +# B1: Missing Ext/ (e.g. Templates/Макет/Template.xml → Templates/Макет/Ext/Template.xml) +if (-not (Test-Path $TemplatePath)) { + $fn = [System.IO.Path]::GetFileName($TemplatePath) + if ($fn -eq "Template.xml") { + $c = Join-Path (Join-Path (Split-Path $TemplatePath) "Ext") $fn + if (Test-Path $c) { $TemplatePath = $c } + } +} +# B2: Descriptor (Templates/Макет.xml → Templates/Макет/Ext/Template.xml) +if (-not (Test-Path $TemplatePath) -and $TemplatePath.EndsWith(".xml")) { + $stem = [System.IO.Path]::GetFileNameWithoutExtension($TemplatePath) + $dir = Split-Path $TemplatePath + $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Template.xml" + if (Test-Path $c) { $TemplatePath = $c } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +# --- Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load((Resolve-Path $TemplatePath).Path) + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") +$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + +$root = $xmlDoc.DocumentElement + +# --- Counters --- + +$errors = 0 +$warnings = 0 +$stopped = $false +$script:okCount = 0 + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Write-Host "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Write-Host "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Write-Host "[WARN] $msg" +} + +$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath))) +if ($Detailed) { + Write-Host "=== Validation: $templateName ===" + Write-Host "" +} + +# --- Collect palettes --- + +$lineNodes = $root.SelectNodes("d:line", $nsMgr) +$lineCount = $lineNodes.Count + +$fontNodes = @() +foreach ($node in $root.ChildNodes) { + if ($node.LocalName -eq "font") { $fontNodes += $node } +} +$fontCount = $fontNodes.Count + +$formatNodes = @() +foreach ($node in $root.ChildNodes) { + if ($node.LocalName -eq "format") { $formatNodes += $node } +} +$formatCount = $formatNodes.Count + +$pictureNodes = $root.SelectNodes("d:picture", $nsMgr) +$pictureCount = $pictureNodes.Count + +# --- Collect column sets --- + +$columnSets = @{} # id -> size +$defaultColCount = 0 + +foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { + $sizeNode = $cols.SelectSingleNode("d:size", $nsMgr) + $idNode = $cols.SelectSingleNode("d:id", $nsMgr) + $size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } + + if ($idNode) { + $columnSets[$idNode.InnerText] = $size + } else { + $defaultColCount = $size + } +} + +# --- Check 1: height vs actual rows --- + +$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr) +$heightNode = $root.SelectSingleNode("d:height", $nsMgr) +$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { 0 } + +# Find max row index (not all rows have rowsItem - implicit rows are skipped) +$maxRowIndex = -1 +foreach ($ri in $rowNodes) { + $idxNode = $ri.SelectSingleNode("d:index", $nsMgr) + if ($idxNode) { + $idx = [int]$idxNode.InnerText + if ($idx -gt $maxRowIndex) { $maxRowIndex = $idx } + } +} + +$expectedMinHeight = $maxRowIndex + 1 +if ($docHeight -ge $expectedMinHeight) { + Report-OK "height ($docHeight) >= max row index + 1 ($expectedMinHeight), rowsItem count=$($rowNodes.Count)" +} else { + Report-Error "height=$docHeight but max row index=$maxRowIndex (need at least $expectedMinHeight)" +} +# --- Check 2: vgRows <= height --- + +$vgRowsNode = $root.SelectSingleNode("d:vgRows", $nsMgr) +if ($vgRowsNode) { + $vgRows = [int]$vgRowsNode.InnerText + if ($vgRows -le $docHeight) { + Report-OK "vgRows ($vgRows) <= height ($docHeight)" + } else { + Report-Warn "vgRows ($vgRows) > height ($docHeight)" + } +} + +# --- Build row data for checks --- + +$maxFormatRef = 0 +$maxFontRef = 0 +$maxLineRef = 0 + +# Check format palette references in formats (font, border indices) +foreach ($fmt in $formatNodes) { + $fontIdx = $fmt.SelectSingleNode("d:font", $nsMgr) + if ($fontIdx) { + $val = [int]$fontIdx.InnerText + if ($val -gt $maxFontRef) { $maxFontRef = $val } + } + + foreach ($border in @("d:leftBorder", "d:topBorder", "d:rightBorder", "d:bottomBorder", "d:drawingBorder")) { + $borderNode = $fmt.SelectSingleNode($border, $nsMgr) + if ($borderNode) { + $val = [int]$borderNode.InnerText + if ($val -gt $maxLineRef) { $maxLineRef = $val } + } + } +} + +# --- Check 10: font indices in formats --- + +if ($fontCount -gt 0) { + if ($maxFontRef -lt $fontCount) { + Report-OK "Font refs: max=$maxFontRef, palette size=$fontCount" + } else { + Report-Error "Font index $maxFontRef exceeds palette size ($fontCount)" + } +} elseif ($maxFontRef -gt 0) { + Report-Error "Font index $maxFontRef referenced but no fonts defined" +} else { + Report-OK "No font references" +} + +# --- Check 11: line/border indices in formats --- + +if ($lineCount -gt 0) { + if ($maxLineRef -lt $lineCount) { + Report-OK "Line/border refs: max=$maxLineRef, palette size=$lineCount" + } else { + Report-Error "Line index $maxLineRef exceeds palette size ($lineCount)" + } +} elseif ($maxLineRef -gt 0) { + Report-Error "Line index $maxLineRef referenced but no lines defined" +} else { + Report-OK "No line/border references" +} + +# --- Check 3, 4, 5, 6: row/cell checks --- + +$maxCellFormatRef = 0 +$maxRowFormatRef = 0 +$maxDefaultColIdx = 0 +$rowIndex = 0 + +foreach ($ri in $rowNodes) { + if ($stopped) { break } + + $idxNode = $ri.SelectSingleNode("d:index", $nsMgr) + $rowIndex = if ($idxNode) { [int]$idxNode.InnerText } else { $rowIndex } + + $row = $ri.SelectSingleNode("d:row", $nsMgr) + if (-not $row) { continue } + + # Row formatIndex + $rowFmtNode = $row.SelectSingleNode("d:formatIndex", $nsMgr) + if ($rowFmtNode) { + $val = [int]$rowFmtNode.InnerText + if ($val -gt $maxRowFormatRef) { $maxRowFormatRef = $val } + if ($val -gt $formatCount) { + Report-Error "Row ${rowIndex}: formatIndex=$val > format palette size ($formatCount)" + } + } + + # Check columnsID + $rowColsId = $null + $colsIdNode = $row.SelectSingleNode("d:columnsID", $nsMgr) + if ($colsIdNode) { + $rowColsId = $colsIdNode.InnerText + if (-not $columnSets.ContainsKey($rowColsId)) { + Report-Error "Row ${rowIndex}: columnsID '$($rowColsId.Substring(0,8))...' not found in column sets" + } + } + + # Determine column count for this row + $rowColCount = $defaultColCount + if ($rowColsId -and $columnSets.ContainsKey($rowColsId)) { + $rowColCount = $columnSets[$rowColsId] + } + + # Cell checks + foreach ($cGroup in $row.SelectNodes("d:c", $nsMgr)) { + $iNode = $cGroup.SelectSingleNode("d:i", $nsMgr) + if ($iNode) { + $colIdx = [int]$iNode.InnerText + # Track max index for default column set only + if (-not $rowColsId -and $colIdx -gt $maxDefaultColIdx) { + $maxDefaultColIdx = $colIdx + } + # Check against row's column count + if ($rowColCount -gt 0 -and $colIdx -ge $rowColCount) { + Report-Error "Row ${rowIndex}: column index $colIdx >= column count ($rowColCount)" + } + } + + $cell = $cGroup.SelectSingleNode("d:c", $nsMgr) + if ($cell) { + $fNode = $cell.SelectSingleNode("d:f", $nsMgr) + if ($fNode) { + $val = [int]$fNode.InnerText + if ($val -gt $maxCellFormatRef) { $maxCellFormatRef = $val } + if ($val -gt $formatCount) { + Report-Error "Row ${rowIndex}: cell format index $val > format palette size ($formatCount)" + } + } + } + } + + $rowIndex++ +} + +# Summary checks for format refs +if (-not $stopped) { + if ($maxCellFormatRef -le $formatCount -and $maxRowFormatRef -le $formatCount) { + Report-OK "Format refs: max cell=$maxCellFormatRef, max row=$maxRowFormatRef, palette size=$formatCount" + } +} + +# Check column format indices +foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { + if ($stopped) { break } + foreach ($ci in $cols.SelectNodes("d:columnsItem", $nsMgr)) { + $col = $ci.SelectSingleNode("d:column", $nsMgr) + if ($col) { + $fmtNode = $col.SelectSingleNode("d:formatIndex", $nsMgr) + if ($fmtNode) { + $val = [int]$fmtNode.InnerText + if ($val -gt $formatCount) { + $colIdx = $ci.SelectSingleNode("d:index", $nsMgr).InnerText + Report-Error "Column ${colIdx}: formatIndex=$val > format palette size ($formatCount)" + } + } + } + } +} + +# --- Check 5: column index summary --- + +if (-not $stopped) { + Report-OK "Column indices: max in default set=$maxDefaultColIdx, default column count=$defaultColCount" +} + +# --- Check 7, 8: named areas --- + +foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) { + if ($stopped) { break } + + $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText + + if ($niType -like "*NamedItemCells*") { + $area = $ni.SelectSingleNode("d:area", $nsMgr) + $beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText + $endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText + + # Check row bounds (skip -1 which means "all") + if ($beginRow -ne -1 -and $beginRow -ge $docHeight) { + Report-Error "Area '$name': beginRow=$beginRow >= height=$docHeight" + } + if ($endRow -ne -1 -and $endRow -ge $docHeight) { + Report-Error "Area '$name': endRow=$endRow >= height=$docHeight" + } + + # Check columnsID reference + $colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr) + if ($colsIdNode) { + $colsId = $colsIdNode.InnerText + if (-not $columnSets.ContainsKey($colsId)) { + Report-Error "Area '$name': columnsID '$($colsId.Substring(0,8))...' not found" + } + } + } +} + +# --- Check 9: merge bounds --- + +foreach ($merge in $root.SelectNodes("d:merge", $nsMgr)) { + if ($stopped) { break } + + $r = [int]$merge.SelectSingleNode("d:r", $nsMgr).InnerText + $c = [int]$merge.SelectSingleNode("d:c", $nsMgr).InnerText + $wNode = $merge.SelectSingleNode("d:w", $nsMgr) + $hNode = $merge.SelectSingleNode("d:h", $nsMgr) + + # r=-1 means all rows, skip bound check + if ($r -ne -1 -and $r -ge $docHeight) { + Report-Error "Merge at row=${r}, col=${c}: row >= height ($docHeight)" + } + + if ($hNode -and $r -ne -1) { + $h = [int]$hNode.InnerText + if (($r + $h) -ge $docHeight) { + Report-Error "Merge at row=${r}: extends to row $($r + $h) >= height ($docHeight)" + } + } + + # Check columnsID in merge + $colsIdNode = $merge.SelectSingleNode("d:columnsID", $nsMgr) + if ($colsIdNode) { + $colsId = $colsIdNode.InnerText + if (-not $columnSets.ContainsKey($colsId)) { + Report-Error "Merge at row=${r}, col=${c}: columnsID '$($colsId.Substring(0,8))...' not found" + } + } +} + +# --- Check 12: drawing picture indices --- + +foreach ($drawing in $root.SelectNodes("d:drawing", $nsMgr)) { + if ($stopped) { break } + + $picIdxNode = $drawing.SelectSingleNode("d:pictureIndex", $nsMgr) + if ($picIdxNode) { + $picIdx = [int]$picIdxNode.InnerText + if ($picIdx -gt $pictureCount) { + $drawId = $drawing.SelectSingleNode("d:id", $nsMgr).InnerText + Report-Error "Drawing id=${drawId}: pictureIndex=$picIdx > picture count ($pictureCount)" + } + } +} + +# --- Summary --- + +$checks = $script:okCount + $errors + $warnings + +if ($errors -eq 0 -and $warnings -eq 0 -and -not $Detailed) { + Write-Host "=== Validation OK: Template.$templateName ($checks checks) ===" +} else { + Write-Host "" + + if ($stopped) { + Write-Host "Stopped after $MaxErrors errors. Fix and re-run." + } + + Write-Host "=== Result: $errors errors, $warnings warnings ($checks checks) ===" +} + +if ($errors -gt 0) { + exit 1 +} else { + exit 0 +} diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md index 038efcdc..d998e1e9 100644 --- a/.claude/skills/role-compile/SKILL.md +++ b/.claude/skills/role-compile/SKILL.md @@ -1,109 +1,109 @@ ---- -name: role-compile -description: Создание роли 1С из описания прав. Используй когда нужно создать новую роль с набором прав на объекты -argument-hint: -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /role-compile — генерация роли 1С из JSON DSL - -Принимает JSON-определение роли → генерирует `Roles/Имя.xml` (метаданные) и `Roles/Имя/Ext/Rights.xml` (права). UUID автоматически. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `JsonPath` | Путь к JSON-определению роли | -| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Roles/` и т.д.) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" -JsonPath "" -OutputDir "" -``` - -Создаёт `{OutputDir}/Roles/Имя.xml` и `{OutputDir}/Roles/Имя/Ext/Rights.xml`. Регистрирует `` в `Configuration.xml`. - -## JSON DSL - -### Структура - -```json -{ "name": "ИмяРоли", "synonym": "Отображаемое имя", "objects": [...], "templates": [...] } -``` - -Необязательные: `comment` (""), `setForNewObjects` (false), `setForAttributesByDefault` (true), `independentRightsOfChildObjects` (false). - -### Shorthand-строки и объектная форма - -```json -"objects": [ - "Catalog.Номенклатура: @view", - "Document.Реализация: @edit", - "DataProcessor.Загрузка: @view", - "InformationRegister.Цены: Read, Update", - { "name": "Document.Продажа", "preset": "view", "rights": {"Delete": false}, "rls": {"Read": "#Шаблон(\"\")"} } -] -``` - -- Shorthand: `"Тип.Имя: @пресет"` или `"Тип.Имя: Право1, Право2"` -- Объектная форма: `preset` + `rights` (переопределения) + `rls` (ограничения) - -### Пресеты - -| Пресет | Действие | -|--------|----------| -| `@view` | Просмотр — Read, View (+InputByString для справочников/документов; Use+View для обработок/отчётов) | -| `@edit` | Полное редактирование — CRUD + Interactive* + Posting (документы) | - -`@` обязателен в shorthand. В объектной форме — `"preset": "view"` без `@`. - -Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`. - -### Русские синонимы - -Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Смешивание допустимо: `"Справочник.Контрагенты: Чтение, View"`. - -### Шаблоны RLS - -```json -"templates": [{"name": "ДляОбъекта(Мод)", "condition": "ГДЕ Организация = &ТекОрг"}] -``` - -Ссылка в `rls`: `"#ДляОбъекта(\"\")"`. Символ `&` автоматически экранируется в XML. - -## Примеры - -### Простая роль - -```json -{ - "name": "ЧтениеНоменклатуры", "synonym": "Чтение номенклатуры", - "objects": ["Catalog.Номенклатура: @view", "Catalog.Контрагенты: @view", "DataProcessor.Загрузка: @view"] -} -``` - -### Роль с RLS - -```json -{ - "name": "ЧтениеДокументовПоОрганизации", - "synonym": "Чтение документов (ограничение по организации)", - "objects": [ - "Catalog.Организации: @view", - {"name": "Document.РеализацияТоваровУслуг", "preset": "view", "rls": {"Read": "#ДляОбъекта(\"\")"}} - ], - "templates": [{"name": "ДляОбъекта(Модификатор)", "condition": "ГДЕ Организация = &ТекущаяОрганизация"}] -} -``` - -Подробные таблицы пресетов, русских синонимов и дополнительные примеры — в `dsl-reference.md`. - -## Верификация - -``` -/role-validate [MetadataPath] — проверка корректности XML, прав, RLS -/role-info — визуальная сводка структуры -``` +--- +name: role-compile +description: Создание роли 1С из описания прав. Используй когда нужно создать новую роль с набором прав на объекты +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /role-compile — генерация роли 1С из JSON DSL + +Принимает JSON-определение роли → генерирует `Roles/Имя.xml` (метаданные) и `Roles/Имя/Ext/Rights.xml` (права). UUID автоматически. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `JsonPath` | Путь к JSON-определению роли | +| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Roles/` и т.д.) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" -JsonPath "" -OutputDir "" +``` + +Создаёт `{OutputDir}/Roles/Имя.xml` и `{OutputDir}/Roles/Имя/Ext/Rights.xml`. Регистрирует `` в `Configuration.xml`. + +## JSON DSL + +### Структура + +```json +{ "name": "ИмяРоли", "synonym": "Отображаемое имя", "objects": [...], "templates": [...] } +``` + +Необязательные: `comment` (""), `setForNewObjects` (false), `setForAttributesByDefault` (true), `independentRightsOfChildObjects` (false). + +### Shorthand-строки и объектная форма + +```json +"objects": [ + "Catalog.Номенклатура: @view", + "Document.Реализация: @edit", + "DataProcessor.Загрузка: @view", + "InformationRegister.Цены: Read, Update", + { "name": "Document.Продажа", "preset": "view", "rights": {"Delete": false}, "rls": {"Read": "#Шаблон(\"\")"} } +] +``` + +- Shorthand: `"Тип.Имя: @пресет"` или `"Тип.Имя: Право1, Право2"` +- Объектная форма: `preset` + `rights` (переопределения) + `rls` (ограничения) + +### Пресеты + +| Пресет | Действие | +|--------|----------| +| `@view` | Просмотр — Read, View (+InputByString для справочников/документов; Use+View для обработок/отчётов) | +| `@edit` | Полное редактирование — CRUD + Interactive* + Posting (документы) | + +`@` обязателен в shorthand. В объектной форме — `"preset": "view"` без `@`. + +Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`. + +### Русские синонимы + +Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Смешивание допустимо: `"Справочник.Контрагенты: Чтение, View"`. + +### Шаблоны RLS + +```json +"templates": [{"name": "ДляОбъекта(Мод)", "condition": "ГДЕ Организация = &ТекОрг"}] +``` + +Ссылка в `rls`: `"#ДляОбъекта(\"\")"`. Символ `&` автоматически экранируется в XML. + +## Примеры + +### Простая роль + +```json +{ + "name": "ЧтениеНоменклатуры", "synonym": "Чтение номенклатуры", + "objects": ["Catalog.Номенклатура: @view", "Catalog.Контрагенты: @view", "DataProcessor.Загрузка: @view"] +} +``` + +### Роль с RLS + +```json +{ + "name": "ЧтениеДокументовПоОрганизации", + "synonym": "Чтение документов (ограничение по организации)", + "objects": [ + "Catalog.Организации: @view", + {"name": "Document.РеализацияТоваровУслуг", "preset": "view", "rls": {"Read": "#ДляОбъекта(\"\")"}} + ], + "templates": [{"name": "ДляОбъекта(Модификатор)", "condition": "ГДЕ Организация = &ТекущаяОрганизация"}] +} +``` + +Подробные таблицы пресетов, русских синонимов и дополнительные примеры — в `dsl-reference.md`. + +## Верификация + +``` +/role-validate [MetadataPath] — проверка корректности XML, прав, RLS +/role-info — визуальная сводка структуры +``` diff --git a/.claude/skills/role-compile/dsl-reference.md b/.claude/skills/role-compile/dsl-reference.md index ea352eed..0e23a589 100644 --- a/.claude/skills/role-compile/dsl-reference.md +++ b/.claude/skills/role-compile/dsl-reference.md @@ -1,313 +1,313 @@ -# Role DSL — полная справка - -Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md). - -## Структура верхнего уровня - -```json -{ - "name": "ИмяРоли", - "synonym": "Отображаемое имя роли", - "comment": "", - "setForNewObjects": false, - "setForAttributesByDefault": true, - "independentRightsOfChildObjects": false, - "objects": [ ... ], - "templates": [ ... ] -} -``` - -- `name` — программное имя роли (обязательно) -- `synonym` — отображаемое имя (по умолчанию = name) -- `comment` — комментарий (по умолчанию пусто) -- Глобальные флаги — по умолчанию `false`, `true`, `false` - -## Объекты: два формата - -Массив `objects` принимает строки (shorthand) и объекты (полная форма). - -### Строковый shorthand - -``` -"ОбъектМетаданных: @пресет" -"ОбъектМетаданных: Право1, Право2" -``` - -Примеры: -```json -"objects": [ - "Catalog.Номенклатура: @view", - "Document.Реализация: @edit", - "InformationRegister.Цены: Read, Update", - "DataProcessor.Загрузка: @view" -] -``` - -### Объектная форма (для RLS и переопределений) - -```json -{ - "name": "Document.Реализация", - "preset": "view", - "rights": { "Delete": false }, - "rls": { "Read": "#ДляОбъекта(\"\")" } -} -``` - -- `preset` — базовый набор прав (`"view"`, `"edit"`) -- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]` -- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}` - -## Пресеты — подробные таблицы - -Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`. - -### `@view` — просмотр - -| Тип объекта | Права | -|-------------|-------| -| Catalog, ExchangePlan, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task | Read, View, InputByString | -| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, Constant, DocumentJournal | Read, View | -| Sequence | Read | -| CommonForm, CommonCommand, Subsystem, FilterCriterion, CommonAttribute | View | -| DataProcessor, Report | Use, View | -| SessionParameter | Get | -| Configuration | ThinClient, WebClient, Output, SaveUserData, MainWindowModeNormal | - -### `@edit` — полное редактирование - -| Тип объекта | Права | -|-------------|-------| -| Catalog, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark | -| Document | Read, Insert, Update, Delete, View, Edit, InputByString, Posting, UndoPosting, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted | -| BusinessProcess | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveStart | -| Task | Read, Insert, Update, Delete, View, Edit, InputByString, Execute, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveExecute | -| InformationRegister, AccumulationRegister, AccountingRegister, Constant | Read, Update, View, Edit | -| DocumentJournal | Read, View | -| Sequence | Read, Update | -| SessionParameter | Get, Set | -| CommonAttribute | View, Edit | - -Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`. - -Если пресет не определён для типа объекта — предупреждение с подсказкой доступных. - -## Русские синонимы - -Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает. - -### Типы объектов - -| Русский | English | -|---------|---------| -| `Справочник` | Catalog | -| `Документ` | Document | -| `РегистрСведений` | InformationRegister | -| `РегистрНакопления` | AccumulationRegister | -| `РегистрБухгалтерии` | AccountingRegister | -| `РегистрРасчета` | CalculationRegister | -| `Константа` | Constant | -| `ПланСчетов` | ChartOfAccounts | -| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes | -| `ПланВидовРасчета` | ChartOfCalculationTypes | -| `ПланОбмена` | ExchangePlan | -| `БизнесПроцесс` | BusinessProcess | -| `Задача` | Task | -| `Обработка` | DataProcessor | -| `Отчет` | Report | -| `ОбщаяФорма` | CommonForm | -| `ОбщаяКоманда` | CommonCommand | -| `Подсистема` | Subsystem | -| `КритерийОтбора` | FilterCriterion | -| `ЖурналДокументов` | DocumentJournal | -| `Последовательность` | Sequence | -| `ВебСервис` | WebService | -| `HTTPСервис` | HTTPService | -| `СервисИнтеграции` | IntegrationService | -| `ПараметрСеанса` | SessionParameter | -| `ОбщийРеквизит` | CommonAttribute | -| `Конфигурация` | Configuration | -| `Перечисление` | Enum | - -### Вложенные типы - -| Русский | English | -|---------|---------| -| `Реквизит` | Attribute | -| `СтандартныйРеквизит` | StandardAttribute | -| `ТабличнаяЧасть` | TabularSection | -| `Измерение` | Dimension | -| `Ресурс` | Resource | -| `Команда` | Command | -| `РеквизитАдресации` | AddressingAttribute | - -### Права (основные) - -| Русский | English | -|---------|---------| -| `Чтение` | Read | -| `Добавление` | Insert | -| `Изменение` | Update | -| `Удаление` | Delete | -| `Просмотр` | View | -| `Редактирование` | Edit | -| `ВводПоСтроке` | InputByString | -| `Проведение` | Posting | -| `ОтменаПроведения` | UndoPosting | -| `Использование` | Use | -| `Получение` | Get | -| `Установка` | Set | -| `Старт` | Start | -| `Выполнение` | Execute | -| `УправлениеИтогами` | TotalsControl | - -### Права (интерактивные) - -| Русский | English | -|---------|---------| -| `ИнтерактивноеДобавление` | InteractiveInsert | -| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark | -| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark | -| `ИнтерактивноеУдаление` | InteractiveDelete | -| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked | -| `ИнтерактивноеПроведение` | InteractivePosting | -| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular | -| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting | -| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted | -| `ИнтерактивныйСтарт` | InteractiveStart | -| `ИнтерактивнаяАктивация` | InteractiveActivate | -| `ИнтерактивноеВыполнение` | InteractiveExecute | - -### Права (конфигурация) - -| Русский | English | -|---------|---------| -| `Администрирование` | Administration | -| `АдминистрированиеДанных` | DataAdministration | -| `ТонкийКлиент` | ThinClient | -| `ТолстыйКлиент` | ThickClient | -| `ВебКлиент` | WebClient | -| `МобильныйКлиент` | MobileClient | -| `ВнешнееСоединение` | ExternalConnection | -| `Вывод` | Output | -| `СохранениеДанныхПользователя` | SaveUserData | - -## Типы объектов без прав в ролях - -Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`): - -| Тип | Причина | -|-----|---------| -| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно | -| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли | -| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав | -| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав | -| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав | -| Language (Язык) | Конфигурационный элемент | -| FunctionalOption (ФункциональнаяОпция) | Не объект прав | -| FunctionalOptionsParameter | Не объект прав | -| EventSubscription (ПодпискаНаСобытие) | Не объект прав | -| ScheduledJob (РегламентноеЗадание) | Не объект прав | -| StyleItem (ЭлементСтиля) | Ресурс оформления | - -## Шаблоны ограничений (RLS templates) - -```json -"templates": [ - { - "name": "ДляОбъекта(Модификатор)", - "condition": "// текст шаблона\nГДЕ 1=1\n&Модификатор" - } -] -``` - -- `&` в условии автоматически экранируется в `&` в XML -- Ссылка на шаблон в `rls`: `"#ИмяШаблона(\"параметры\")"` — начинается с `#` -- Параметры шаблона можно передавать пустыми: `#ДляОбъекта("")` - -## Примеры - -### 1. Простая роль (только пресеты) - -```json -{ - "name": "ЧтениеНоменклатуры", - "synonym": "Чтение номенклатуры", - "objects": [ - "Catalog.Номенклатура: @view", - "Catalog.Контрагенты: @view", - "DataProcessor.Загрузка: @view" - ] -} -``` - -### 2. Роль для регламентного задания - -```json -{ - "name": "ОбновлениеЦен", - "synonym": "Обновление цен номенклатуры", - "objects": [ - "Catalog.Номенклатура: Read", - "Catalog.Валюты: Read", - "InformationRegister.ЦеныНоменклатуры: Read, Update", - "Constant.ОсновнаяВалюта: Read" - ] -} -``` - -### 3. Роль с RLS - -```json -{ - "name": "ЧтениеДокументовПоОрганизации", - "synonym": "Чтение документов (ограничение по организации)", - "objects": [ - "Catalog.Организации: @view", - { - "name": "Document.РеализацияТоваровУслуг", - "preset": "view", - "rls": { - "Read": "#ДляОбъекта(\"\")" - } - } - ], - "templates": [ - { - "name": "ДляОбъекта(Модификатор)", - "condition": "ГДЕ Организация = &ТекущаяОрганизация" - } - ] -} -``` - -### 4. Роль с русскими синонимами - -```json -{ - "name": "ПросмотрДанных", - "synonym": "Просмотр данных", - "objects": [ - "Справочник.Контрагенты: @view", - "Документ.Реализация: Чтение, Просмотр", - "РегистрСведений.Цены: @edit", - "Обработка.ЗагрузкаДанных: @view" - ] -} -``` - -### 5. Роль с переопределением прав из пресета - -```json -{ - "name": "ОграниченноеРедактирование", - "synonym": "Редактирование без удаления", - "objects": [ - { - "name": "Catalog.Контрагенты", - "preset": "edit", - "rights": { "Delete": false } - } - ] -} -``` +# Role DSL — полная справка + +Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md). + +## Структура верхнего уровня + +```json +{ + "name": "ИмяРоли", + "synonym": "Отображаемое имя роли", + "comment": "", + "setForNewObjects": false, + "setForAttributesByDefault": true, + "independentRightsOfChildObjects": false, + "objects": [ ... ], + "templates": [ ... ] +} +``` + +- `name` — программное имя роли (обязательно) +- `synonym` — отображаемое имя (по умолчанию = name) +- `comment` — комментарий (по умолчанию пусто) +- Глобальные флаги — по умолчанию `false`, `true`, `false` + +## Объекты: два формата + +Массив `objects` принимает строки (shorthand) и объекты (полная форма). + +### Строковый shorthand + +``` +"ОбъектМетаданных: @пресет" +"ОбъектМетаданных: Право1, Право2" +``` + +Примеры: +```json +"objects": [ + "Catalog.Номенклатура: @view", + "Document.Реализация: @edit", + "InformationRegister.Цены: Read, Update", + "DataProcessor.Загрузка: @view" +] +``` + +### Объектная форма (для RLS и переопределений) + +```json +{ + "name": "Document.Реализация", + "preset": "view", + "rights": { "Delete": false }, + "rls": { "Read": "#ДляОбъекта(\"\")" } +} +``` + +- `preset` — базовый набор прав (`"view"`, `"edit"`) +- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]` +- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}` + +## Пресеты — подробные таблицы + +Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`. + +### `@view` — просмотр + +| Тип объекта | Права | +|-------------|-------| +| Catalog, ExchangePlan, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task | Read, View, InputByString | +| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, Constant, DocumentJournal | Read, View | +| Sequence | Read | +| CommonForm, CommonCommand, Subsystem, FilterCriterion, CommonAttribute | View | +| DataProcessor, Report | Use, View | +| SessionParameter | Get | +| Configuration | ThinClient, WebClient, Output, SaveUserData, MainWindowModeNormal | + +### `@edit` — полное редактирование + +| Тип объекта | Права | +|-------------|-------| +| Catalog, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark | +| Document | Read, Insert, Update, Delete, View, Edit, InputByString, Posting, UndoPosting, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted | +| BusinessProcess | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveStart | +| Task | Read, Insert, Update, Delete, View, Edit, InputByString, Execute, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveExecute | +| InformationRegister, AccumulationRegister, AccountingRegister, Constant | Read, Update, View, Edit | +| DocumentJournal | Read, View | +| Sequence | Read, Update | +| SessionParameter | Get, Set | +| CommonAttribute | View, Edit | + +Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`. + +Если пресет не определён для типа объекта — предупреждение с подсказкой доступных. + +## Русские синонимы + +Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает. + +### Типы объектов + +| Русский | English | +|---------|---------| +| `Справочник` | Catalog | +| `Документ` | Document | +| `РегистрСведений` | InformationRegister | +| `РегистрНакопления` | AccumulationRegister | +| `РегистрБухгалтерии` | AccountingRegister | +| `РегистрРасчета` | CalculationRegister | +| `Константа` | Constant | +| `ПланСчетов` | ChartOfAccounts | +| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes | +| `ПланВидовРасчета` | ChartOfCalculationTypes | +| `ПланОбмена` | ExchangePlan | +| `БизнесПроцесс` | BusinessProcess | +| `Задача` | Task | +| `Обработка` | DataProcessor | +| `Отчет` | Report | +| `ОбщаяФорма` | CommonForm | +| `ОбщаяКоманда` | CommonCommand | +| `Подсистема` | Subsystem | +| `КритерийОтбора` | FilterCriterion | +| `ЖурналДокументов` | DocumentJournal | +| `Последовательность` | Sequence | +| `ВебСервис` | WebService | +| `HTTPСервис` | HTTPService | +| `СервисИнтеграции` | IntegrationService | +| `ПараметрСеанса` | SessionParameter | +| `ОбщийРеквизит` | CommonAttribute | +| `Конфигурация` | Configuration | +| `Перечисление` | Enum | + +### Вложенные типы + +| Русский | English | +|---------|---------| +| `Реквизит` | Attribute | +| `СтандартныйРеквизит` | StandardAttribute | +| `ТабличнаяЧасть` | TabularSection | +| `Измерение` | Dimension | +| `Ресурс` | Resource | +| `Команда` | Command | +| `РеквизитАдресации` | AddressingAttribute | + +### Права (основные) + +| Русский | English | +|---------|---------| +| `Чтение` | Read | +| `Добавление` | Insert | +| `Изменение` | Update | +| `Удаление` | Delete | +| `Просмотр` | View | +| `Редактирование` | Edit | +| `ВводПоСтроке` | InputByString | +| `Проведение` | Posting | +| `ОтменаПроведения` | UndoPosting | +| `Использование` | Use | +| `Получение` | Get | +| `Установка` | Set | +| `Старт` | Start | +| `Выполнение` | Execute | +| `УправлениеИтогами` | TotalsControl | + +### Права (интерактивные) + +| Русский | English | +|---------|---------| +| `ИнтерактивноеДобавление` | InteractiveInsert | +| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark | +| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark | +| `ИнтерактивноеУдаление` | InteractiveDelete | +| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked | +| `ИнтерактивноеПроведение` | InteractivePosting | +| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular | +| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting | +| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted | +| `ИнтерактивныйСтарт` | InteractiveStart | +| `ИнтерактивнаяАктивация` | InteractiveActivate | +| `ИнтерактивноеВыполнение` | InteractiveExecute | + +### Права (конфигурация) + +| Русский | English | +|---------|---------| +| `Администрирование` | Administration | +| `АдминистрированиеДанных` | DataAdministration | +| `ТонкийКлиент` | ThinClient | +| `ТолстыйКлиент` | ThickClient | +| `ВебКлиент` | WebClient | +| `МобильныйКлиент` | MobileClient | +| `ВнешнееСоединение` | ExternalConnection | +| `Вывод` | Output | +| `СохранениеДанныхПользователя` | SaveUserData | + +## Типы объектов без прав в ролях + +Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`): + +| Тип | Причина | +|-----|---------| +| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно | +| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли | +| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав | +| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав | +| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав | +| Language (Язык) | Конфигурационный элемент | +| FunctionalOption (ФункциональнаяОпция) | Не объект прав | +| FunctionalOptionsParameter | Не объект прав | +| EventSubscription (ПодпискаНаСобытие) | Не объект прав | +| ScheduledJob (РегламентноеЗадание) | Не объект прав | +| StyleItem (ЭлементСтиля) | Ресурс оформления | + +## Шаблоны ограничений (RLS templates) + +```json +"templates": [ + { + "name": "ДляОбъекта(Модификатор)", + "condition": "// текст шаблона\nГДЕ 1=1\n&Модификатор" + } +] +``` + +- `&` в условии автоматически экранируется в `&` в XML +- Ссылка на шаблон в `rls`: `"#ИмяШаблона(\"параметры\")"` — начинается с `#` +- Параметры шаблона можно передавать пустыми: `#ДляОбъекта("")` + +## Примеры + +### 1. Простая роль (только пресеты) + +```json +{ + "name": "ЧтениеНоменклатуры", + "synonym": "Чтение номенклатуры", + "objects": [ + "Catalog.Номенклатура: @view", + "Catalog.Контрагенты: @view", + "DataProcessor.Загрузка: @view" + ] +} +``` + +### 2. Роль для регламентного задания + +```json +{ + "name": "ОбновлениеЦен", + "synonym": "Обновление цен номенклатуры", + "objects": [ + "Catalog.Номенклатура: Read", + "Catalog.Валюты: Read", + "InformationRegister.ЦеныНоменклатуры: Read, Update", + "Constant.ОсновнаяВалюта: Read" + ] +} +``` + +### 3. Роль с RLS + +```json +{ + "name": "ЧтениеДокументовПоОрганизации", + "synonym": "Чтение документов (ограничение по организации)", + "objects": [ + "Catalog.Организации: @view", + { + "name": "Document.РеализацияТоваровУслуг", + "preset": "view", + "rls": { + "Read": "#ДляОбъекта(\"\")" + } + } + ], + "templates": [ + { + "name": "ДляОбъекта(Модификатор)", + "condition": "ГДЕ Организация = &ТекущаяОрганизация" + } + ] +} +``` + +### 4. Роль с русскими синонимами + +```json +{ + "name": "ПросмотрДанных", + "synonym": "Просмотр данных", + "objects": [ + "Справочник.Контрагенты: @view", + "Документ.Реализация: Чтение, Просмотр", + "РегистрСведений.Цены: @edit", + "Обработка.ЗагрузкаДанных: @view" + ] +} +``` + +### 5. Роль с переопределением прав из пресета + +```json +{ + "name": "ОграниченноеРедактирование", + "synonym": "Редактирование без удаления", + "objects": [ + { + "name": "Catalog.Контрагенты", + "preset": "edit", + "rights": { "Delete": false } + } + ] +} +``` diff --git a/.claude/skills/role-info/SKILL.md b/.claude/skills/role-info/SKILL.md index 69de2ddb..6fbed3d0 100644 --- a/.claude/skills/role-info/SKILL.md +++ b/.claude/skills/role-info/SKILL.md @@ -1,44 +1,44 @@ ---- -name: role-info -description: Компактная сводка прав роли 1С из Rights.xml — объекты, права, RLS, шаблоны ограничений. Используй для аудита прав — какие объекты и действия доступны, ограничения RLS -argument-hint: -allowed-tools: - - Bash - - Read ---- - -# /role-info — анализ роли 1С - -Парсит `Rights.xml` роли и выдаёт компактную сводку: объекты сгруппированы по типу, показаны только разрешённые права. Сжатие: тысячи строк XML → 50–150 строк текста. - -## Использование - -``` -/role-info -``` - -**RightsPath** — путь к файлу `Rights.xml` роли (обычно `Roles/ИмяРоли/Ext/Rights.xml`). - -## Запуск скрипта - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-info.ps1" -RightsPath -OutFile -``` - -### Параметры - -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| `-RightsPath` | да | Путь к Rights.xml | -| `-ShowDenied` | нет | Показать запрещённые права (по умолчанию скрыты) | -| `-Limit` | нет | Макс. строк вывода (по умолчанию `150`). `0` = без ограничений | -| `-Offset` | нет | Пропустить N строк — для пагинации (по умолчанию `0`) | -| `-OutFile` | нет | Записать результат в файл (UTF-8 BOM). Без этого — вывод в консоль | - -**Важно:** Всегда используй `-OutFile` и читай результат через Read tool. Прямой вывод в консоль через bash ломает кириллицу. - -Для большой роли при усечении вывода: -```powershell -... -Offset 150 # пагинация: пропустить первые 150 строк -``` - +--- +name: role-info +description: Компактная сводка прав роли 1С из Rights.xml — объекты, права, RLS, шаблоны ограничений. Используй для аудита прав — какие объекты и действия доступны, ограничения RLS +argument-hint: +allowed-tools: + - Bash + - Read +--- + +# /role-info — анализ роли 1С + +Парсит `Rights.xml` роли и выдаёт компактную сводку: объекты сгруппированы по типу, показаны только разрешённые права. Сжатие: тысячи строк XML → 50–150 строк текста. + +## Использование + +``` +/role-info +``` + +**RightsPath** — путь к файлу `Rights.xml` роли (обычно `Roles/ИмяРоли/Ext/Rights.xml`). + +## Запуск скрипта + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-info.ps1" -RightsPath -OutFile +``` + +### Параметры + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-RightsPath` | да | Путь к Rights.xml | +| `-ShowDenied` | нет | Показать запрещённые права (по умолчанию скрыты) | +| `-Limit` | нет | Макс. строк вывода (по умолчанию `150`). `0` = без ограничений | +| `-Offset` | нет | Пропустить N строк — для пагинации (по умолчанию `0`) | +| `-OutFile` | нет | Записать результат в файл (UTF-8 BOM). Без этого — вывод в консоль | + +**Важно:** Всегда используй `-OutFile` и читай результат через Read tool. Прямой вывод в консоль через bash ломает кириллицу. + +Для большой роли при усечении вывода: +```powershell +... -Offset 150 # пагинация: пропустить первые 150 строк +``` + diff --git a/.claude/skills/role-info/scripts/role-info.ps1 b/.claude/skills/role-info/scripts/role-info.ps1 index 46b8d324..766fa6fd 100644 --- a/.claude/skills/role-info/scripts/role-info.ps1 +++ b/.claude/skills/role-info/scripts/role-info.ps1 @@ -1,305 +1,305 @@ -# role-info v1.1 — Analyze 1C role rights -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath, - [switch]$ShowDenied, - [int]$Limit = 150, - [int]$Offset = 0, - [string]$OutFile -) - -$ErrorActionPreference = 'Stop' -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Output helper (always collect, paginate at the end) --- -$script:lines = @() -function Out([string]$text) { $script:lines += $text } - -# --- Resolve paths --- -if (-not [System.IO.Path]::IsPathRooted($RightsPath)) { - $RightsPath = Join-Path (Get-Location).Path $RightsPath -} - -if (-not (Test-Path $RightsPath)) { - Write-Host "[ERROR] File not found: $RightsPath" - exit 1 -} - -# --- Try to find metadata file for role name/synonym --- -$roleName = "" -$roleSynonym = "" -$extDir = Split-Path $RightsPath # .../Ext -$roleDir = Split-Path $extDir # .../RoleName -$rolesDir = Split-Path $roleDir # .../Roles -$roleFolderName = Split-Path $roleDir -Leaf -$metaPath = Join-Path $rolesDir "$roleFolderName.xml" - -if (Test-Path $metaPath) { - try { - [xml]$metaXml = Get-Content -Path $metaPath -Encoding UTF8 - $ns = New-Object System.Xml.XmlNamespaceManager($metaXml.NameTable) - $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - $nameNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Name", $ns) - if ($nameNode) { $roleName = $nameNode.InnerText } - $synNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - if ($synNode) { $roleSynonym = $synNode.InnerText } - } catch { - # Ignore metadata parsing errors - } -} - -if (-not $roleName) { $roleName = $roleFolderName } - -# --- Parse Rights.xml --- -[xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8 -$root = $xml.DocumentElement -$rightsNs = "http://v8.1c.ru/8.2/roles" - -# Global flags -$setForNew = $root.setForNewObjects -$setForAttrs = $root.setForAttributesByDefault -$independentChild = $root.independentRightsOfChildObjects - -# --- Collect objects --- -# Structure: grouped by type prefix, then by object short name -$allowed = [ordered]@{} # type -> [ordered]@{ shortName -> [list of rights] } -$denied = [ordered]@{} # type -> [ordered]@{ shortName -> [list of rights] } -$rlsObjects = @() -$totalAllowed = 0 -$totalDenied = 0 - -$objects = $root.GetElementsByTagName("object", $rightsNs) -foreach ($obj in $objects) { - $objName = "" - $rights = @() - - foreach ($child in $obj.ChildNodes) { - if ($child.LocalName -eq "name" -and $child.NamespaceURI -eq $rightsNs) { - $objName = $child.InnerText - } - if ($child.LocalName -eq "right" -and $child.NamespaceURI -eq $rightsNs) { - $rName = "" - $rValue = "" - $hasRLS = $false - foreach ($rc in $child.ChildNodes) { - if ($rc.LocalName -eq "name") { $rName = $rc.InnerText } - if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText } - if ($rc.LocalName -eq "restrictionByCondition") { $hasRLS = $true } - } - if ($rName -and $rValue) { - $rights += @{ name = $rName; value = $rValue; rls = $hasRLS } - } - } - } - - if (-not $objName -or $rights.Count -eq 0) { continue } - - # Split into type prefix and short name - $dotIdx = $objName.IndexOf(".") - if ($dotIdx -lt 0) { continue } - $typePrefix = $objName.Substring(0, $dotIdx) - $shortName = $objName.Substring($dotIdx + 1) - - foreach ($r in $rights) { - if ($r.value -eq "true") { - $totalAllowed++ - if (-not $allowed.Contains($typePrefix)) { - $allowed[$typePrefix] = [ordered]@{} - } - if (-not $allowed[$typePrefix].Contains($shortName)) { - $allowed[$typePrefix][$shortName] = @() - } - $suffix = $r.name - if ($r.rls) { - $suffix += " [RLS]" - $rlsObjects += "$typePrefix.$shortName ($($r.name))" - } - $allowed[$typePrefix][$shortName] += $suffix - } - else { - $totalDenied++ - if (-not $denied.Contains($typePrefix)) { - $denied[$typePrefix] = [ordered]@{} - } - if (-not $denied[$typePrefix].Contains($shortName)) { - $denied[$typePrefix][$shortName] = @() - } - $denied[$typePrefix][$shortName] += $r.name - } - } -} - -# --- Restriction templates --- -$templates = @() -$tplNodes = $root.GetElementsByTagName("restrictionTemplate", $rightsNs) -foreach ($tpl in $tplNodes) { - foreach ($child in $tpl.ChildNodes) { - if ($child.LocalName -eq "name") { - $tName = $child.InnerText - # Extract just the name part before parentheses - $parenIdx = $tName.IndexOf("(") - if ($parenIdx -gt 0) { $tName = $tName.Substring(0, $parenIdx) } - $templates += $tName - } - } -} - -function Get-SupportStatusForPath([string]$targetPath) { - try { - $rp = (Resolve-Path $targetPath).Path - $elemUuid = $null - $binPath = $null - # Reads the uuid of the first metadata element in an .xml file (or $null). - function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null - } - # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). - $elemUuid = Get-RootUuid $rp - $d = [System.IO.Path]::GetDirectoryName($rp) - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $binPath) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } - } - if ($elemUuid -and $binPath) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return "не на поддержке" } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return "снято с поддержки (правки свободны)" } - if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } - if (-not $elemUuid) { return "не на поддержке" } - $u = [regex]::Escape($elemUuid.ToLower()) - $best = $null - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - if ($null -eq $best) { return "не на поддержке" } - switch ($best) { - 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } - 1 { return "редактируется с сохранением поддержки" } - 2 { return "снято с поддержки (правки свободны)" } - } - return "не на поддержке" - } catch { return "не на поддержке" } -} - -# --- Output --- -$header = "=== Role: $roleName" -if ($roleSynonym) { $header += " --- `"$roleSynonym`"" } -$header += " ===" -Out $header -Out "Поддержка: $(Get-SupportStatusForPath $RightsPath)" -Out "" - -Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild" -Out "" - -# Helper: output group -function OutGroup($objMap, [string]$prefix, [switch]$isDenied) { - foreach ($shortName in @($objMap.Keys)) { - if ($isDenied) { - $rightsList = ($objMap[$shortName] | ForEach-Object { "-$_" }) -join ", " - } else { - $rightsList = $objMap[$shortName] -join ", " - } - Out " ${shortName}: $rightsList" - } -} - -# Allowed rights grouped by type -if ($allowed.Count -gt 0) { - Out "Allowed rights:" - Out "" - foreach ($typePrefix in $allowed.Keys) { - $objMap = $allowed[$typePrefix] - Out " $typePrefix ($($objMap.Count)):" - OutGroup $objMap $typePrefix - Out "" - } -} -else { - Out "(no allowed rights)" - Out "" -} - -# Denied rights -if ($ShowDenied -and $denied.Count -gt 0) { - Out "Denied rights:" - Out "" - foreach ($typePrefix in $denied.Keys) { - $objMap = $denied[$typePrefix] - Out " $typePrefix ($($objMap.Count)):" - OutGroup $objMap $typePrefix -isDenied - Out "" - } -} -elseif ($totalDenied -gt 0) { - Out "Denied: $totalDenied rights (use -ShowDenied to list)" - Out "" -} - -# RLS summary -if ($rlsObjects.Count -gt 0) { - Out "RLS: $($rlsObjects.Count) restrictions" -} - -# Templates -if ($templates.Count -gt 0) { - Out "Templates: $($templates -join ', ')" -} - -Out "" -Out "---" -Out "Total: $totalAllowed allowed, $totalDenied denied" - -# --- Pagination and output --- -$totalLines = $script:lines.Count -$lines = $script:lines - -if ($Offset -gt 0) { - if ($Offset -ge $totalLines) { - Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." - exit 0 - } - $lines = $lines[$Offset..($totalLines - 1)] -} - -if ($Limit -gt 0 -and $lines.Count -gt $Limit) { - $shown = $lines[0..($Limit - 1)] - $remaining = $totalLines - $Offset - $Limit - $shown += "" - $shown += "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." - $lines = $shown -} - -if ($OutFile) { - if (-not [System.IO.Path]::IsPathRooted($OutFile)) { - $OutFile = Join-Path (Get-Location).Path $OutFile - } - $utf8 = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($OutFile, $lines, $utf8) - Write-Host "Output written to $OutFile" -} else { - foreach ($l in $lines) { Write-Host $l } -} +# role-info v1.1 — Analyze 1C role rights +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath, + [switch]$ShowDenied, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper (always collect, paginate at the end) --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +# --- Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($RightsPath)) { + $RightsPath = Join-Path (Get-Location).Path $RightsPath +} + +if (-not (Test-Path $RightsPath)) { + Write-Host "[ERROR] File not found: $RightsPath" + exit 1 +} + +# --- Try to find metadata file for role name/synonym --- +$roleName = "" +$roleSynonym = "" +$extDir = Split-Path $RightsPath # .../Ext +$roleDir = Split-Path $extDir # .../RoleName +$rolesDir = Split-Path $roleDir # .../Roles +$roleFolderName = Split-Path $roleDir -Leaf +$metaPath = Join-Path $rolesDir "$roleFolderName.xml" + +if (Test-Path $metaPath) { + try { + [xml]$metaXml = Get-Content -Path $metaPath -Encoding UTF8 + $ns = New-Object System.Xml.XmlNamespaceManager($metaXml.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + $nameNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Name", $ns) + if ($nameNode) { $roleName = $nameNode.InnerText } + $synNode = $metaXml.SelectSingleNode("//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + if ($synNode) { $roleSynonym = $synNode.InnerText } + } catch { + # Ignore metadata parsing errors + } +} + +if (-not $roleName) { $roleName = $roleFolderName } + +# --- Parse Rights.xml --- +[xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8 +$root = $xml.DocumentElement +$rightsNs = "http://v8.1c.ru/8.2/roles" + +# Global flags +$setForNew = $root.setForNewObjects +$setForAttrs = $root.setForAttributesByDefault +$independentChild = $root.independentRightsOfChildObjects + +# --- Collect objects --- +# Structure: grouped by type prefix, then by object short name +$allowed = [ordered]@{} # type -> [ordered]@{ shortName -> [list of rights] } +$denied = [ordered]@{} # type -> [ordered]@{ shortName -> [list of rights] } +$rlsObjects = @() +$totalAllowed = 0 +$totalDenied = 0 + +$objects = $root.GetElementsByTagName("object", $rightsNs) +foreach ($obj in $objects) { + $objName = "" + $rights = @() + + foreach ($child in $obj.ChildNodes) { + if ($child.LocalName -eq "name" -and $child.NamespaceURI -eq $rightsNs) { + $objName = $child.InnerText + } + if ($child.LocalName -eq "right" -and $child.NamespaceURI -eq $rightsNs) { + $rName = "" + $rValue = "" + $hasRLS = $false + foreach ($rc in $child.ChildNodes) { + if ($rc.LocalName -eq "name") { $rName = $rc.InnerText } + if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText } + if ($rc.LocalName -eq "restrictionByCondition") { $hasRLS = $true } + } + if ($rName -and $rValue) { + $rights += @{ name = $rName; value = $rValue; rls = $hasRLS } + } + } + } + + if (-not $objName -or $rights.Count -eq 0) { continue } + + # Split into type prefix and short name + $dotIdx = $objName.IndexOf(".") + if ($dotIdx -lt 0) { continue } + $typePrefix = $objName.Substring(0, $dotIdx) + $shortName = $objName.Substring($dotIdx + 1) + + foreach ($r in $rights) { + if ($r.value -eq "true") { + $totalAllowed++ + if (-not $allowed.Contains($typePrefix)) { + $allowed[$typePrefix] = [ordered]@{} + } + if (-not $allowed[$typePrefix].Contains($shortName)) { + $allowed[$typePrefix][$shortName] = @() + } + $suffix = $r.name + if ($r.rls) { + $suffix += " [RLS]" + $rlsObjects += "$typePrefix.$shortName ($($r.name))" + } + $allowed[$typePrefix][$shortName] += $suffix + } + else { + $totalDenied++ + if (-not $denied.Contains($typePrefix)) { + $denied[$typePrefix] = [ordered]@{} + } + if (-not $denied[$typePrefix].Contains($shortName)) { + $denied[$typePrefix][$shortName] = @() + } + $denied[$typePrefix][$shortName] += $r.name + } + } +} + +# --- Restriction templates --- +$templates = @() +$tplNodes = $root.GetElementsByTagName("restrictionTemplate", $rightsNs) +foreach ($tpl in $tplNodes) { + foreach ($child in $tpl.ChildNodes) { + if ($child.LocalName -eq "name") { + $tName = $child.InnerText + # Extract just the name part before parentheses + $parenIdx = $tName.IndexOf("(") + if ($parenIdx -gt 0) { $tName = $tName.Substring(0, $parenIdx) } + $templates += $tName + } + } +} + +function Get-SupportStatusForPath([string]$targetPath) { + try { + $rp = (Resolve-Path $targetPath).Path + $elemUuid = $null + $binPath = $null + # Reads the uuid of the first metadata element in an .xml file (or $null). + function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null + } + # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). + $elemUuid = Get-RootUuid $rp + $d = [System.IO.Path]::GetDirectoryName($rp) + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $binPath) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } + } + if ($elemUuid -and $binPath) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $elemUuid) { return "не на поддержке" } + $u = [regex]::Escape($elemUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + +# --- Output --- +$header = "=== Role: $roleName" +if ($roleSynonym) { $header += " --- `"$roleSynonym`"" } +$header += " ===" +Out $header +Out "Поддержка: $(Get-SupportStatusForPath $RightsPath)" +Out "" + +Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild" +Out "" + +# Helper: output group +function OutGroup($objMap, [string]$prefix, [switch]$isDenied) { + foreach ($shortName in @($objMap.Keys)) { + if ($isDenied) { + $rightsList = ($objMap[$shortName] | ForEach-Object { "-$_" }) -join ", " + } else { + $rightsList = $objMap[$shortName] -join ", " + } + Out " ${shortName}: $rightsList" + } +} + +# Allowed rights grouped by type +if ($allowed.Count -gt 0) { + Out "Allowed rights:" + Out "" + foreach ($typePrefix in $allowed.Keys) { + $objMap = $allowed[$typePrefix] + Out " $typePrefix ($($objMap.Count)):" + OutGroup $objMap $typePrefix + Out "" + } +} +else { + Out "(no allowed rights)" + Out "" +} + +# Denied rights +if ($ShowDenied -and $denied.Count -gt 0) { + Out "Denied rights:" + Out "" + foreach ($typePrefix in $denied.Keys) { + $objMap = $denied[$typePrefix] + Out " $typePrefix ($($objMap.Count)):" + OutGroup $objMap $typePrefix -isDenied + Out "" + } +} +elseif ($totalDenied -gt 0) { + Out "Denied: $totalDenied rights (use -ShowDenied to list)" + Out "" +} + +# RLS summary +if ($rlsObjects.Count -gt 0) { + Out "RLS: $($rlsObjects.Count) restrictions" +} + +# Templates +if ($templates.Count -gt 0) { + Out "Templates: $($templates -join ', ')" +} + +Out "" +Out "---" +Out "Total: $totalAllowed allowed, $totalDenied denied" + +# --- Pagination and output --- +$totalLines = $script:lines.Count +$lines = $script:lines + +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $lines = $lines[$Offset..($totalLines - 1)] +} + +if ($Limit -gt 0 -and $lines.Count -gt $Limit) { + $shown = $lines[0..($Limit - 1)] + $remaining = $totalLines - $Offset - $Limit + $shown += "" + $shown += "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." + $lines = $shown +} + +if ($OutFile) { + if (-not [System.IO.Path]::IsPathRooted($OutFile)) { + $OutFile = Join-Path (Get-Location).Path $OutFile + } + $utf8 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($OutFile, $lines, $utf8) + Write-Host "Output written to $OutFile" +} else { + foreach ($l in $lines) { Write-Host $l } +} diff --git a/.claude/skills/role-validate/SKILL.md b/.claude/skills/role-validate/SKILL.md index d67ed7f1..f3dab146 100644 --- a/.claude/skills/role-validate/SKILL.md +++ b/.claude/skills/role-validate/SKILL.md @@ -1,27 +1,27 @@ ---- -name: role-validate -description: Валидация роли 1С. Используй после создания или модификации роли для проверки корректности -argument-hint: [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read ---- - -# /role-validate — валидация роли 1С - -Проверяет корректность `Rights.xml` роли: формат XML, namespace, глобальные флаги, типы объектов, имена прав, RLS-ограничения, шаблоны. Опционально проверяет метаданные роли (UUID, имя, синоним). - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|--------------|:-----:|---------|-------------------------------------------------| -| RightsPath | да | — | Путь к роли (директория или `Rights.xml`) | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Макс. ошибок до остановки (по умолчанию 30) | -| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-validate.ps1" -RightsPath "Roles/МояРоль" -``` +--- +name: role-validate +description: Валидация роли 1С. Используй после создания или модификации роли для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read +--- + +# /role-validate — валидация роли 1С + +Проверяет корректность `Rights.xml` роли: формат XML, namespace, глобальные флаги, типы объектов, имена прав, RLS-ограничения, шаблоны. Опционально проверяет метаданные роли (UUID, имя, синоним). + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|--------------|:-----:|---------|-------------------------------------------------| +| RightsPath | да | — | Путь к роли (директория или `Rights.xml`) | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Макс. ошибок до остановки (по умолчанию 30) | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-validate.ps1" -RightsPath "Roles/МояРоль" +``` diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1 index e2c639ca..98ca95c9 100644 --- a/.claude/skills/role-validate/scripts/role-validate.ps1 +++ b/.claude/skills/role-validate/scripts/role-validate.ps1 @@ -1,510 +1,510 @@ -# role-validate v1.1 — Validate 1C role structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$RightsPath, - - [string]$OutFile, - - [switch]$Detailed, - - [int]$MaxErrors = 30 -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- 1. Known rights per object type --- - -$script:knownRights = @{ - "Configuration" = @( - "Administration","DataAdministration","UpdateDataBaseConfiguration", - "ConfigurationExtensionsAdministration","ActiveUsers","EventLog","ExclusiveMode", - "ThinClient","ThickClient","WebClient","MobileClient","ExternalConnection", - "Automation","Output","SaveUserData","TechnicalSpecialistMode", - "InteractiveOpenExtDataProcessors","InteractiveOpenExtReports", - "AnalyticsSystemClient","CollaborationSystemInfoBaseRegistration", - "MainWindowModeNormal","MainWindowModeWorkplace", - "MainWindowModeEmbeddedWorkplace","MainWindowModeFullscreenWorkplace","MainWindowModeKiosk" - ) - "Catalog" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveDeleteMarked", - "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", - "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "Document" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "Posting","UndoPosting", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveDeleteMarked", - "InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting", - "InteractiveChangeOfPosted", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "InformationRegister" = @( - "Read","Update","View","Edit","TotalsControl", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "AccumulationRegister" = @("Read","Update","View","Edit","TotalsControl") - "AccountingRegister" = @("Read","Update","View","Edit","TotalsControl") - "CalculationRegister" = @("Read","View") - "Constant" = @( - "Read","Update","View","Edit", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "ChartOfAccounts" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete", - "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", - "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", - "ReadDataHistory","ReadDataHistoryOfMissingData", - "UpdateDataHistory","UpdateDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment" - ) - "ChartOfCharacteristicTypes" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveDeleteMarked", - "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", - "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "ChartOfCalculationTypes" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete", - "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", - "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData" - ) - "ExchangePlan" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveDeleteMarked", - "ReadDataHistory","ViewDataHistory","UpdateDataHistory", - "ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData", - "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", - "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" - ) - "BusinessProcess" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveActivate","InteractiveStart" - ) - "Task" = @( - "Read","Insert","Update","Delete","View","Edit","InputByString", - "Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", - "InteractiveDelete","InteractiveActivate","InteractiveExecute" - ) - "DataProcessor" = @("Use","View") - "Report" = @("Use","View") - "CommonForm" = @("View") - "CommonCommand" = @("View") - "Subsystem" = @("View") - "FilterCriterion" = @("View") - "DocumentJournal" = @("Read","View") - "Sequence" = @("Read","Update") - "WebService" = @("Use") - "HTTPService" = @("Use") - "IntegrationService" = @("Use") - "SessionParameter" = @("Get","Set") - "CommonAttribute" = @("View","Edit") -} - -$script:nestedRights = @("View","Edit") -$script:channelRights = @("Use") -$script:commandRights = @("View") - -# --- 2. Output helpers --- - -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -function Get-ObjectType { - param([string]$name) - $dotIdx = $name.IndexOf(".") - if ($dotIdx -lt 0) { return $name } - return $name.Substring(0, $dotIdx) -} - -function Is-NestedObject { - param([string]$name) - return ($name.Split(".").Count -ge 3) -} - -function Find-Similar { - param([string]$needle, [string[]]$haystack) - $result = @($haystack | Where-Object { - $_ -like "*$needle*" -or $needle -like "*$_*" - }) - if ($result.Count -gt 3) { $result = $result[0..2] } - return $result -} - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($RightsPath)) { - $RightsPath = Join-Path (Get-Location).Path $RightsPath -} -# A: Directory → Ext/Rights.xml -if (Test-Path $RightsPath -PathType Container) { - $RightsPath = Join-Path (Join-Path $RightsPath "Ext") "Rights.xml" -} -# B1: Missing Ext/ (e.g. Roles/МояРоль/Rights.xml → Roles/МояРоль/Ext/Rights.xml) -if (-not (Test-Path $RightsPath)) { - $fn = [System.IO.Path]::GetFileName($RightsPath) - if ($fn -eq "Rights.xml") { - $c = Join-Path (Join-Path (Split-Path $RightsPath) "Ext") $fn - if (Test-Path $c) { $RightsPath = $c } - } -} - -# --- 3. Validate Rights.xml --- - -if (-not (Test-Path $RightsPath)) { - Report-Error "File not found: $RightsPath" - $result = $script:output.ToString() - Write-Host $result - if ($OutFile) { - $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } - $outDir = [System.IO.Path]::GetDirectoryName($outPath) - if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) - Write-Host "Written to: $outPath" - } - exit 1 -} - -# Auto-detect metadata: Roles/Name/Ext/Rights.xml → Roles/Name.xml -$resolvedRights = (Resolve-Path $RightsPath).Path -$extDir = Split-Path $resolvedRights -Parent -$roleDir = Split-Path $extDir -Parent -$rolesDir = Split-Path $roleDir -Parent -$roleDirName = Split-Path $roleDir -Leaf -$MetadataPath = Join-Path $rolesDir "$roleDirName.xml" - -# 3a. Parse XML -try { - [xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8 - Report-OK "XML well-formed" -} catch { - Report-Error "XML parse error: $($_.Exception.Message)" - $result = $script:output.ToString() - Write-Host $result - if ($OutFile) { - $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } - $outDir = [System.IO.Path]::GetDirectoryName($outPath) - if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) - Write-Host "Written to: $outPath" - } - exit 1 -} - -$root = $xml.DocumentElement -$rightsNs = "http://v8.1c.ru/8.2/roles" - -# 3b. Check root element -if ($root.LocalName -ne "Rights") { - Report-Error "Root element is '$($root.LocalName)', expected 'Rights'" -} elseif ($root.NamespaceURI -ne $rightsNs) { - Report-Warn "Namespace is '$($root.NamespaceURI)', expected '$rightsNs'" -} else { - Report-OK "Root element: with correct namespace" -} - -# 3c. Global flags -$flagNames = @("setForNewObjects","setForAttributesByDefault","independentRightsOfChildObjects") -$flagsFound = 0 -foreach ($fn in $flagNames) { - $node = $root.GetElementsByTagName($fn, $rightsNs) - if ($node.Count -gt 0) { - $val = $node[0].InnerText - if ($val -ne "true" -and $val -ne "false") { - Report-Warn "$fn = '$val' (expected 'true' or 'false')" - } - $flagsFound++ - } else { - Report-Warn "Missing global flag: $fn" - } -} -if ($flagsFound -eq 3) { - Report-OK "3 global flags present" -} - -# 3d. Objects -$objects = $root.GetElementsByTagName("object", $rightsNs) -$objCount = $objects.Count -$rightCount = 0 -$rlsCount = 0 - -foreach ($obj in $objects) { - $objName = "" - foreach ($child in $obj.ChildNodes) { - if ($child.LocalName -eq "name") { - $objName = $child.InnerText - break - } - } - - if (-not $objName) { - Report-Error "Object without " - continue - } - - $objectType = Get-ObjectType $objName - $isNested = Is-NestedObject $objName - - # Check object type is known - if (-not $isNested -and -not $script:knownRights.ContainsKey($objectType)) { - Report-Warn "${objName}: unknown object type '$objectType'" - } - - # Check rights - foreach ($child in $obj.ChildNodes) { - if ($child.LocalName -ne "right") { continue } - - $rName = "" - $rValue = "" - $hasRLS = $false - - foreach ($rc in $child.ChildNodes) { - if ($rc.LocalName -eq "name") { $rName = $rc.InnerText } - if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText } - if ($rc.LocalName -eq "restrictionByCondition") { - $hasRLS = $true - $rlsCount++ - # Check condition not empty - $condNode = $null - foreach ($rcc in $rc.ChildNodes) { - if ($rcc.LocalName -eq "condition") { $condNode = $rcc } - } - if (-not $condNode -or -not $condNode.InnerText) { - Report-Warn "${objName}: RLS condition for '$rName' is empty" - } - } - } - - if (-not $rName) { - Report-Error "${objName}: without " - continue - } - - if ($rValue -ne "true" -and $rValue -ne "false") { - Report-Error "${objName}: right '$rName' has invalid value '$rValue'" - continue - } - - $rightCount++ - - # Validate right name - if ($isNested) { - if ($objName -match '\.Command\.') { - if ($rName -notin $script:commandRights) { - Report-Warn "${objName}: '$rName' not valid for commands (only: View)" - } - } elseif ($objName -match '\.IntegrationServiceChannel\.') { - if ($rName -notin $script:channelRights) { - Report-Warn "${objName}: '$rName' not valid for channels (only: Use)" - } - } else { - if ($rName -notin $script:nestedRights) { - Report-Warn "${objName}: '$rName' not valid for nested objects (only: View, Edit)" - } - } - } elseif ($script:knownRights.ContainsKey($objectType)) { - $validRights = $script:knownRights[$objectType] - if ($rName -notin $validRights) { - $similar = Find-Similar -needle $rName -haystack $validRights - $sugStr = if ($similar.Count -gt 0) { " Did you mean: $($similar -join ', ')?" } else { "" } - Report-Warn "${objName}: unknown right '$rName'.$sugStr" - } - } - } -} - -Report-OK "$objCount objects, $rightCount rights" -if ($rlsCount -gt 0) { - Report-OK "$rlsCount RLS restrictions" -} - -# 3e. Templates -$templates = $root.GetElementsByTagName("restrictionTemplate", $rightsNs) -if ($templates.Count -gt 0) { - $tplNames = @() - foreach ($tpl in $templates) { - $tName = "" - $tCond = "" - foreach ($child in $tpl.ChildNodes) { - if ($child.LocalName -eq "name") { $tName = $child.InnerText } - if ($child.LocalName -eq "condition") { $tCond = $child.InnerText } - } - if (-not $tName) { - Report-Warn "Restriction template without " - } else { - $parenIdx = $tName.IndexOf("(") - $shortName = if ($parenIdx -gt 0) { $tName.Substring(0, $parenIdx) } else { $tName } - $tplNames += $shortName - } - if (-not $tCond) { - Report-Warn "Template '$tName': empty " - } - } - Report-OK "$($templates.Count) templates: $($tplNames -join ', ')" -} - -# --- 4. Validate metadata --- - -if (Test-Path $MetadataPath) { - Out-Line "" - try { - [xml]$metaXml = Get-Content -Path $MetadataPath -Encoding UTF8 - $roleNode = $metaXml.DocumentElement.SelectSingleNode("//*[local-name()='Role']") - if (-not $roleNode) { - Report-Error "Metadata: element not found" - } else { - $uuid = $roleNode.GetAttribute("uuid") - if ($uuid -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { - Report-OK "Metadata: UUID valid ($uuid)" - } else { - Report-Error "Metadata: invalid UUID format '$uuid'" - } - - $nameNode = $roleNode.SelectSingleNode(".//*[local-name()='Name']") - if ($nameNode -and $nameNode.InnerText) { - Report-OK "Metadata: Name = $($nameNode.InnerText)" - } else { - Report-Error "Metadata: is empty or missing" - } - - $synNode = $roleNode.SelectSingleNode(".//*[local-name()='Synonym']") - if ($synNode -and $synNode.InnerXml) { - Report-OK "Metadata: Synonym present" - } else { - Report-Warn "Metadata: is empty" - } - } - } catch { - Report-Error "Metadata XML parse error: $($_.Exception.Message)" - } -} - -# --- 5. Check registration in Configuration.xml --- - -$configDir = Split-Path $rolesDir -Parent -$configXmlPath = Join-Path $configDir "Configuration.xml" -$inferredRoleName = $roleDirName - -# Use metadata name if available -if (Test-Path $MetadataPath) { - try { - [xml]$metaXml2 = Get-Content -Path $MetadataPath -Encoding UTF8 - $nameNode2 = $metaXml2.DocumentElement.SelectSingleNode("//*[local-name()='Role']//*[local-name()='Name']") - if ($nameNode2 -and $nameNode2.InnerText) { - $inferredRoleName = $nameNode2.InnerText - } - } catch { } -} - -if (Test-Path $configXmlPath) { - Out-Line "" - try { - [xml]$cfgXml = Get-Content -Path $configXmlPath -Encoding UTF8 - $cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgXml.NameTable) - $cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $childObj = $cfgXml.SelectSingleNode("//md:Configuration/md:ChildObjects", $cfgNs) - if ($childObj) { - $roleNodes = $childObj.SelectNodes("md:Role", $cfgNs) - $found = $false - foreach ($rn in $roleNodes) { - if ($rn.InnerText -eq $inferredRoleName) { - $found = $true - break - } - } - if ($found) { - Report-OK "Configuration.xml: $inferredRoleName registered" - } else { - Report-Warn "Configuration.xml: $inferredRoleName NOT found in ChildObjects" - } - } - } catch { - Report-Warn "Configuration.xml: parse error — $($_.Exception.Message)" - } -} - -# --- 6. Summary --- - -# Insert header -$script:output.Insert(0, "=== Validation: Role.$inferredRoleName ===$([Environment]::NewLine)") | Out-Null - -$checks = $script:okCount + $script:errors + $script:warnings -if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: Role.$inferredRoleName ($checks checks) ===" -} else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() -} -Write-Host $result - -if ($OutFile) { - $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } - $outDir = [System.IO.Path]::GetDirectoryName($outPath) - if (-not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null - } - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) - Write-Host "Written to: $outPath" -} - -if ($script:errors -gt 0) { exit 1 } else { exit 0 } +# role-validate v1.1 — Validate 1C role structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$RightsPath, + + [string]$OutFile, + + [switch]$Detailed, + + [int]$MaxErrors = 30 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Known rights per object type --- + +$script:knownRights = @{ + "Configuration" = @( + "Administration","DataAdministration","UpdateDataBaseConfiguration", + "ConfigurationExtensionsAdministration","ActiveUsers","EventLog","ExclusiveMode", + "ThinClient","ThickClient","WebClient","MobileClient","ExternalConnection", + "Automation","Output","SaveUserData","TechnicalSpecialistMode", + "InteractiveOpenExtDataProcessors","InteractiveOpenExtReports", + "AnalyticsSystemClient","CollaborationSystemInfoBaseRegistration", + "MainWindowModeNormal","MainWindowModeWorkplace", + "MainWindowModeEmbeddedWorkplace","MainWindowModeFullscreenWorkplace","MainWindowModeKiosk" + ) + "Catalog" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveDeleteMarked", + "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", + "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "Document" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "Posting","UndoPosting", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveDeleteMarked", + "InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting", + "InteractiveChangeOfPosted", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "InformationRegister" = @( + "Read","Update","View","Edit","TotalsControl", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "AccumulationRegister" = @("Read","Update","View","Edit","TotalsControl") + "AccountingRegister" = @("Read","Update","View","Edit","TotalsControl") + "CalculationRegister" = @("Read","View") + "Constant" = @( + "Read","Update","View","Edit", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "ChartOfAccounts" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete", + "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", + "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", + "ReadDataHistory","ReadDataHistoryOfMissingData", + "UpdateDataHistory","UpdateDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment" + ) + "ChartOfCharacteristicTypes" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveDeleteMarked", + "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", + "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "ChartOfCalculationTypes" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete", + "InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData", + "InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData" + ) + "ExchangePlan" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveDeleteMarked", + "ReadDataHistory","ViewDataHistory","UpdateDataHistory", + "ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData", + "UpdateDataHistorySettings","UpdateDataHistoryVersionComment", + "EditDataHistoryVersionComment","SwitchToDataHistoryVersion" + ) + "BusinessProcess" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveActivate","InteractiveStart" + ) + "Task" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark", + "InteractiveDelete","InteractiveActivate","InteractiveExecute" + ) + "DataProcessor" = @("Use","View") + "Report" = @("Use","View") + "CommonForm" = @("View") + "CommonCommand" = @("View") + "Subsystem" = @("View") + "FilterCriterion" = @("View") + "DocumentJournal" = @("Read","View") + "Sequence" = @("Read","Update") + "WebService" = @("Use") + "HTTPService" = @("Use") + "IntegrationService" = @("Use") + "SessionParameter" = @("Get","Set") + "CommonAttribute" = @("View","Edit") +} + +$script:nestedRights = @("View","Edit") +$script:channelRights = @("Use") +$script:commandRights = @("View") + +# --- 2. Output helpers --- + +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +function Get-ObjectType { + param([string]$name) + $dotIdx = $name.IndexOf(".") + if ($dotIdx -lt 0) { return $name } + return $name.Substring(0, $dotIdx) +} + +function Is-NestedObject { + param([string]$name) + return ($name.Split(".").Count -ge 3) +} + +function Find-Similar { + param([string]$needle, [string[]]$haystack) + $result = @($haystack | Where-Object { + $_ -like "*$needle*" -or $needle -like "*$_*" + }) + if ($result.Count -gt 3) { $result = $result[0..2] } + return $result +} + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($RightsPath)) { + $RightsPath = Join-Path (Get-Location).Path $RightsPath +} +# A: Directory → Ext/Rights.xml +if (Test-Path $RightsPath -PathType Container) { + $RightsPath = Join-Path (Join-Path $RightsPath "Ext") "Rights.xml" +} +# B1: Missing Ext/ (e.g. Roles/МояРоль/Rights.xml → Roles/МояРоль/Ext/Rights.xml) +if (-not (Test-Path $RightsPath)) { + $fn = [System.IO.Path]::GetFileName($RightsPath) + if ($fn -eq "Rights.xml") { + $c = Join-Path (Join-Path (Split-Path $RightsPath) "Ext") $fn + if (Test-Path $c) { $RightsPath = $c } + } +} + +# --- 3. Validate Rights.xml --- + +if (-not (Test-Path $RightsPath)) { + Report-Error "File not found: $RightsPath" + $result = $script:output.ToString() + Write-Host $result + if ($OutFile) { + $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } + $outDir = [System.IO.Path]::GetDirectoryName($outPath) + if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) + Write-Host "Written to: $outPath" + } + exit 1 +} + +# Auto-detect metadata: Roles/Name/Ext/Rights.xml → Roles/Name.xml +$resolvedRights = (Resolve-Path $RightsPath).Path +$extDir = Split-Path $resolvedRights -Parent +$roleDir = Split-Path $extDir -Parent +$rolesDir = Split-Path $roleDir -Parent +$roleDirName = Split-Path $roleDir -Leaf +$MetadataPath = Join-Path $rolesDir "$roleDirName.xml" + +# 3a. Parse XML +try { + [xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8 + Report-OK "XML well-formed" +} catch { + Report-Error "XML parse error: $($_.Exception.Message)" + $result = $script:output.ToString() + Write-Host $result + if ($OutFile) { + $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } + $outDir = [System.IO.Path]::GetDirectoryName($outPath) + if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) + Write-Host "Written to: $outPath" + } + exit 1 +} + +$root = $xml.DocumentElement +$rightsNs = "http://v8.1c.ru/8.2/roles" + +# 3b. Check root element +if ($root.LocalName -ne "Rights") { + Report-Error "Root element is '$($root.LocalName)', expected 'Rights'" +} elseif ($root.NamespaceURI -ne $rightsNs) { + Report-Warn "Namespace is '$($root.NamespaceURI)', expected '$rightsNs'" +} else { + Report-OK "Root element: with correct namespace" +} + +# 3c. Global flags +$flagNames = @("setForNewObjects","setForAttributesByDefault","independentRightsOfChildObjects") +$flagsFound = 0 +foreach ($fn in $flagNames) { + $node = $root.GetElementsByTagName($fn, $rightsNs) + if ($node.Count -gt 0) { + $val = $node[0].InnerText + if ($val -ne "true" -and $val -ne "false") { + Report-Warn "$fn = '$val' (expected 'true' or 'false')" + } + $flagsFound++ + } else { + Report-Warn "Missing global flag: $fn" + } +} +if ($flagsFound -eq 3) { + Report-OK "3 global flags present" +} + +# 3d. Objects +$objects = $root.GetElementsByTagName("object", $rightsNs) +$objCount = $objects.Count +$rightCount = 0 +$rlsCount = 0 + +foreach ($obj in $objects) { + $objName = "" + foreach ($child in $obj.ChildNodes) { + if ($child.LocalName -eq "name") { + $objName = $child.InnerText + break + } + } + + if (-not $objName) { + Report-Error "Object without " + continue + } + + $objectType = Get-ObjectType $objName + $isNested = Is-NestedObject $objName + + # Check object type is known + if (-not $isNested -and -not $script:knownRights.ContainsKey($objectType)) { + Report-Warn "${objName}: unknown object type '$objectType'" + } + + # Check rights + foreach ($child in $obj.ChildNodes) { + if ($child.LocalName -ne "right") { continue } + + $rName = "" + $rValue = "" + $hasRLS = $false + + foreach ($rc in $child.ChildNodes) { + if ($rc.LocalName -eq "name") { $rName = $rc.InnerText } + if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText } + if ($rc.LocalName -eq "restrictionByCondition") { + $hasRLS = $true + $rlsCount++ + # Check condition not empty + $condNode = $null + foreach ($rcc in $rc.ChildNodes) { + if ($rcc.LocalName -eq "condition") { $condNode = $rcc } + } + if (-not $condNode -or -not $condNode.InnerText) { + Report-Warn "${objName}: RLS condition for '$rName' is empty" + } + } + } + + if (-not $rName) { + Report-Error "${objName}: without " + continue + } + + if ($rValue -ne "true" -and $rValue -ne "false") { + Report-Error "${objName}: right '$rName' has invalid value '$rValue'" + continue + } + + $rightCount++ + + # Validate right name + if ($isNested) { + if ($objName -match '\.Command\.') { + if ($rName -notin $script:commandRights) { + Report-Warn "${objName}: '$rName' not valid for commands (only: View)" + } + } elseif ($objName -match '\.IntegrationServiceChannel\.') { + if ($rName -notin $script:channelRights) { + Report-Warn "${objName}: '$rName' not valid for channels (only: Use)" + } + } else { + if ($rName -notin $script:nestedRights) { + Report-Warn "${objName}: '$rName' not valid for nested objects (only: View, Edit)" + } + } + } elseif ($script:knownRights.ContainsKey($objectType)) { + $validRights = $script:knownRights[$objectType] + if ($rName -notin $validRights) { + $similar = Find-Similar -needle $rName -haystack $validRights + $sugStr = if ($similar.Count -gt 0) { " Did you mean: $($similar -join ', ')?" } else { "" } + Report-Warn "${objName}: unknown right '$rName'.$sugStr" + } + } + } +} + +Report-OK "$objCount objects, $rightCount rights" +if ($rlsCount -gt 0) { + Report-OK "$rlsCount RLS restrictions" +} + +# 3e. Templates +$templates = $root.GetElementsByTagName("restrictionTemplate", $rightsNs) +if ($templates.Count -gt 0) { + $tplNames = @() + foreach ($tpl in $templates) { + $tName = "" + $tCond = "" + foreach ($child in $tpl.ChildNodes) { + if ($child.LocalName -eq "name") { $tName = $child.InnerText } + if ($child.LocalName -eq "condition") { $tCond = $child.InnerText } + } + if (-not $tName) { + Report-Warn "Restriction template without " + } else { + $parenIdx = $tName.IndexOf("(") + $shortName = if ($parenIdx -gt 0) { $tName.Substring(0, $parenIdx) } else { $tName } + $tplNames += $shortName + } + if (-not $tCond) { + Report-Warn "Template '$tName': empty " + } + } + Report-OK "$($templates.Count) templates: $($tplNames -join ', ')" +} + +# --- 4. Validate metadata --- + +if (Test-Path $MetadataPath) { + Out-Line "" + try { + [xml]$metaXml = Get-Content -Path $MetadataPath -Encoding UTF8 + $roleNode = $metaXml.DocumentElement.SelectSingleNode("//*[local-name()='Role']") + if (-not $roleNode) { + Report-Error "Metadata: element not found" + } else { + $uuid = $roleNode.GetAttribute("uuid") + if ($uuid -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { + Report-OK "Metadata: UUID valid ($uuid)" + } else { + Report-Error "Metadata: invalid UUID format '$uuid'" + } + + $nameNode = $roleNode.SelectSingleNode(".//*[local-name()='Name']") + if ($nameNode -and $nameNode.InnerText) { + Report-OK "Metadata: Name = $($nameNode.InnerText)" + } else { + Report-Error "Metadata: is empty or missing" + } + + $synNode = $roleNode.SelectSingleNode(".//*[local-name()='Synonym']") + if ($synNode -and $synNode.InnerXml) { + Report-OK "Metadata: Synonym present" + } else { + Report-Warn "Metadata: is empty" + } + } + } catch { + Report-Error "Metadata XML parse error: $($_.Exception.Message)" + } +} + +# --- 5. Check registration in Configuration.xml --- + +$configDir = Split-Path $rolesDir -Parent +$configXmlPath = Join-Path $configDir "Configuration.xml" +$inferredRoleName = $roleDirName + +# Use metadata name if available +if (Test-Path $MetadataPath) { + try { + [xml]$metaXml2 = Get-Content -Path $MetadataPath -Encoding UTF8 + $nameNode2 = $metaXml2.DocumentElement.SelectSingleNode("//*[local-name()='Role']//*[local-name()='Name']") + if ($nameNode2 -and $nameNode2.InnerText) { + $inferredRoleName = $nameNode2.InnerText + } + } catch { } +} + +if (Test-Path $configXmlPath) { + Out-Line "" + try { + [xml]$cfgXml = Get-Content -Path $configXmlPath -Encoding UTF8 + $cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgXml.NameTable) + $cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $childObj = $cfgXml.SelectSingleNode("//md:Configuration/md:ChildObjects", $cfgNs) + if ($childObj) { + $roleNodes = $childObj.SelectNodes("md:Role", $cfgNs) + $found = $false + foreach ($rn in $roleNodes) { + if ($rn.InnerText -eq $inferredRoleName) { + $found = $true + break + } + } + if ($found) { + Report-OK "Configuration.xml: $inferredRoleName registered" + } else { + Report-Warn "Configuration.xml: $inferredRoleName NOT found in ChildObjects" + } + } + } catch { + Report-Warn "Configuration.xml: parse error — $($_.Exception.Message)" + } +} + +# --- 6. Summary --- + +# Insert header +$script:output.Insert(0, "=== Validation: Role.$inferredRoleName ===$([Environment]::NewLine)") | Out-Null + +$checks = $script:okCount + $script:errors + $script:warnings +if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Role.$inferredRoleName ($checks checks) ===" +} else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() +} +Write-Host $result + +if ($OutFile) { + $outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile } + $outDir = [System.IO.Path]::GetDirectoryName($outPath) + if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + } + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($outPath, $result, $utf8Bom) + Write-Host "Written to: $outPath" +} + +if ($script:errors -gt 0) { exit 1 } else { exit 0 } diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index aefdc940..2945214c 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -1,436 +1,436 @@ ---- -name: skd-compile -description: Компиляция схемы компоновки данных 1С (СКД) из компактного JSON-определения. Используй когда нужно создать СКД с нуля -argument-hint: "[-DefinitionFile | -Value ] -OutputPath " -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /skd-compile — генерация СКД из JSON DSL - -Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema). - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `DefinitionFile` | Путь к JSON-файлу с определением СКД (взаимоисключающий с Value) | -| `Value` | JSON-строка с определением СКД (взаимоисключающий с DefinitionFile) | -| `OutputPath` | Путь к выходному Template.xml | - -```powershell -# Из файла -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -DefinitionFile "" -OutputPath "" - -# Из строки (без промежуточного файла) -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -Value '' -OutputPath "" -``` - -## JSON DSL — краткий справочник - -Справочник ниже. Все примеры компилируемы как есть. - -### Корневая структура - -```json -{ - "dataSets": [...], - "calculatedFields": [...], - "totalFields": [...], - "parameters": [...], - "templates": [...], - "groupTemplates": [...], - "dataSetLinks": [...], - "settingsVariants": [...] -} -``` - -Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями. - -### Наборы данных - -Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion. - -```json -{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] } -``` - -Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD. - -**DataSetObject** — внешний набор данных (без источника-запроса). Поля описываются явно; данные передаются вторым параметром `ПроцессорКомпоновкиДанных.Инициализировать(Макет, Новый Структура("", ТЗ), ...)`. - -```json -{ "name": "ЖурналОшибок", "objectName": "ЖурналОшибок", "fields": [ - { "field": "ТекстСообщения", "title": "Текст сообщения", "type": "string(150)" }, - { "field": "Расшифровка", "title": "Описание", "type": "CatalogRef.СтруктураПредприятия" } -]} -``` - -`name` — имя набора в схеме, `objectName` — ключ в структуре передачи данных. - -### Поля — shorthand и объектная форма - -``` -"Наименование" — просто имя -"Количество: decimal(15,2)" — имя + тип -"Организация: CatalogRef.Организации @dimension" — + роль -"Служебное: string #noFilter #noOrder" — + ограничения -``` - -Объектная форма — когда нужен title или другие свойства: -```json -{ "field": "ОстатокНаНачалоПериода", "title": "Остаток на начало периода" } -``` -`dataPath` автоматически берётся из `field`, если не указан явно. - -Многоязычный заголовок: `"title": { "ru": "...", "en": "..." }`. Применимо везде, где принимается title/presentation (поля, calculatedFields, parameters, settingsVariants, availableValues и пр.). Строка эквивалентна `{ "ru": "..." }`. - -Типы: `string`, `string(N)`, `decimal`, `decimal(D)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией. - -`decimal` без скобок = `10,2` (деньги по умолчанию), `decimal(N)` = `N,0` (целое); `,nonneg` в конце скобок → AllowedSign=Nonnegative. - -Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу. - -Роли (shorthand или объект): - -- `@`-флаги: `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues` -- KV: `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType` - -``` -"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance" -``` - -Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. - -В объектной форме: `"useRestriction": { "field": true, "condition": true, "group": true, "order": true }` или `"restrict": ["noField", "noFilter"]`. - -Дополнительные ключи объектной формы: -- `"presentationExpression": "<выражение>"` — что показывать вместо значения поля. Исходное значение остаётся «под капотом» для перехода/расшифровки. -- `"appearance": { "<параметр>": "<значение>" }` — оформление колонки по умолчанию (применяется во всех вариантах настроек). Ключи — параметры платформы (`ГоризонтальноеПоложение`, `МинимальнаяШирина`, `Формат`, `Текст` и т.п.). -- `"orderExpression": { "expression": "<выражение>", "orderType": "Asc"/"Desc", "autoOrder": true/false }` — сортировка поля по выражению (например `ЕстьNULL(Поле.Порядок, 10000)`). - -```json -{ "field": "Сумма", "title": "Сумма продажи", "type": "decimal(15,2)", - "appearance": { "ГоризонтальноеПоложение": "Right", "МинимальнаяШирина": "80" } } -``` - -### Вычисляемые поля (calculatedFields) - -Shorthand: `"Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder"` — все части кроме имени опциональны. - -```json -"calculatedFields": [ - "Маржа = Цена - Закупка", - "Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100", - "Служебное: string = \"\" #noField #noFilter #noGroup #noOrder" -] -``` - -Объектная форма — когда нужна `appearance`: -```json -{ "name": "Маржа", "title": "Маржа", "expression": "Цена - Закупка", "type": "decimal(15,2)", "useRestriction": "#noField #noFilter" } -``` - -### Итоги (shorthand) - -```json -"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] -``` - -### Параметры (shorthand + @autoDates) - -```json -"parameters": [ - "Период [Отчетный период]: StandardPeriod = LastMonth @autoDates" -] -``` - -Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `` (LocalStringType). - -Флаги shorthand: -- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить. -- `@valueList` — `<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений (при значении-списке ниже подразумевается автоматически) -- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"` - -Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`. - -Значение-список: несколько значений по умолчанию через запятую в `значение` (для запятой внутри значения — кавычки `'...'`). В объектной форме — массив в `value`. - -```json -"parameters": [ - "Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" -] -``` - -Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме. - -Список допустимых значений (availableValues): - -```json -{ - "name": "ПорядокОкругления", - "type": "EnumRef.Округления", - "value": "Перечисление.Округления.Окр1_00", - "use": "Always", - "denyIncompleteValues": true, - "availableValues": [ - {"value": "Перечисление.Округления.Окр1_00", "presentation": "руб. коп"}, - {"value": "Перечисление.Округления.Окр1", "presentation": "руб."}, - {"value": "Перечисление.Округления.Окр1000", "presentation": "тыс. руб"} - ] -} -``` - -В варианте настроек `"dataParameters": "auto"` выводит все не-hidden параметры с `userSettingID`. Значения по умолчанию наследуются и остаются активными; параметры без значения по умолчанию отключаются (пользователь включит их в настройках). - -### Фильтры — shorthand - -```json -"filter": [ - "Организация = _ @off @user", - "Дата >= 2024-01-01T00:00:00", - "Статус filled" -] -``` - -Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`. - -В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`. - -Группы фильтров (Or/And/Not): -```json -{ "group": "Or", "items": [ - { "group": "And", "items": [ - { "field": "Статус", "op": "=", "value": "Активен" }, - { "field": "Сумма", "op": ">", "value": 1000 } - ]}, - { "field": "Количество", "op": "filled" } -]} -``` - -### Параметры данных — shorthand - -```json -"dataParameters": [ - "Период = LastMonth @user", - "Организация @off @user" -] -``` - -Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. - -### Структура — string shorthand - -```json -"structure": "Организация > details" -"structure": "Организация > Номенклатура > details" -``` - -`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне. - -Объектная форма — для сложных случаев (именованные группировки, selection/filter на уровне группировки, таблицы, диаграммы): - -```json -"structure": [ - { - "name": "ПоОрганизациям", - "groupFields": ["Организация"], - "selection": ["Организация", "Сумма", "Auto"], - "children": [{ "groupFields": [] }] - } -] -``` - -`type` по умолчанию `"group"` (можно не указывать). `groupFields` — алиас для `groupBy`. Поддержка `name`, `selection`, `order`, `filter`, `outputParameters`, рекурсивных `children`. - -### Варианты настроек - -```json -"settingsVariants": [{ - "name": "Основной", - "title": "Продажи по организациям", - "settings": { - "selection": ["Номенклатура", "Количество", "Auto"], - "filter": ["Организация = _ @off @user"], - "order": ["Количество desc", "Auto"], - "conditionalAppearance": [ - { - "filter": ["Просрочено = true"], - "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" }, - "presentation": "Выделять просроченные", - "viewMode": "Normal", - "userSettingID": "auto" - } - ], - "outputParameters": { "Заголовок": "Мой отчёт" }, - "dataParameters": ["Период = LastMonth @user"], - "structure": "Организация > details" - } -}] -``` - -### Условное оформление (conditionalAppearance) - -```json -"conditionalAppearance": [ - { - "selection": ["Поле1"], - "filter": ["Поле1 notFilled"], - "appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" }, - "presentation": "Описание", - "viewMode": "Normal", - "userSettingID": "auto" - } -] -``` - -Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, прочее → String. - -Типы значений фильтра: `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue (автодетект). - -OrGroup в фильтре: `{"group": "Or", "items": ["условие1", "условие2"]}`. - -Folder в selection: `{"folder": "Поступление", "items": ["ПолеА", "ПолеБ"]}` → SelectedItemFolder с lwsTitle и placement=Auto. - -### Итоги с привязкой к группировкам - -```json -"totalFields": [ - { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] } -] -``` - -### Шаблоны вывода — компактный DSL - -Вместо raw XML (`template`) — табличное описание через `rows` + именованный стиль `style`: - -```json -"templates": [ - { - "name": "Макет1", - "style": "header", - "widths": [36, 33, 16, 17], - "minHeight": 24.75, - "rows": [ - ["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец периода"], - ["|", "|", "|", "|"], - ["К1", "К2", "К3", "К4"] - ] - }, - { - "name": "Макет2", - "style": "data", - "widths": [36, 33, 16, 17], - "rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]], - "parameters": [ - { "name": "ВидКассы", "expression": "Представление(Счет)" }, - { "name": "Остаток", "expression": "ОстатокНаНачалоПериода" } - ] - } -] -``` - -Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `">"` — объединение с ячейкой слева, `null` — пустая. - -Двухуровневая шапка с горизонтальным объединением: -```json -"rows": [ - ["Вид актива", "Остаток начало", "Поступление", ">", ">", ">", "Выбытие", ">", ">", "Остаток конец"], - ["|", "|", "из произв.", "из п/ф", "со сч.40", "прочее", "Реализ.", "отгруж.", "прочее", "|"], - ["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8", "К9", "К10"] -] -``` - -Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы. - -Пользовательские стили: файл `skd-styles.json` рядом с JSON-определением, в текущей директории, или в `presets/skills/skd/skd-styles.json` (поиск вверх от OutputPath). Первый найденный файл побеждает. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`. - -Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw. - -### Расшифровка (drilldown) в параметрах шаблона - -Ключ `drilldown` в параметре шаблона автоматически генерирует `DetailsAreaTemplateParameter` и привязку `Расшифровка` в appearance ячеек: - -```json -"parameters": [ - { "name": "Сырье", "expression": "ПоступлениеСырья", "drilldown": "ПоступлениеСырья" } -] -``` - -Генерирует: `ExpressionAreaTemplateParameter` (обычный) + `DetailsAreaTemplateParameter` с именем `Расшифровка_ПоступлениеСырья`, `fieldExpression` по полю `ИмяРесурса`, `mainAction=DrillDown`. Ячейки `{Сырье}` автоматически получают appearance `Расшифровка = Расшифровка_ПоступлениеСырья`. - -### Привязки макетов к группировкам - -```json -"groupTemplates": [ - { "groupName": "ДанныеОтчета", "templateType": "GroupHeader", "template": "Макет1" }, - { "groupField": "Счет", "templateType": "Header", "template": "Макет2" }, - { "groupField": "Счет", "templateType": "OverallHeader", "template": "Макет3" } -] -``` - -`groupField` — привязка к полю группировки, `groupName` — к именованной группировке в структуре варианта. - -`templateType`: `Header` (строки данных) → `<groupTemplate>`, `OverallHeader` (итоги) → `<groupTemplate>`, `GroupHeader` (шапка) → `<groupHeaderTemplate>`. - -## Примеры - -### Минимальный - -```json -{ - "dataSets": [{ - "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", - "fields": ["Наименование"] - }] -} -``` - -### С запросом из внешнего файла (@file) - -```json -{ - "dataSets": [{ - "query": "@queries/sales.sql", - "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] - }] -} -``` - -### С ресурсами, параметрами и @autoDates - -```json -{ - "dataSets": [{ - "query": "ВЫБРАТЬ Продажи.Организация, Продажи.Номенклатура, Продажи.КоличествоОборот КАК Количество, Продажи.СуммаОборот КАК Сумма ИЗ РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода) КАК Продажи", - "fields": [ - "Организация: СправочникСсылка.Организации @dimension", - "Номенклатура: СправочникСсылка.Номенклатура @dimension", - "Количество: число(15,3)", - "Сумма: число(15,2)" - ] - }], - "totalFields": ["Количество: Сумма", "Сумма: Сумма"], - "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"], - "settingsVariants": [{ - "name": "Основной", - "settings": { - "selection": ["Организация", "Номенклатура", "Количество", "Сумма"], - "filter": ["Организация = _ @off @user"], - "dataParameters": "auto", - "structure": "Организация > details" - } - }] -} -``` - -## Верификация - -``` -/skd-validate <OutputPath> — валидация структуры XML -/skd-info <OutputPath> — визуальная сводка -/skd-info <OutputPath> -Mode variant -Name 1 — проверка варианта настроек -``` +--- +name: skd-compile +description: Компиляция схемы компоновки данных 1С (СКД) из компактного JSON-определения. Используй когда нужно создать СКД с нуля +argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputPath <Template.xml>" +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-compile — генерация СКД из JSON DSL + +Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema). + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `DefinitionFile` | Путь к JSON-файлу с определением СКД (взаимоисключающий с Value) | +| `Value` | JSON-строка с определением СКД (взаимоисключающий с DefinitionFile) | +| `OutputPath` | Путь к выходному Template.xml | + +```powershell +# Из файла +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -DefinitionFile "<json>" -OutputPath "<Template.xml>" + +# Из строки (без промежуточного файла) +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -Value '<json-string>' -OutputPath "<Template.xml>" +``` + +## JSON DSL — краткий справочник + +Справочник ниже. Все примеры компилируемы как есть. + +### Корневая структура + +```json +{ + "dataSets": [...], + "calculatedFields": [...], + "totalFields": [...], + "parameters": [...], + "templates": [...], + "groupTemplates": [...], + "dataSetLinks": [...], + "settingsVariants": [...] +} +``` + +Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями. + +### Наборы данных + +Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion. + +```json +{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] } +``` + +Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD. + +**DataSetObject** — внешний набор данных (без источника-запроса). Поля описываются явно; данные передаются вторым параметром `ПроцессорКомпоновкиДанных.Инициализировать(Макет, Новый Структура("<objectName>", ТЗ), ...)`. + +```json +{ "name": "ЖурналОшибок", "objectName": "ЖурналОшибок", "fields": [ + { "field": "ТекстСообщения", "title": "Текст сообщения", "type": "string(150)" }, + { "field": "Расшифровка", "title": "Описание", "type": "CatalogRef.СтруктураПредприятия" } +]} +``` + +`name` — имя набора в схеме, `objectName` — ключ в структуре передачи данных. + +### Поля — shorthand и объектная форма + +``` +"Наименование" — просто имя +"Количество: decimal(15,2)" — имя + тип +"Организация: CatalogRef.Организации @dimension" — + роль +"Служебное: string #noFilter #noOrder" — + ограничения +``` + +Объектная форма — когда нужен title или другие свойства: +```json +{ "field": "ОстатокНаНачалоПериода", "title": "Остаток на начало периода" } +``` +`dataPath` автоматически берётся из `field`, если не указан явно. + +Многоязычный заголовок: `"title": { "ru": "...", "en": "..." }`. Применимо везде, где принимается title/presentation (поля, calculatedFields, parameters, settingsVariants, availableValues и пр.). Строка эквивалентна `{ "ru": "..." }`. + +Типы: `string`, `string(N)`, `decimal`, `decimal(D)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией. + +`decimal` без скобок = `10,2` (деньги по умолчанию), `decimal(N)` = `N,0` (целое); `,nonneg` в конце скобок → AllowedSign=Nonnegative. + +Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу. + +Роли (shorthand или объект): + +- `@`-флаги: `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues` +- KV: `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType` + +``` +"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance" +``` + +Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. + +В объектной форме: `"useRestriction": { "field": true, "condition": true, "group": true, "order": true }` или `"restrict": ["noField", "noFilter"]`. + +Дополнительные ключи объектной формы: +- `"presentationExpression": "<выражение>"` — что показывать вместо значения поля. Исходное значение остаётся «под капотом» для перехода/расшифровки. +- `"appearance": { "<параметр>": "<значение>" }` — оформление колонки по умолчанию (применяется во всех вариантах настроек). Ключи — параметры платформы (`ГоризонтальноеПоложение`, `МинимальнаяШирина`, `Формат`, `Текст` и т.п.). +- `"orderExpression": { "expression": "<выражение>", "orderType": "Asc"/"Desc", "autoOrder": true/false }` — сортировка поля по выражению (например `ЕстьNULL(Поле.Порядок, 10000)`). + +```json +{ "field": "Сумма", "title": "Сумма продажи", "type": "decimal(15,2)", + "appearance": { "ГоризонтальноеПоложение": "Right", "МинимальнаяШирина": "80" } } +``` + +### Вычисляемые поля (calculatedFields) + +Shorthand: `"Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder"` — все части кроме имени опциональны. + +```json +"calculatedFields": [ + "Маржа = Цена - Закупка", + "Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100", + "Служебное: string = \"\" #noField #noFilter #noGroup #noOrder" +] +``` + +Объектная форма — когда нужна `appearance`: +```json +{ "name": "Маржа", "title": "Маржа", "expression": "Цена - Закупка", "type": "decimal(15,2)", "useRestriction": "#noField #noFilter" } +``` + +### Итоги (shorthand) + +```json +"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +### Параметры (shorthand + @autoDates) + +```json +"parameters": [ + "Период [Отчетный период]: StandardPeriod = LastMonth @autoDates" +] +``` + +Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>` (LocalStringType). + +Флаги shorthand: +- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить. +- `@valueList` — `<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений (при значении-списке ниже подразумевается автоматически) +- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"` + +Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`. + +Значение-список: несколько значений по умолчанию через запятую в `значение` (для запятой внутри значения — кавычки `'...'`). В объектной форме — массив в `value`. + +```json +"parameters": [ + "Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" +] +``` + +Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме. + +Список допустимых значений (availableValues): + +```json +{ + "name": "ПорядокОкругления", + "type": "EnumRef.Округления", + "value": "Перечисление.Округления.Окр1_00", + "use": "Always", + "denyIncompleteValues": true, + "availableValues": [ + {"value": "Перечисление.Округления.Окр1_00", "presentation": "руб. коп"}, + {"value": "Перечисление.Округления.Окр1", "presentation": "руб."}, + {"value": "Перечисление.Округления.Окр1000", "presentation": "тыс. руб"} + ] +} +``` + +В варианте настроек `"dataParameters": "auto"` выводит все не-hidden параметры с `userSettingID`. Значения по умолчанию наследуются и остаются активными; параметры без значения по умолчанию отключаются (пользователь включит их в настройках). + +### Фильтры — shorthand + +```json +"filter": [ + "Организация = _ @off @user", + "Дата >= 2024-01-01T00:00:00", + "Статус filled" +] +``` + +Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`. + +В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`. + +Группы фильтров (Or/And/Not): +```json +{ "group": "Or", "items": [ + { "group": "And", "items": [ + { "field": "Статус", "op": "=", "value": "Активен" }, + { "field": "Сумма", "op": ">", "value": 1000 } + ]}, + { "field": "Количество", "op": "filled" } +]} +``` + +### Параметры данных — shorthand + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. + +### Структура — string shorthand + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +``` + +`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне. + +Объектная форма — для сложных случаев (именованные группировки, selection/filter на уровне группировки, таблицы, диаграммы): + +```json +"structure": [ + { + "name": "ПоОрганизациям", + "groupFields": ["Организация"], + "selection": ["Организация", "Сумма", "Auto"], + "children": [{ "groupFields": [] }] + } +] +``` + +`type` по умолчанию `"group"` (можно не указывать). `groupFields` — алиас для `groupBy`. Поддержка `name`, `selection`, `order`, `filter`, `outputParameters`, рекурсивных `children`. + +### Варианты настроек + +```json +"settingsVariants": [{ + "name": "Основной", + "title": "Продажи по организациям", + "settings": { + "selection": ["Номенклатура", "Количество", "Auto"], + "filter": ["Организация = _ @off @user"], + "order": ["Количество desc", "Auto"], + "conditionalAppearance": [ + { + "filter": ["Просрочено = true"], + "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" }, + "presentation": "Выделять просроченные", + "viewMode": "Normal", + "userSettingID": "auto" + } + ], + "outputParameters": { "Заголовок": "Мой отчёт" }, + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } +}] +``` + +### Условное оформление (conditionalAppearance) + +```json +"conditionalAppearance": [ + { + "selection": ["Поле1"], + "filter": ["Поле1 notFilled"], + "appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" }, + "presentation": "Описание", + "viewMode": "Normal", + "userSettingID": "auto" + } +] +``` + +Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, прочее → String. + +Типы значений фильтра: `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue (автодетект). + +OrGroup в фильтре: `{"group": "Or", "items": ["условие1", "условие2"]}`. + +Folder в selection: `{"folder": "Поступление", "items": ["ПолеА", "ПолеБ"]}` → SelectedItemFolder с lwsTitle и placement=Auto. + +### Итоги с привязкой к группировкам + +```json +"totalFields": [ + { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] } +] +``` + +### Шаблоны вывода — компактный DSL + +Вместо raw XML (`template`) — табличное описание через `rows` + именованный стиль `style`: + +```json +"templates": [ + { + "name": "Макет1", + "style": "header", + "widths": [36, 33, 16, 17], + "minHeight": 24.75, + "rows": [ + ["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец периода"], + ["|", "|", "|", "|"], + ["К1", "К2", "К3", "К4"] + ] + }, + { + "name": "Макет2", + "style": "data", + "widths": [36, 33, 16, 17], + "rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]], + "parameters": [ + { "name": "ВидКассы", "expression": "Представление(Счет)" }, + { "name": "Остаток", "expression": "ОстатокНаНачалоПериода" } + ] + } +] +``` + +Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `">"` — объединение с ячейкой слева, `null` — пустая. + +Двухуровневая шапка с горизонтальным объединением: +```json +"rows": [ + ["Вид актива", "Остаток начало", "Поступление", ">", ">", ">", "Выбытие", ">", ">", "Остаток конец"], + ["|", "|", "из произв.", "из п/ф", "со сч.40", "прочее", "Реализ.", "отгруж.", "прочее", "|"], + ["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8", "К9", "К10"] +] +``` + +Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы. + +Пользовательские стили: файл `skd-styles.json` рядом с JSON-определением, в текущей директории, или в `presets/skills/skd/skd-styles.json` (поиск вверх от OutputPath). Первый найденный файл побеждает. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`. + +Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw. + +### Расшифровка (drilldown) в параметрах шаблона + +Ключ `drilldown` в параметре шаблона автоматически генерирует `DetailsAreaTemplateParameter` и привязку `Расшифровка` в appearance ячеек: + +```json +"parameters": [ + { "name": "Сырье", "expression": "ПоступлениеСырья", "drilldown": "ПоступлениеСырья" } +] +``` + +Генерирует: `ExpressionAreaTemplateParameter` (обычный) + `DetailsAreaTemplateParameter` с именем `Расшифровка_ПоступлениеСырья`, `fieldExpression` по полю `ИмяРесурса`, `mainAction=DrillDown`. Ячейки `{Сырье}` автоматически получают appearance `Расшифровка = Расшифровка_ПоступлениеСырья`. + +### Привязки макетов к группировкам + +```json +"groupTemplates": [ + { "groupName": "ДанныеОтчета", "templateType": "GroupHeader", "template": "Макет1" }, + { "groupField": "Счет", "templateType": "Header", "template": "Макет2" }, + { "groupField": "Счет", "templateType": "OverallHeader", "template": "Макет3" } +] +``` + +`groupField` — привязка к полю группировки, `groupName` — к именованной группировке в структуре варианта. + +`templateType`: `Header` (строки данных) → `<groupTemplate>`, `OverallHeader` (итоги) → `<groupTemplate>`, `GroupHeader` (шапка) → `<groupHeaderTemplate>`. + +## Примеры + +### Минимальный + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] +} +``` + +### С запросом из внешнего файла (@file) + +```json +{ + "dataSets": [{ + "query": "@queries/sales.sql", + "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] + }] +} +``` + +### С ресурсами, параметрами и @autoDates + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Продажи.Организация, Продажи.Номенклатура, Продажи.КоличествоОборот КАК Количество, Продажи.СуммаОборот КАК Сумма ИЗ РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода) КАК Продажи", + "fields": [ + "Организация: СправочникСсылка.Организации @dimension", + "Номенклатура: СправочникСсылка.Номенклатура @dimension", + "Количество: число(15,3)", + "Сумма: число(15,2)" + ] + }], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Организация", "Номенклатура", "Количество", "Сумма"], + "filter": ["Организация = _ @off @user"], + "dataParameters": "auto", + "structure": "Организация > details" + } + }] +} +``` + +## Верификация + +``` +/skd-validate <OutputPath> — валидация структуры XML +/skd-info <OutputPath> — визуальная сводка +/skd-info <OutputPath> -Mode variant -Name 1 — проверка варианта настроек +``` diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 0696ed91..3bd81e3b 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,3686 +1,3686 @@ -# skd-compile v1.107 — Compile 1C DCS from JSON -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [string]$DefinitionFile, - [string]$Value, - [Parameter(Mandatory)] - [string]$OutputPath -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -# --- 1. Load and validate JSON --- - -if ($DefinitionFile -and $Value) { - Write-Error "Cannot use both -DefinitionFile and -Value" - exit 1 -} -if (-not $DefinitionFile -and -not $Value) { - Write-Error "Either -DefinitionFile or -Value is required" - exit 1 -} - -if ($DefinitionFile) { - if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { - $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile - } - if (-not (Test-Path $DefinitionFile)) { - Write-Error "Definition file not found: $DefinitionFile" - exit 1 - } - $json = Get-Content -Raw -Encoding UTF8 $DefinitionFile -} else { - $json = $Value -} - -$def = $json | ConvertFrom-Json - -# --- Sentinel check: refuse to compile if JSON contains skd-decompile sentinels --- -# These mark places the decompiler couldn't reverse cleanly; user must resolve -# them manually before compile (see <basename>.warnings.md alongside the JSON). -$script:foundSentinels = @() -function Scan-Sentinels { - param($obj, [string]$path) - if ($null -eq $obj) { return } - if ($obj -is [System.Collections.IDictionary]) { - foreach ($k in @($obj.Keys)) { - if ($k -eq '__unsupported__') { - $u = $obj[$k] - $id = $u.id; $kind = $u.kind; $loc = $u.loc - $script:foundSentinels += " $id [$kind] at $path → $loc" - } else { - Scan-Sentinels -obj $obj[$k] -path "$path/$k" - } - } - } elseif ($obj -is [System.Management.Automation.PSCustomObject]) { - foreach ($p in $obj.PSObject.Properties) { - if ($p.Name -eq '__unsupported__') { - $u = $p.Value - $id = $u.id; $kind = $u.kind; $loc = $u.loc - $script:foundSentinels += " $id [$kind] at $path → $loc" - } else { - Scan-Sentinels -obj $p.Value -path "$path/$($p.Name)" - } - } - } elseif ($obj -is [System.Collections.IEnumerable] -and -not ($obj -is [string])) { - $i = 0 - foreach ($item in $obj) { Scan-Sentinels -obj $item -path "$path[$i]"; $i++ } - } -} -Scan-Sentinels -obj $def -path '' -if ($script:foundSentinels.Count -gt 0) { - [Console]::Error.WriteLine("skd-compile: JSON содержит __unsupported__ маркеры от skd-decompile.") - [Console]::Error.WriteLine("Это конструкции, которые декомпиляция не смогла обратить — нужно разрешить вручную перед компиляцией.") - [Console]::Error.WriteLine("См. <basename>.warnings.md рядом с JSON. Найдено:") - foreach ($s in $script:foundSentinels) { [Console]::Error.WriteLine($s) } - exit 4 -} - -if (-not $def.dataSets -or $def.dataSets.Count -eq 0) { - Write-Error "JSON must have at least one entry in 'dataSets'" - exit 1 -} - -# Base directory for resolving @file references in query -$script:queryBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName($DefinitionFile) } else { (Get-Location).Path } - -# --- 2. XML helpers --- - -$script:xml = New-Object System.Text.StringBuilder 16384 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>') -} - -function Resolve-QueryValue { - param([string]$val, [string]$baseDir) - if (-not $val.StartsWith("@")) { return $val } - $filePath = $val.Substring(1) - if ([System.IO.Path]::IsPathRooted($filePath)) { - $candidates = @($filePath) - } else { - $candidates = @( - (Join-Path $baseDir $filePath), - (Join-Path (Get-Location).Path $filePath) - ) - } - foreach ($c in $candidates) { - if (Test-Path $c) { - return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() - } - } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" - exit 1 -} - -function Emit-MLText { - param([string]$tag, $text, [string]$indent, [switch]$NoXsiType) - # Empty value → self-closing tag (matches platform output) - if ($null -eq $text -or ($text -is [string] -and $text -eq '')) { - if ($NoXsiType) { - X "$indent<$tag/>" - } else { - X "$indent<$tag xsi:type=`"v8:LocalStringType`"/>" - } - return - } - if ($NoXsiType) { - X "$indent<$tag>" - } else { - X "$indent<$tag xsi:type=`"v8:LocalStringType`">" - } - # Multi-lang: object form { ru: "...", en: "..." } → one <v8:item> per language - if ($text -is [System.Management.Automation.PSCustomObject] -or $text -is [hashtable] -or $text -is [System.Collections.IDictionary]) { - $props = if ($text -is [System.Management.Automation.PSCustomObject]) { $text.PSObject.Properties } else { $text.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } } - foreach ($p in $props) { - $lang = if ($p -is [hashtable]) { $p.Name } else { $p.Name } - $content = if ($p -is [hashtable]) { $p.Value } else { $p.Value } - X "$indent`t<v8:item>" - X "$indent`t`t<v8:lang>$(Esc-Xml "$lang")</v8:lang>" - X "$indent`t`t<v8:content>$(Esc-Xml "$content")</v8:content>" - X "$indent`t</v8:item>" - } - } else { - X "$indent`t<v8:item>" - X "$indent`t`t<v8:lang>ru</v8:lang>" - X "$indent`t`t<v8:content>$(Esc-Xml "$text")</v8:content>" - X "$indent`t</v8:item>" - } - X "$indent</$tag>" -} - -function New-Guid-String { - return [System.Guid]::NewGuid().ToString() -} - -# --- 3. Resolve defaults --- - -# DataSources -$dataSources = @() -if ($def.dataSources) { - foreach ($ds in $def.dataSources) { - $dataSources += @{ - name = "$($ds.name)" - type = if ($ds.type) { "$($ds.type)" } else { "Local" } - } - } -} else { - $dataSources += @{ name = "ИсточникДанных1"; type = "Local" } -} - -$defaultSource = $dataSources[0].name - -# Auto-name dataSets -$dsIndex = 1 -foreach ($ds in $def.dataSets) { - if (-not $ds.name) { - $ds | Add-Member -NotePropertyName "name" -NotePropertyValue "НаборДанных$dsIndex" -Force - } - $dsIndex++ -} - -# --- 4. Type system --- - -# Type synonyms — normalize Russian/common names to canonical DSL types -# Use case-sensitive hashtable to avoid PS 5.1 DuplicateKeyInHashLiteral -$script:typeSynonyms = New-Object System.Collections.Hashtable -# Russian names (case doesn't matter — we'll also do case-insensitive lookup) -$script:typeSynonyms["число"] = "decimal" -$script:typeSynonyms["строка"] = "string" -$script:typeSynonyms["булево"] = "boolean" -$script:typeSynonyms["дата"] = "date" -$script:typeSynonyms["датавремя"] = "dateTime" -$script:typeSynonyms["время"] = "time" -$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" -# English canonical (lowercase for lookup) -$script:typeSynonyms["bool"] = "boolean" -$script:typeSynonyms["str"] = "string" -$script:typeSynonyms["int"] = "decimal" -$script:typeSynonyms["integer"] = "decimal" -$script:typeSynonyms["number"] = "decimal" -$script:typeSynonyms["num"] = "decimal" -# Reference synonyms (Russian, lowercase) -$script:typeSynonyms["справочникссылка"] = "CatalogRef" -$script:typeSynonyms["документссылка"] = "DocumentRef" -$script:typeSynonyms["перечислениессылка"] = "EnumRef" -$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - - # Check for parameterized types: число(15,2), строка(100), etc. - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $baseName = $Matches[1].Trim() - $params = $Matches[2] - - # Resolve base name (case-insensitive via .ToLower()) - $resolved = $script:typeSynonyms[$baseName.ToLower()] - if ($resolved) { return "$resolved($params)" } - - return $typeStr - } - - # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации - if ($typeStr.Contains('.')) { - $dotIdx = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $dotIdx) - $suffix = $typeStr.Substring($dotIdx) # includes the dot - $resolved = $script:typeSynonyms[$prefix.ToLower()] - if ($resolved) { return "$resolved$suffix" } - return $typeStr - } - - # Simple name lookup (case-insensitive) - $resolved = $script:typeSynonyms[$typeStr.ToLower()] - if ($resolved) { return $resolved } - - return $typeStr -} - -function Emit-ValueType { - param($typeStr, [string]$indent) - - if (-not $typeStr) { return } - - # Multi-type: iterate and emit each type with its qualifiers - if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { - foreach ($t in $typeStr) { Emit-SingleValueType -typeStr "$t" -indent $indent } - return - } - - Emit-SingleValueType -typeStr "$typeStr" -indent $indent -} - -function Emit-SingleValueType { - param([string]$typeStr, [string]$indent) - - if (-not $typeStr) { return } - - # Resolve synonyms first - $typeStr = Resolve-TypeStr $typeStr - - # boolean - if ($typeStr -eq "boolean") { - X "$indent<v8:Type>xs:boolean</v8:Type>" - return - } - - # string, string(N), string(N,fix) — fix → AllowedLength=Fixed - if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - $al = if ($Matches[4]) { "Fixed" } else { "Variable" } - X "$indent<v8:Type>xs:string</v8:Type>" - X "$indent<v8:StringQualifiers>" - X "$indent`t<v8:Length>$len</v8:Length>" - X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" - X "$indent</v8:StringQualifiers>" - return - } - - # decimal forms (defaults — bare decimal = money 10,2; decimal(N) = integer N,0): - # decimal → 10,2,Any - # decimal(N) → N,0,Any - # decimal(N,nonneg) → N,0,Nonnegative - # decimal(N,M) → N,M,Any - # decimal(N,M,nonneg) → N,M,Nonnegative - if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { - if (-not $Matches[1]) { - $digits = "10"; $fraction = "2"; $sign = "Any" - } else { - $digits = $Matches[2] - $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } - $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } - } - X "$indent<v8:Type>xs:decimal</v8:Type>" - X "$indent<v8:NumberQualifiers>" - X "$indent`t<v8:Digits>$digits</v8:Digits>" - X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" - X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" - X "$indent</v8:NumberQualifiers>" - return - } - - # date / dateTime / time — all use xs:dateTime, differ only in DateFractions - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { - "date" { "Date" } - "dateTime" { "DateTime" } - "time" { "Time" } - } - X "$indent<v8:Type>xs:dateTime</v8:Type>" - X "$indent<v8:DateQualifiers>" - X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" - X "$indent</v8:DateQualifiers>" - return - } - - # StandardPeriod - if ($typeStr -eq "StandardPeriod") { - X "$indent<v8:Type>v8:StandardPeriod</v8:Type>" - return - } - - # Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc. - # Real DCS files use inline namespace d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" - if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { - X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" - return - } - - # TypeSet (композитный тип-набор): голое имя без точки, типа DocumentRef / CatalogRef / - # EnumRef / ChartOfAccountsRef / etc. (все ссылки указанного класса). - # Эмитим <v8:TypeSet xmlns:dN="..."> вместо <v8:Type>. - if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef|InformationRegisterRef|AnyRef)$') { - X "$indent<v8:TypeSet xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:TypeSet>" - return - } - - # Fallback — assume dot-qualified types are also config references - if ($typeStr.Contains('.')) { - X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" - return - } - - X "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>" -} - -# --- 5. Field shorthand parser --- - -function Parse-FieldShorthand { - param([string]$s) - - $result = @{ - dataPath = ""; field = ""; title = ""; type = "" - roles = @(); restrict = @(); appearance = [ordered]@{} - roleExtras = [ordered]@{} - } - - # Extract @roles - $roleMatches = [regex]::Matches($s, '@(\w+)') - foreach ($m in $roleMatches) { - $result.roles += $m.Groups[1].Value - } - $s = [regex]::Replace($s, '\s*@\w+', '') - - # Extract #restrictions - $restrictMatches = [regex]::Matches($s, '#(\w+)') - foreach ($m in $restrictMatches) { - $result.restrict += $m.Groups[1].Value - } - $s = [regex]::Replace($s, '\s*#\w+', '') - - # Extract role kv=value (e.g. balanceGroupName=Сумма balanceType=OpeningBalance) - $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') - foreach ($m in $kvMatches) { $result.roleExtras[$m.Groups[1].Value] = $m.Groups[2].Value } - $s = [regex]::Replace($s, '\s*\w+=\S+', '') - - # Split name: type - $s = $s.Trim() - if ($s.Contains(':')) { - $parts = $s -split ':', 2 - $result.dataPath = $parts[0].Trim() - $result.type = Resolve-TypeStr ($parts[1].Trim()) - } else { - $result.dataPath = $s - } - - $result.field = $result.dataPath - return $result -} - -# Universal role spec parser: string / array / object / null -# Returns @{ tokens = @(...); extras = [ordered]@{...} } -function Parse-RoleSpec { - param($spec) - $tokens = @() - $extras = [ordered]@{} - - if ($null -ne $spec) { - if ($spec -is [string]) { - if ($spec -notmatch '\s' -and $spec -notmatch '=') { - $tokens += $spec - } else { - $s = $spec.Trim() - foreach ($m in [regex]::Matches($s, '@(\w+)')) { $tokens += $m.Groups[1].Value } - $s = [regex]::Replace($s, '\s*@\w+', '').Trim() - foreach ($m in [regex]::Matches($s, '(\w+)=(\S+)')) { $extras[$m.Groups[1].Value] = $m.Groups[2].Value } - } - } elseif ($spec -is [array] -or $spec -is [System.Collections.IList]) { - foreach ($t in $spec) { $tokens += "$t" } - } elseif ($spec.PSObject -and $spec.PSObject.Properties) { - foreach ($prop in $spec.PSObject.Properties) { - $val = $prop.Value - if ($val -is [bool]) { - if ($val) { $tokens += $prop.Name } - } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { - $extras[$prop.Name] = "$val" - } - } - } elseif ($spec -is [hashtable] -or $spec -is [System.Collections.IDictionary]) { - foreach ($k in $spec.Keys) { - $val = $spec[$k] - if ($val -is [bool]) { - if ($val) { $tokens += "$k" } - } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { - $extras["$k"] = "$val" - } - } - } - } - - # Deprecated alias: balanceGroup → balanceGroupName (старое имя в коде compile, в реальном XML — Name) - if ($extras.Contains('balanceGroup') -and -not $extras.Contains('balanceGroupName')) { - $extras['balanceGroupName'] = $extras['balanceGroup'] - $extras.Remove('balanceGroup') - } - - return @{ tokens = $tokens; extras = $extras } -} - -# --- 6. Total field shorthand parser --- - -function Parse-TotalShorthand { - param([string]$s) - - # "DataPath: Func" or "DataPath: Func(expr)" - $parts = $s -split ':', 2 - $dataPath = $parts[0].Trim() - $funcPart = $parts[1].Trim() - - # Known DCS aggregate functions (ru + en) - $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', - 'Sum','Count','Min','Max','Avg', - 'Minimum','Maximum','Average') - - if ($funcPart -match '^\w+\(') { - # Already has expression form: Func(expr) - return @{ dataPath = $dataPath; expression = $funcPart } - } elseif ($funcPart -in $aggFuncs) { - # Short: Func → Func(DataPath) - return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } - } else { - # Identity or custom expression — use as-is - return @{ dataPath = $dataPath; expression = $funcPart } - } -} - -# --- 7. Parameter shorthand parser --- - -function Split-ValueListCsv { - # Split on top-level commas (respecting 'single'/"double" quotes), strip quotes, - # drop empties. No ':' handling — values may contain colons (dateTime). - param([string]$s) - $result = @() - if ($null -eq $s) { return ,$result } - $items = @() - $buf = New-Object System.Text.StringBuilder - $inQuote = $null - for ($i = 0; $i -lt $s.Length; $i++) { - $ch = $s[$i] - if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } - elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } - elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } - else { [void]$buf.Append($ch) } - } - if ($buf.Length -gt 0) { $items += $buf.ToString() } - foreach ($raw in $items) { - $t = $raw.Trim() - if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } - if ($t -ne "") { $result += $t } - } - return ,$result -} - -function Parse-ParamShorthand { - param([string]$s) - - $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null } - - # Extract @autoDates flag - if ($s -match '@autoDates') { - $result.autoDates = $true - $s = $s -replace '\s*@autoDates', '' - } - - # Extract @valueList flag - if ($s -match '@valueList') { - $result.valueListAllowed = $true - $s = $s -replace '\s*@valueList', '' - } - - # Extract @hidden flag - if ($s -match '@hidden') { - $result.hidden = $true - $s = $s -replace '\s*@hidden', '' - } - - # Extract optional [Title] (mirrors Parse-FieldShorthand) - if ($s -match '\[([^\]]*)\]') { - $result.title = $Matches[1].Trim() - $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() - } - - # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value - if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { - $result.name = $Matches[1].Trim() - $result.type = Resolve-TypeStr ($Matches[2].Trim()) - if ($Matches[4]) { - $rhs = $Matches[4].Trim() - $items = Split-ValueListCsv $rhs - if ($items.Count -ge 2) { - # Multi-value default → list; valueListAllowed implied - $result.value = $items - $result.valueListAllowed = $true - } elseif ($items.Count -eq 1) { - $result.value = $items[0] - } else { - $result.value = $rhs - } - } - } else { - $result.name = $s.Trim() - } - - return $result -} - -# --- 8. Calculated field shorthand parser --- - -function Parse-CalcShorthand { - param([string]$s) - - # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". - # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside - # an expression (e.g. index access) isn't interpreted as a title. - # - `#restrict` flags use a known-names pattern and are extracted globally — - # the docs put them after `=`, and the closed flag set avoids matching - # `#word` that happens to appear inside a string literal. - $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' - - $restrict = @() - foreach ($m in [regex]::Matches($s, $restrictPattern)) { - $restrict += $m.Groups[1].Value - } - $s = [regex]::Replace($s, "\s*$restrictPattern", '') - - $eqIdx = $s.IndexOf('=') - if ($eqIdx -gt 0) { - $lhs = $s.Substring(0, $eqIdx) - $rhs = $s.Substring($eqIdx + 1).Trim() - } else { - $lhs = $s - $rhs = "" - } - - $title = "" - if ($lhs -match '\[([^\]]+)\]') { - $title = $Matches[1] - $lhs = $lhs -replace '\s*\[[^\]]+\]', '' - } - $lhs = $lhs.Trim() - - $type = "" - $dataPath = $lhs - if ($lhs.Contains(':')) { - $parts = $lhs -split ':', 2 - $dataPath = $parts[0].Trim() - $type = Resolve-TypeStr ($parts[1].Trim()) - } - - return @{ - dataPath = $dataPath - expression = $rhs - type = $type - title = $title - restrict = $restrict - } -} - -# --- 8b. DataParameter shorthand parser --- -# Formats: "Период = LastMonth @user", "Организация @off @user", "Период @user" -function Parse-DataParamShorthand { - param([string]$s) - - $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } - - # Extract @flags - if ($s -match '@user') { - $result.userSettingID = "auto" - $s = $s -replace '\s*@user', '' - } - if ($s -match '@off') { - $result.use = $false - $s = $s -replace '\s*@off', '' - } - if ($s -match '@quickAccess') { - $result.viewMode = "QuickAccess" - $s = $s -replace '\s*@quickAccess', '' - } - if ($s -match '@normal') { - $result.viewMode = "Normal" - $s = $s -replace '\s*@normal', '' - } - - $s = $s.Trim() - - # Split "Name = Value" - if ($s -match '^([^=]+)=\s*(.+)$') { - $result.parameter = $Matches[1].Trim() - $valStr = $Matches[2].Trim() - - # Detect StandardPeriod variants - $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") - if ($periodVariants -contains $valStr) { - $result.value = @{ variant = $valStr } - } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { - $result.value = $valStr - } elseif ($valStr -eq "true" -or $valStr -eq "false") { - $result.value = [bool]($valStr -eq "true") - } else { - $result.value = $valStr - } - } else { - $result.parameter = $s - } - - return $result -} - -# --- 8c. Filter item shorthand parser --- -# Formats: "Организация = _ @off @user", "Дата >= 2024-01-01T00:00:00", "Статус filled" -function Parse-FilterShorthand { - param([string]$s) - - $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } - - # Extract @flags - if ($s -match '@user') { - $result.userSettingID = "auto" - $s = $s -replace '\s*@user', '' - } - if ($s -match '@off') { - $result.use = $false - $s = $s -replace '\s*@off', '' - } - if ($s -match '@quickAccess') { - $result.viewMode = "QuickAccess" - $s = $s -replace '\s*@quickAccess', '' - } - if ($s -match '@normal') { - $result.viewMode = "Normal" - $s = $s -replace '\s*@normal', '' - } - if ($s -match '@inaccessible') { - $result.viewMode = "Inaccessible" - $s = $s -replace '\s*@inaccessible', '' - } - - $s = $s.Trim() - - # Try to match: Field op Value, or Field op (no value for filled/notFilled) - # Operators sorted longest first to match >= before > - $opPatterns = @('<>', '>=', '<=', '=', '>', '<', - 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', - 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', - 'notFilled\b', 'filled\b') - $opJoined = $opPatterns -join '|' - - if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { - $result.field = $Matches[1].Trim() - $opRaw = $Matches[2].Trim() - $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } - - # Map op - $opMap = @{ - "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" - "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" - "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" - "contains" = "Contains"; "notContains" = "NotContains" - "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" - "filled" = "Filled"; "notFilled" = "NotFilled" - } - $mapped = $opMap[$opRaw] - if ($mapped) { $result.op = $opRaw } else { $result.op = $opRaw } - - # Parse value (skip "_" which means empty/placeholder) - if ($valPart -and $valPart -ne "_") { - if ($valPart -eq "true" -or $valPart -eq "false") { - $result.value = [bool]($valPart -eq "true") - $result["valueType"] = "xs:boolean" - } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { - $result.value = $valPart - $result["valueType"] = "xs:dateTime" - } elseif ($valPart -match '^\d+(\.\d+)?$') { - $result.value = $valPart - $result["valueType"] = "xs:decimal" - } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { - $result.value = $valPart - $result["valueType"] = "dcscor:DesignTimeValue" - } else { - $result.value = $valPart - $result["valueType"] = "xs:string" - } - } - } else { - # No operator found — just a field name - $result.field = $s - } - - return $result -} - -# --- 9. Comparison type mapper --- - -$script:comparisonTypes = @{ - "=" = "Equal"; "<>" = "NotEqual" - ">" = "Greater"; ">=" = "GreaterOrEqual" - "<" = "Less"; "<=" = "LessOrEqual" - "in" = "InList"; "notIn" = "NotInList" - "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" - "contains" = "Contains"; "notContains" = "NotContains" - "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" - "filled" = "Filled"; "notFilled" = "NotFilled" -} - -# --- 10. Output parameter type detection --- - -$script:outputParamTypes = @{ - "Заголовок" = "mltext" - "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" - "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" - "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" - "МакетОформления" = "xs:string" - "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" - "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" - "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" - "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" - "РасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" - "РасположениеИтогов" = "dcscor:DataCompositionTotalPlacement" - "РасположениеГруппировки" = "dcsset:DataCompositionFieldGroupPlacement" - "РасположениеРесурсов" = "dcsset:DataCompositionResourcesPlacement" - "ТипМакета" = "dcsset:DataCompositionGroupTemplateType" -} - -# --- 11. Emit sections --- - -# === DataSources === -function Emit-DataSources { - foreach ($ds in $dataSources) { - X "`t<dataSource>" - X "`t`t<name>$(Esc-Xml $ds.name)</name>" - X "`t`t<dataSourceType>$(Esc-Xml $ds.type)</dataSourceType>" - X "`t</dataSource>" - } -} - -# === Fields === -function Has-JsonProp { - param($obj, [string]$name) - if ($null -eq $obj) { return $false } - if ($obj.PSObject -and $obj.PSObject.Properties) { - return $null -ne $obj.PSObject.Properties[$name] - } - if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } - return $false -} - -function Emit-InputParameters { - param($ip, [string]$indent) - if ($null -eq $ip) { return } - $items = @($ip) - if ($items.Count -eq 0) { return } - X "$indent<inputParameters>" - foreach ($item in $items) { - X "$indent`t<dcscor:item>" - if ((Has-JsonProp $item 'use') -and $null -ne $item.use -and -not $item.use) { - X "$indent`t`t<dcscor:use>false</dcscor:use>" - } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" - if (Has-JsonProp $item 'choiceParameters') { - $cp = $item.choiceParameters - $cpItems = if ($null -ne $cp) { @($cp) } else { @() } - if ($cpItems.Count -eq 0) { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" - foreach ($cpItem in $cpItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" - foreach ($v in @($cpItem.values)) { - if ($v -is [bool]) { - $vStr = if ($v) { 'true' } else { 'false' } - X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$vStr</dcscor:value>" - } elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { - X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" - } else { - X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" - } - } - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-JsonProp $item 'choiceParameterLinks') { - $cpl = $item.choiceParameterLinks - $cplItems = if ($null -ne $cpl) { @($cpl) } else { @() } - if ($cplItems.Count -eq 0) { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" - foreach ($cplItem in $cplItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" - X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" - $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } - X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-JsonProp $item 'value') { - # Simple typed value — определяем xsi:type из JSON-типа - $val = $item.value - # Явный кастомный type из decompile: {uri, name} → <value xmlns:dN="uri" xsi:type="dN:name"> - $customType = $null - if (Has-JsonProp $item 'valueType') { - $vtSrc = $item.valueType - $uri = $null; $tName = $null - if ($vtSrc -is [PSCustomObject]) { - if ($vtSrc.PSObject.Properties['uri']) { $uri = "$($vtSrc.uri)" } - if ($vtSrc.PSObject.Properties['name']) { $tName = "$($vtSrc.name)" } - } elseif ($vtSrc -is [System.Collections.IDictionary]) { - if ($vtSrc.Contains('uri')) { $uri = "$($vtSrc['uri'])" } - if ($vtSrc.Contains('name')) { $tName = "$($vtSrc['name'])" } - } - if ($uri -and $tName) { $customType = @{ uri = $uri; name = $tName } } - } - if ($customType) { - X "$indent`t`t<dcscor:value xmlns:dN=`"$($customType.uri)`" xsi:type=`"dN:$($customType.name)`">$(Esc-Xml "$val")</dcscor:value>" - } elseif ($val -is [bool]) { - $vStr = if ($val) { 'true' } else { 'false' } - X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$vStr</dcscor:value>" - } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { - X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" - } elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { - # Multilang dict {ru, en, ...} → LocalStringType - Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" - } - } - X "$indent`t</dcscor:item>" - } - X "$indent</inputParameters>" -} - -function Emit-Field { - param($fieldDef, [string]$indent) - - if ($fieldDef -is [string]) { - $f = Parse-FieldShorthand $fieldDef - } else { - $f = @{ - dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" } - field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" } - title = if ($fieldDef.title) { $fieldDef.title } else { "" } - type = if ($fieldDef.type) { - if ($fieldDef.type -is [array] -or $fieldDef.type -is [System.Collections.IList]) { - @($fieldDef.type | ForEach-Object { Resolve-TypeStr "$_" }) - } else { - Resolve-TypeStr "$($fieldDef.type)" - } - } else { "" } - roles = @() - restrict = @() - appearance = [ordered]@{} - roleExtras = [ordered]@{} - } - # Parse role (string shorthand / array / object — единый формат с /skd-edit set-field-role) - if ($fieldDef.role) { - $parsed = Parse-RoleSpec $fieldDef.role - $f.roles = $parsed.tokens - $f.roleExtras = $parsed.extras - } - # Parse restrictions - if ($fieldDef.restrict) { - $f.restrict = @($fieldDef.restrict) - } - # Parse appearance (сохраняем значение как есть — может быть string или multilang dict) - if ($fieldDef.appearance) { - foreach ($prop in $fieldDef.appearance.PSObject.Properties) { - $f.appearance[$prop.Name] = $prop.Value - } - } - if ($fieldDef.presentationExpression) { - $f["presentationExpression"] = "$($fieldDef.presentationExpression)" - } - # attrRestrict - if ($fieldDef.attrRestrict) { - $f["attrRestrict"] = @($fieldDef.attrRestrict) - } - # availableValues — array of {value, presentation} - if ($fieldDef.availableValues) { - $f["availableValues"] = $fieldDef.availableValues - } - # orderExpression — {expression, orderType, autoOrder} - if ($fieldDef.orderExpression) { - $f["orderExpression"] = $fieldDef.orderExpression - } - # inputParameters — массив элементов, типизированных по форме value - if ($null -ne $fieldDef.inputParameters) { - $f["inputParameters"] = $fieldDef.inputParameters - } - # folder: true → DataSetFieldFolder (поле-папка для UI-группировки, только dataPath+title) - if ($fieldDef.folder -eq $true) { - $f["folder"] = $true - } - } - - # DataSetFieldFolder — только dataPath + title (для UI-группировки полей в композиторе) - if ($f["folder"]) { - X "$indent<field xsi:type=`"DataSetFieldFolder`">" - X "$indent`t<dataPath>$(Esc-Xml $f.dataPath)</dataPath>" - if ($f.title) { Emit-MLText -tag "title" -text $f.title -indent "$indent`t" } - X "$indent</field>" - return - } - - X "$indent<field xsi:type=`"DataSetFieldField`">" - X "$indent`t<dataPath>$(Esc-Xml $f.dataPath)</dataPath>" - X "$indent`t<field>$(Esc-Xml $f.field)</field>" - - # Title - if ($f.title) { - Emit-MLText -tag "title" -text $f.title -indent "$indent`t" - } - - # UseRestriction - $restrictMap = @{ - "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" - "noGroup" = "group"; "noOrder" = "order" - } - if ($f.restrict.Count -gt 0) { - X "$indent`t<useRestriction>" - foreach ($r in $f.restrict) { - $xmlName = $restrictMap["$r"] - if ($xmlName) { - X "$indent`t`t<$xmlName>true</$xmlName>" - } - } - X "$indent`t</useRestriction>" - } - - # AttributeUseRestriction - if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) { - X "$indent`t<attributeUseRestriction>" - foreach ($r in $f["attrRestrict"]) { - $xmlName = $restrictMap["$r"] - if ($xmlName) { - X "$indent`t`t<$xmlName>true</$xmlName>" - } - } - X "$indent`t</attributeUseRestriction>" - } - - # Role - $hasExtras = $f["roleExtras"] -and $f["roleExtras"].Count -gt 0 - if ($f.roles.Count -gt 0 -or $hasExtras) { - X "$indent`t<role>" - foreach ($role in $f.roles) { - if ($role -eq "period") { - # @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить. - $pnInExtras = $hasExtras -and $f["roleExtras"].Contains('periodNumber') - $ptInExtras = $hasExtras -and $f["roleExtras"].Contains('periodType') - if (-not $pnInExtras) { X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>" } - if (-not $ptInExtras) { X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>" } - } else { - X "$indent`t`t<dcscom:$role>true</dcscom:$role>" - } - } - if ($hasExtras) { - foreach ($k in $f["roleExtras"].Keys) { - X "$indent`t`t<dcscom:$k>$(Esc-Xml "$($f["roleExtras"][$k])")</dcscom:$k>" - } - } - X "$indent`t</role>" - } - - # OrderExpression — после role, до valueType. Допустим массив (multi-sort). - if ($f["orderExpression"]) { - $oeRaw = $f["orderExpression"] - if ($oeRaw -is [System.Collections.IDictionary]) { - $oeList = @($oeRaw) - } elseif ($oeRaw -is [System.Collections.IList]) { - $oeList = $oeRaw - } else { - $oeList = @($oeRaw) - } - foreach ($oe in $oeList) { - $expr = if ($oe.expression) { "$($oe.expression)" } else { '' } - $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' } - $autoOrder = if ($null -ne $oe.autoOrder) { $(if ($oe.autoOrder) { 'true' } else { 'false' }) } else { 'false' } - X "$indent`t<orderExpression>" - X "$indent`t`t<dcscom:expression>$(Esc-Xml $expr)</dcscom:expression>" - X "$indent`t`t<dcscom:orderType>$oType</dcscom:orderType>" - X "$indent`t`t<dcscom:autoOrder>$autoOrder</dcscom:autoOrder>" - X "$indent`t</orderExpression>" - } - } - - # ValueType - if ($f.type) { - X "$indent`t<valueType>" - Emit-ValueType -typeStr $f.type -indent "$indent`t`t" - X "$indent`t</valueType>" - } - - # AvailableValues — list of allowed values with optional multilang presentation - if ($f["availableValues"]) { - foreach ($av in $f["availableValues"]) { - X "$indent`t<availableValue>" - $avVal = $av.value - $avType = if ($av.valueType) { "$($av.valueType)" } else { '' } - if (-not $avType) { - if ($avVal -is [bool]) { $avType = 'xs:boolean' } - elseif ($avVal -is [int] -or $avVal -is [long] -or $avVal -is [double]) { $avType = 'xs:decimal' } - elseif ("$avVal" -match '^\d{4}-\d{2}-\d{2}T') { $avType = 'xs:dateTime' } - else { $avType = 'xs:string' } - } - $avStr = if ($avVal -is [bool]) { "$avVal".ToLower() } else { Esc-Xml "$avVal" } - X "$indent`t`t<value xsi:type=`"$avType`">$avStr</value>" - if ($av.presentation) { - Emit-MLText -tag "presentation" -text $av.presentation -indent "$indent`t`t" - } - X "$indent`t</availableValue>" - } - } - - # Appearance - if ($f.appearance -and $f.appearance.Count -gt 0) { - X "$indent`t<appearance>" - foreach ($key in $f.appearance.Keys) { - $val = $f.appearance[$key] - # ГоризонтальноеПоложение требует специального xsi:type (v8ui:HorizontalAlign), не строка - if ($key -eq "ГоризонтальноеПоложение" -and -not ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject])) { - X "$indent`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - X "$indent`t`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" - X "$indent`t`t`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml "$val")</dcscor:value>" - X "$indent`t`t</dcscor:item>" - } else { - Emit-AppearanceValue -key $key -val $val -indent "$indent`t`t" - } - } - X "$indent`t</appearance>" - } - - # PresentationExpression - if ($f["presentationExpression"]) { - X "$indent`t<presentationExpression>$(Esc-Xml $f["presentationExpression"])</presentationExpression>" - } - - # InputParameters — в конце field - if ($f["inputParameters"]) { - Emit-InputParameters -ip $f["inputParameters"] -indent "$indent`t" - } - - X "$indent</field>" -} - -# === DataSets === -function Emit-DataSet { - param($ds, [string]$indent, [string]$tagName = "dataSet") - - # Determine type - if ($ds.items) { - $dsType = "DataSetUnion" - } elseif ($ds.objectName) { - $dsType = "DataSetObject" - } else { - $dsType = "DataSetQuery" - } - - X "$indent<$tagName xsi:type=`"$dsType`">" - X "$indent`t<name>$(Esc-Xml "$($ds.name)")</name>" - - # Fields - if ($ds.fields) { - foreach ($f in $ds.fields) { - Emit-Field -fieldDef $f -indent "$indent`t" - } - } - - # DataSource (not for Union) - if ($dsType -ne "DataSetUnion") { - $src = if ($ds.source) { "$($ds.source)" } else { $defaultSource } - X "$indent`t<dataSource>$(Esc-Xml $src)</dataSource>" - } - - # Type-specific content - if ($dsType -eq "DataSetQuery") { - $queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir - X "$indent`t<query>$(Esc-Xml $queryText)</query>" - if ($ds.autoFillFields -eq $false) { - X "$indent`t<autoFillFields>false</autoFillFields>" - } - } elseif ($dsType -eq "DataSetObject") { - X "$indent`t<objectName>$(Esc-Xml "$($ds.objectName)")</objectName>" - } elseif ($dsType -eq "DataSetUnion") { - foreach ($item in $ds.items) { - # Union inner items are wrapped as <item xsi:type="..."> - Emit-DataSet -ds $item -indent "$indent`t" -tagName "item" | Out-Null - } - } - - X "$indent</$tagName>" -} - -function Emit-DataSets { - foreach ($ds in $def.dataSets) { - Emit-DataSet -ds $ds -indent "`t" - } -} - -# === DataSetLinks === -function Emit-DataSetLinks { - if (-not $def.dataSetLinks) { return } - foreach ($link in $def.dataSetLinks) { - X "`t<dataSetLink>" - $srcDS = if ($link.source) { "$($link.source)" } elseif ($link.sourceDataSet) { "$($link.sourceDataSet)" } else { "" } - $dstDS = if ($link.dest) { "$($link.dest)" } elseif ($link.destinationDataSet) { "$($link.destinationDataSet)" } else { "" } - $srcEx = if ($link.sourceExpr) { "$($link.sourceExpr)" } elseif ($link.sourceExpression) { "$($link.sourceExpression)" } else { "" } - $dstEx = if ($link.destExpr) { "$($link.destExpr)" } elseif ($link.destinationExpression) { "$($link.destinationExpression)" } else { "" } - X "`t`t<sourceDataSet>$(Esc-Xml $srcDS)</sourceDataSet>" - X "`t`t<destinationDataSet>$(Esc-Xml $dstDS)</destinationDataSet>" - X "`t`t<sourceExpression>$(Esc-Xml $srcEx)</sourceExpression>" - X "`t`t<destinationExpression>$(Esc-Xml $dstEx)</destinationExpression>" - if ($link.parameter) { - X "`t`t<parameter>$(Esc-Xml "$($link.parameter)")</parameter>" - } - if ($link.PSObject.Properties.Match('parameterListAllowed').Count -gt 0 -and $link.parameterListAllowed) { - X "`t`t<parameterListAllowed>true</parameterListAllowed>" - } - if ($link.PSObject.Properties.Match('startExpression').Count -gt 0 -and $null -ne $link.startExpression) { - X "`t`t<startExpression>$(Esc-Xml "$($link.startExpression)")</startExpression>" - } - if ($link.PSObject.Properties.Match('linkConditionExpression').Count -gt 0 -and $null -ne $link.linkConditionExpression) { - X "`t`t<linkConditionExpression>$(Esc-Xml "$($link.linkConditionExpression)")</linkConditionExpression>" - } - X "`t</dataSetLink>" - } -} - -# === CalculatedFields === -function Emit-CalcFields { - if (-not $def.calculatedFields) { return } - $restrictMap = @{ - "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" - "noGroup" = "group"; "noOrder" = "order" - } - foreach ($cf in $def.calculatedFields) { - # Collect dataPath/expression/title/type/restrict/appearance from either - # shorthand string or object form. Object form accepts dataPath/field/name - # as synonyms; useRestriction/restrict accepts object, array, or flag string. - $title = "" - $typeStr = "" - $restrictTokens = @() - $restrictObj = $null - $appearance = $null - - if ($cf -is [string]) { - $parsed = Parse-CalcShorthand $cf - $dataPath = "$($parsed.dataPath)" - $expression = "$($parsed.expression)" - $title = $parsed.title - $typeStr = "$($parsed.type)" - if ($parsed.restrict) { $restrictTokens = @($parsed.restrict) } - } else { - $dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } - elseif ($cf.field) { "$($cf.field)" } - else { "$($cf.name)" } - $expression = "$($cf.expression)" - if ($cf.title) { $title = $cf.title } - if ($cf.type) { $typeStr = Resolve-TypeStr "$($cf.type)" } - - $restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null } - if ($restrictVal) { - if ($restrictVal -is [System.Management.Automation.PSCustomObject] -or $restrictVal -is [hashtable]) { - $restrictObj = $restrictVal - } elseif ($restrictVal -is [string]) { - # Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`) - foreach ($tok in ($restrictVal -split '\s+')) { - $t = $tok.Trim().TrimStart('#') - if ($t) { $restrictTokens += $t } - } - } else { - # Array form: ["noField", "noFilter", ...] - foreach ($r in $restrictVal) { $restrictTokens += "$r" } - } - } - if ($cf.appearance) { $appearance = $cf.appearance } - } - - X "`t<calculatedField>" - X "`t`t<dataPath>$(Esc-Xml $dataPath)</dataPath>" - X "`t`t<expression>$(Esc-Xml $expression)</expression>" - - if ($title) { - Emit-MLText -tag "title" -text $title -indent "`t`t" - } - if ($typeStr) { - X "`t`t<valueType>" - Emit-ValueType -typeStr $typeStr -indent "`t`t`t" - X "`t`t</valueType>" - } - if ($restrictObj -or $restrictTokens.Count -gt 0) { - X "`t`t<useRestriction>" - if ($restrictObj) { - foreach ($prop in $restrictObj.PSObject.Properties) { - if ($prop.Value -eq $true) { - X "`t`t`t<$($prop.Name)>true</$($prop.Name)>" - } - } - } else { - foreach ($r in $restrictTokens) { - $xmlName = $restrictMap["$r"] - if ($xmlName) { X "`t`t`t<$xmlName>true</$xmlName>" } - } - } - X "`t`t</useRestriction>" - } - if ($appearance) { - X "`t`t<appearance>" - foreach ($prop in $appearance.PSObject.Properties) { - # ГоризонтальноеПоложение — особый xsi:type (если не multilang) - if ($prop.Name -eq "ГоризонтальноеПоложение" -and -not ($prop.Value -is [hashtable] -or $prop.Value -is [System.Collections.IDictionary] -or $prop.Value -is [PSCustomObject])) { - X "`t`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - X "`t`t`t`t<dcscor:parameter>$(Esc-Xml $prop.Name)</dcscor:parameter>" - X "`t`t`t`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml "$($prop.Value)")</dcscor:value>" - X "`t`t`t</dcscor:item>" - } else { - Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "`t`t`t" - } - } - X "`t`t</appearance>" - } - - X "`t</calculatedField>" - } -} - -# === TotalFields === -function Emit-TotalFields { - if (-not $def.totalFields) { return } - foreach ($tf in $def.totalFields) { - if ($tf -is [string]) { - $parsed = Parse-TotalShorthand $tf - } else { - $parsed = @{ - dataPath = "$($tf.dataPath)" - expression = "$($tf.expression)" - } - if ($tf.group) { $parsed.groups = @($tf.group) } - } - - X "`t<totalField>" - X "`t`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" - X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>" - if ($parsed.groups) { - foreach ($g in $parsed.groups) { - X "`t`t<group>$(Esc-Xml "$g")</group>" - } - } - X "`t</totalField>" - } -} - -# === Parameters === - -function Emit-SingleParam { - param($p, $parsed) - - X "`t<parameter>" - X "`t`t<name>$(Esc-Xml $parsed.name)</name>" - - # Title (from parsed first, then from object form; accept `presentation` as - # a synonym — 1C UI labels a parameter's caption "Представление"). - $title = "" - if ($parsed.title) { - $title = $parsed.title - } elseif ($p -isnot [string] -and $p.title) { - $title = $p.title - } elseif ($p -isnot [string] -and $p.presentation) { - $title = $p.presentation - } - if ($title) { - Emit-MLText -tag "title" -text $title -indent "`t`t" - } - - # ValueType - if ($parsed.type) { - X "`t`t<valueType>" - Emit-ValueType -typeStr $parsed.type -indent "`t`t`t" - X "`t`t</valueType>" - } - - # Value — for valueListAllowed params Designer omits <value> when empty - $vla = [bool]$parsed.valueListAllowed - # Multi-value (массив значений по умолчанию для valueListAllowed-параметра) — эмитим - # каждый отдельным <value>. Различаем массив значений от composite type (тоже array, - # но в parsed.type). - $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) - if ($parsed.type -is [array] -or $parsed.type -is [System.Collections.IList]) { - # Composite type — Designer writes xsi:nil for any empty composite; - # non-empty composite values are uncommon and would need per-type tagging. - if (Test-EmptyValue $parsed.value) { - if (-not $vla) { X "`t`t<value xsi:nil=`"true`"/>" } - } - } elseif ($parsed.nilValue -eq $true) { - # Принудительный xsi:nil даже когда тип известен (для bit-perfect round-trip). - if (-not $vla) { X "`t`t<value xsi:nil=`"true`"/>" } - } elseif ($valIsArray) { - foreach ($v in @($parsed.value)) { - Emit-ParamValue -type $parsed.type -val $v -indent "`t`t" -valueListAllowed $false - } - } else { - Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla - } - - # Hidden implies useRestriction=true + availableAsField=false - if ($parsed.hidden -eq $true) { - $parsed.availableAsField = $false - $parsed.useRestriction = $true - } - - # UseRestriction — платформа всегда эмитит этот тег у параметра (true/false) - $urEmit = $false - if ($parsed.useRestriction -eq $true) { $urEmit = $true } - elseif ($p -isnot [string] -and $p.useRestriction -eq $true) { $urEmit = $true } - X ("`t`t<useRestriction>" + $(if ($urEmit) { 'true' } else { 'false' }) + "</useRestriction>") - - # Expression - if ($parsed.expression) { - X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>" - } - - # AvailableAsField - if ($parsed.availableAsField -eq $false) { - X "`t`t<availableAsField>false</availableAsField>" - } - - # ValueListAllowed - if ($parsed.valueListAllowed -eq $true) { - X "`t`t<valueListAllowed>true</valueListAllowed>" - } - - # AvailableValues - if ($p -isnot [string] -and $p.availableValues) { - foreach ($av in $p.availableValues) { - X "`t`t<availableValue>" - if (Test-EmptyValue $av.value) { - Emit-EmptyValue -type $parsed.type -indent "`t`t`t" -tagPrefix "" -valueListAllowed $false - } else { - $av_v = $av.value - if ($av_v -is [bool]) { - $bv = "$av_v".ToLower() - X "`t`t`t<value xsi:type=`"xs:boolean`">$bv</value>" - } elseif ($av_v -is [int] -or $av_v -is [long] -or $av_v -is [double]) { - X "`t`t`t<value xsi:type=`"xs:decimal`">$av_v</value>" - } else { - $avVal = "$av_v" - $avType = "xs:string" - if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { - $avType = "dcscor:DesignTimeValue" - } - X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>" - } - } - # `title` accepted as synonym of `presentation` — both map to the same UI label. - $avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" } - if ($avPres) { - Emit-MLText -tag "presentation" -text $avPres -indent "`t`t`t" - } - X "`t`t</availableValue>" - } - } - - # DenyIncompleteValues - $deny = $parsed.denyIncompleteValues -eq $true -or ( - $null -ne $p -and $p -isnot [string] -and $p.denyIncompleteValues -eq $true) - if ($deny) { - X "`t`t<denyIncompleteValues>true</denyIncompleteValues>" - } - - # Use — object form wins, else parsed (set by @autoDates default) - $useVal = $null - if ($null -ne $p -and $p -isnot [string] -and $p.use) { $useVal = "$($p.use)" } - elseif ($parsed.use) { $useVal = "$($parsed.use)" } - if ($useVal) { - X "`t`t<use>$(Esc-Xml $useVal)</use>" - } - - # InputParameters на параметре (ФорматРедактирования и т.п.) - if ($null -ne $p -and $p -isnot [string] -and $p.inputParameters) { - Emit-InputParameters -ip $p.inputParameters -indent "`t`t" - } - - X "`t</parameter>" -} - -$script:allParams = @() - -function Emit-Parameters { - if (-not $def.parameters) { return } - foreach ($p in $def.parameters) { - if ($p -is [string]) { - $parsed = Parse-ParamShorthand $p - } else { - # Composite type: ["string(10,fix)", "CatalogRef.X"] → array of resolved - # strings; emit-valueType handles arrays, empty value falls through to nil. - $resolvedType = "" - if ($p.type) { - if ($p.type -is [array] -or $p.type -is [System.Collections.IList]) { - $resolvedType = @($p.type | ForEach-Object { Resolve-TypeStr "$_" }) - } else { - $resolvedType = Resolve-TypeStr "$($p.type)" - } - } - $parsed = @{ - name = "$($p.name)" - type = $resolvedType - value = $p.value - autoDates = $false - } - if ($p.expression) { $parsed.expression = "$($p.expression)" } - if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false } - if ($p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } - if ($p.hidden -eq $true) { $parsed.hidden = $true } - if ($p.autoDates -eq $true) { $parsed.autoDates = $true } - if ($p.nilValue -eq $true) { $parsed.nilValue = $true } - } - - # @autoDates implies use=Always + denyIncompleteValues=true by default - # (derived &НачалоПериода/&КонецПериода need a populated period). - # Explicit values in object form override these defaults. - if ($parsed.autoDates) { - $isObj = ($p -isnot [string]) -and ($null -ne $p) - if (-not ($isObj -and $null -ne $p.use)) { $parsed.use = 'Always' } - if (-not ($isObj -and $null -ne $p.denyIncompleteValues)) { $parsed.denyIncompleteValues = $true } - } - - Emit-SingleParam -p $p -parsed $parsed - - # Track parameter for auto dataParameters - $script:allParams += @{ name = $parsed.name; hidden = [bool]$parsed.hidden; type = "$($parsed.type)"; value = $parsed.value } - - # @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern). - # type=dateTime + DateFractions=DateTime — иначе КонецПериода обрезается до 00:00:00 - # и запрос `Дата МЕЖДУ &НачалоПериода И &КонецПериода` теряет данные за последний день. - if ($parsed.autoDates) { - $paramName = $parsed.name - $beginParsed = @{ - name = "НачалоПериода"; title = "Начало периода" - type = "dateTime"; value = "0001-01-01T00:00:00" - useRestriction = $true - expression = "&$paramName.ДатаНачала" - } - Emit-SingleParam -p $null -parsed $beginParsed - $endParsed = @{ - name = "КонецПериода"; title = "Конец периода" - type = "dateTime"; value = "0001-01-01T00:00:00" - useRestriction = $true - expression = "&$paramName.ДатаОкончания" - } - Emit-SingleParam -p $null -parsed $endParsed - } - } -} - -function Test-EmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "") { return $true } - if ($s -eq "_") { return $true } - if ($s.ToLowerInvariant() -eq "null") { return $true } - return $false -} - -function Emit-EmptyValue { - param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) - - if ($valueListAllowed) { return } - $t = if ($null -eq $type) { "" } else { "$type" } - # Нормализация: убираем префикс xs: (валидный для valueType из decompile/DSL) - $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } - $pf = $tagPrefix - - if ($t -eq "") { - X "$indent<${pf}value xsi:nil=`"true`"/>" - } elseif ($t -eq "StandardPeriod") { - X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" - X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - X "$indent</${pf}value>" - } elseif ($tBare -match '^string') { - X "$indent<${pf}value xsi:type=`"xs:string`"/>" - } elseif ($tBare -match '^(date|time)') { - X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" - } elseif ($tBare -match '^decimal') { - X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" - } elseif ($tBare -eq "boolean") { - X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" - } else { - # Ref types or unknown — safe nil - X "$indent<${pf}value xsi:nil=`"true`"/>" - } -} - -function Emit-ParamValue { - param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) - - if (Test-EmptyValue $val) { - Emit-EmptyValue -type $type -indent $indent -tagPrefix "" -valueListAllowed $valueListAllowed - return - } - - # val может быть строкой (variant only) или объектом {variant, startDate?, endDate?}. - $valIsDict = ($val -is [hashtable]) -or ($val -is [System.Collections.IDictionary]) -or ($val -is [PSCustomObject]) - $variantStr = $null - $sdStr = $null - $edStr = $null - if ($valIsDict) { - if ($val -is [PSCustomObject]) { - if ($val.PSObject.Properties['variant']) { $variantStr = "$($val.variant)" } - if ($val.PSObject.Properties['startDate']) { $sdStr = "$($val.startDate)" } - if ($val.PSObject.Properties['endDate']) { $edStr = "$($val.endDate)" } - } else { - if ($val.Contains('variant')) { $variantStr = "$($val['variant'])" } - if ($val.Contains('startDate')) { $sdStr = "$($val['startDate'])" } - if ($val.Contains('endDate')) { $edStr = "$($val['endDate'])" } - } - } - $valStr = if ($variantStr) { $variantStr } else { "$val" } - - if ($type -eq "StandardPeriod") { - # Platform-pattern: startDate/endDate эмитятся ТОЛЬКО для variant=Custom. - # Для всех остальных вариантов (ThisMonth, LastYear, Today, ...) — без дат. - X "$indent<value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>" - if ($valStr -eq 'Custom') { - $sdOut = if ($sdStr) { $sdStr } else { '0001-01-01T00:00:00' } - $edOut = if ($edStr) { $edStr } else { '0001-01-01T00:00:00' } - X "$indent`t<v8:startDate>$(Esc-Xml $sdOut)</v8:startDate>" - X "$indent`t<v8:endDate>$(Esc-Xml $edOut)</v8:endDate>" - } - X "$indent</value>" - } elseif ($type -match '^date') { - X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>" - } elseif ($type -eq "boolean") { - X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>" - } elseif ($type -match '^decimal') { - X "$indent<value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</value>" - } elseif ($type -match '^string') { - X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>" - } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { - X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>" - } else { - # Guess from value - if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { - X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>" - } elseif ($valStr -eq "true" -or $valStr -eq "false") { - X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>" - } elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>" - } else { - X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>" - } - } -} - -# === AreaTemplate DSL === - -# Built-in style presets -$script:areaStylePresets = @{ - none = @{ - font = $null; fontSize = $null; bold = $false; italic = $false - hAlign = $null; vAlign = $null; wrap = $false - bgColor = $null; textColor = $null - borderColor = $null; borders = $false - } - data = @{ - font = 'Arial'; fontSize = 10; bold = $false; italic = $false - hAlign = $null; vAlign = $null; wrap = $false - bgColor = 'style:ReportGroup1BackColor'; textColor = $null - borderColor = 'style:ReportLineColor'; borders = $true - } - header = @{ - font = 'Arial'; fontSize = 10; bold = $false; italic = $false - hAlign = 'Center'; vAlign = $null; wrap = $true - bgColor = 'style:ReportHeaderBackColor'; textColor = $null - borderColor = 'style:ReportLineColor'; borders = $true - } - subheader = @{ - font = 'Arial'; fontSize = 10; bold = $false; italic = $false - hAlign = 'Center'; vAlign = $null; wrap = $true - bgColor = $null; textColor = $null - borderColor = 'style:ReportLineColor'; borders = $true - } - total = @{ - font = 'Arial'; fontSize = 10; bold = $false; italic = $false - hAlign = $null; vAlign = $null; wrap = $false - bgColor = $null; textColor = $null - borderColor = 'style:ReportLineColor'; borders = $true - } -} - -# Load user presets from skd-styles.json -# Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/ -$script:userStylesLoaded = $false -$searchPaths = @( - (Join-Path $script:queryBaseDir "skd-styles.json"), - (Join-Path (Get-Location).Path "skd-styles.json") -) -$outResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location).Path $OutputPath } -$scanDir = [System.IO.Path]::GetDirectoryName($outResolved) -while ($scanDir) { - $searchPaths += Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "skd") "skd-styles.json" - $parentDir = Split-Path $scanDir -Parent - if ($parentDir -eq $scanDir) { break } - $scanDir = $parentDir -} -foreach ($stylesFile in $searchPaths) { - if (Test-Path $stylesFile) { - $userStyles = Get-Content -Raw -Encoding UTF8 $stylesFile | ConvertFrom-Json - foreach ($prop in $userStyles.PSObject.Properties) { - $preset = @{} - # Start from 'data' defaults - foreach ($k in $script:areaStylePresets['data'].Keys) { - $preset[$k] = $script:areaStylePresets['data'][$k] - } - # If overriding existing preset, start from it instead - if ($script:areaStylePresets.ContainsKey($prop.Name)) { - foreach ($k in $script:areaStylePresets[$prop.Name].Keys) { - $preset[$k] = $script:areaStylePresets[$prop.Name][$k] - } - } - # Apply user overrides - foreach ($up in $prop.Value.PSObject.Properties) { - $preset[$up.Name] = $up.Value - } - $script:areaStylePresets[$prop.Name] = $preset - } - $script:userStylesLoaded = $true - break - } -} - -function Emit-ColorValue { - param([string]$color, [string]$indent) - # Префиксы style:/web:/win: → соответствующий xmlns + dN:Name - $colorPrefixToUri = @{ - 'style:' = 'http://v8.1c.ru/8.1/data/ui/style' - 'web:' = 'http://v8.1c.ru/8.1/data/ui/colors/web' - 'win:' = 'http://v8.1c.ru/8.1/data/ui/colors/windows' - } - foreach ($pfx in $colorPrefixToUri.Keys) { - if ($color.StartsWith($pfx)) { - $name = $color.Substring($pfx.Length) - $uri = $colorPrefixToUri[$pfx] - X "$indent<dcscor:value xmlns:d8p1=`"$uri`" xsi:type=`"v8ui:Color`">d8p1:$name</dcscor:value>" - return - } - } - X "$indent<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $color)</dcscor:value>" -} - -function Emit-CellAppearance { - param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @()) - $ind = "`t`t`t`t`t`t" - # Если ничего внутри appearance не будет — не эмитим блок вовсе - # (оригинал платформы для cells без атрибутов не пишет <appearance></appearance>). - $hasContent = $style.bgColor -or $style.textColor -or $style.borders -or $style.font -or ` - $style.hAlign -or $style.vAlign -or $style.wrap -or ` - ($width -gt 0) -or ($minHeight -gt 0) -or $vMerge -or $hMerge -or ` - ($extraItems -and @($extraItems).Count -gt 0) - if (-not $hasContent) { return } - X "`t`t`t`t`t<dcsat:appearance>" - # Background color - if ($style.bgColor) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ЦветФона</dcscor:parameter>" - Emit-ColorValue $style.bgColor "$ind`t" - X "$ind</dcscor:item>" - } - # Text color - if ($style.textColor) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ЦветТекста</dcscor:parameter>" - Emit-ColorValue $style.textColor "$ind`t" - X "$ind</dcscor:item>" - } - # Border color + border style (4 sides) - if ($style.borders) { - if ($style.borderColor) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ЦветГраницы</dcscor:parameter>" - Emit-ColorValue $style.borderColor "$ind`t" - X "$ind</dcscor:item>" - } - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>СтильГраницы</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"0`" gap=`"false`">" - X "$ind`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">None</v8ui:style>" - X "$ind`t</dcscor:value>" - foreach ($side in @('Слева','Сверху','Справа','Снизу')) { - X "$ind`t<dcscor:item>" - X "$ind`t`t<dcscor:parameter>СтильГраницы.$side</dcscor:parameter>" - X "$ind`t`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"1`" gap=`"false`">" - X "$ind`t`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>" - X "$ind`t`t</dcscor:value>" - X "$ind`t</dcscor:item>" - } - X "$ind</dcscor:item>" - } - # Font (skip if style has no font configured — for "none" preset) - if ($style.font) { - $boldStr = if ($style.bold) { "true" } else { "false" } - $italicStr = if ($style.italic) { "true" } else { "false" } - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>Шрифт</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"v8ui:Font`" faceName=`"$($style.font)`" height=`"$($style.fontSize)`" bold=`"$boldStr`" italic=`"$italicStr`" underline=`"false`" strikeout=`"false`" kind=`"Absolute`" scale=`"100`"/>" - X "$ind</dcscor:item>" - } - # Horizontal alignment - if ($style.hAlign) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ГоризонтальноеПоложение</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml $style.hAlign)</dcscor:value>" - X "$ind</dcscor:item>" - } - # Vertical alignment - if ($style.vAlign) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ВертикальноеПоложение</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"v8ui:VerticalAlign`">$(Esc-Xml $style.vAlign)</dcscor:value>" - X "$ind</dcscor:item>" - } - # Text placement (wrap) - if ($style.wrap) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>Размещение</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"dcscor:DataCompositionTextPlacementType`">Wrap</dcscor:value>" - X "$ind</dcscor:item>" - } - # Width - if ($width -gt 0) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>МинимальнаяШирина</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>" - X "$ind</dcscor:item>" - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>МаксимальнаяШирина</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>" - X "$ind</dcscor:item>" - } - # Min height - if ($minHeight -gt 0) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>МинимальнаяВысота</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$minHeight</dcscor:value>" - X "$ind</dcscor:item>" - } - # Vertical merge - if ($vMerge) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ОбъединятьПоВертикали</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>" - X "$ind</dcscor:item>" - } - # Horizontal merge - if ($hMerge) { - X "$ind<dcscor:item>" - X "$ind`t<dcscor:parameter>ОбъединятьПоГоризонтали</dcscor:parameter>" - X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>" - X "$ind</dcscor:item>" - } - # Extra appearance items (e.g. drilldown Расшифровка) - foreach ($ei in $extraItems) { X $ei } - X "`t`t`t`t`t</dcsat:appearance>" -} - -# Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}. -# Helpers извлекают значение и эффективный стиль ячейки. -function Get-CellValue { - param($cell) - if ($null -eq $cell) { return $null } - if ($cell -is [string]) { return $cell } - if ($cell -is [hashtable] -or $cell -is [System.Collections.IDictionary]) { - if ($cell.Contains('value')) { return $cell['value'] } - return $cell # multilang dict без обёртки - } - if ($cell.PSObject -and $cell.PSObject.Properties['value']) { return $cell.value } - # PSCustomObject без 'value' — это multilang dict ({ru, en, ...}), отдаём как есть - if ($cell -is [PSCustomObject]) { return $cell } - return $null -} - -function Get-CellStyleOrDefault { - param($cell, $defaultStyle) - if ($null -ne $cell -and -not ($cell -is [string]) -and $cell.PSObject -and $cell.PSObject.Properties['style']) { - $sName = "$($cell.style)" - if ($script:areaStylePresets.ContainsKey($sName)) { - return $script:areaStylePresets[$sName] - } - Write-Warning "Unknown cell style preset '$sName', falling back to template default" - } - return $defaultStyle -} - -function Emit-AreaTemplateDSL { - param($t) - $styleName = if ($t.style) { "$($t.style)" } else { "data" } - if (-not $script:areaStylePresets.ContainsKey($styleName)) { - Write-Warning "Unknown area style preset '$styleName', falling back to 'data'" - $styleName = "data" - } - $style = $script:areaStylePresets[$styleName] - - $rows = @($t.rows) - # PS-quirk: if-expression unwraps single-element @() результат - # (`$x = if (...) { @($arr) }` даёт скаляр при одном элементе). - # Используем обычный if вместо if-expression. - $widths = @() - if ($t.widths) { $widths = @($t.widths) } - $minHeight = if ($t.minHeight) { [double]$t.minHeight } else { 0 } - $colCount = if ($widths.Count -gt 0) { $widths.Count } else { $rows[0].Count } - - # Build vertical merge map: vMerge[row][col] = $true if cell is merged with above - $vMerge = @{} - for ($r = $rows.Count - 1; $r -ge 1; $r--) { - $vMerge[$r] = @{} - for ($c = 0; $c -lt $colCount; $c++) { - $cellValStr = Get-CellValue $rows[$r][$c] - if ($cellValStr -eq '|') { $vMerge[$r][$c] = $true } - } - } - if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} } - - # Build horizontal merge map: hMerge[row][col] = $true if cell is merged with left - $hMerge = @{} - for ($r = 0; $r -lt $rows.Count; $r++) { - $hMerge[$r] = @{} - for ($c = 0; $c -lt $colCount; $c++) { - $cellValStr = Get-CellValue $rows[$r][$c] - if ($cellValStr -eq '>') { $hMerge[$r][$c] = $true } - } - } - - # Build drilldown map: param_name -> drilldown_value (только для shortcut-формы — drilldown:string). - # Форма C (drilldown:object) — DetailsAreaTemplateParameter с произвольным именем, в map не идёт. - $drilldownMap = @{} - if ($t.parameters) { - foreach ($tp in $t.parameters) { - if ($tp.drilldown -and ($tp.drilldown -is [string])) { - $drilldownMap["$($tp.name)"] = "$($tp.drilldown)" - } - } - } - - X "`t<template>" - X "`t`t<name>$(Esc-Xml "$($t.name)")</name>" - X "`t`t<template xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:AreaTemplate`">" - - for ($r = 0; $r -lt $rows.Count; $r++) { - X "`t`t`t<dcsat:item xsi:type=`"dcsat:TableRow`">" - for ($c = 0; $c -lt $colCount; $c++) { - $cellRaw = $rows[$r][$c] - $cellVal = Get-CellValue $cellRaw - $cellStyle = Get-CellStyleOrDefault $cellRaw $style - $w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 } - $isVMerged = $vMerge[$r][$c] -eq $true - $isHMerged = $hMerge[$r][$c] -eq $true - X "`t`t`t`t<dcsat:tableCell>" - if ($isVMerged) { - # Vertically merged cell — only appearance with vMerge flag + width - Emit-CellAppearance $cellStyle $w $true - } elseif ($isHMerged) { - # Horizontally merged cell — only appearance with hMerge flag + width - Emit-CellAppearance $cellStyle $w $false $true - } else { - # Cell value - $cellIsDict = ($cellVal -is [hashtable]) -or ($cellVal -is [System.Collections.IDictionary]) -or ($cellVal -is [PSCustomObject]) - if ($cellIsDict) { - # Multilang static text — эмитим напрямую с lwsTitle-подобной структурой - X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" - Emit-MLText -tag "dcsat:value" -text $cellVal -indent "`t`t`t`t`t`t" - X "`t`t`t`t`t</dcsat:item>" - $cellExtraItems = @() - } elseif ($null -ne $cellVal -and $cellVal -ne '') { - $cellStr = "$cellVal" - # Unescape \| and \> - if ($cellStr -eq '\|') { $cellStr = '|' } - elseif ($cellStr -eq '\>') { $cellStr = '>' } - if ($cellStr -match '^\{(.+)\}$') { - # Parameter reference - $paramName = $Matches[1] - X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" - X "`t`t`t`t`t`t<dcsat:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $paramName)</dcsat:value>" - X "`t`t`t`t`t</dcsat:item>" - # Build drilldown appearance extra items. - # Приоритет: per-cell override (cell={value, drilldown}) → drilldownMap (shortcut form B). - $cellExtraItems = @() - $cellDrillOverride = $null - if ($cellRaw -is [PSCustomObject] -and $cellRaw.PSObject.Properties['drilldown']) { - $cellDrillOverride = "$($cellRaw.drilldown)" - } elseif (($cellRaw -is [hashtable] -or $cellRaw -is [System.Collections.IDictionary]) -and $cellRaw.Contains('drilldown')) { - $cellDrillOverride = "$($cellRaw['drilldown'])" - } - $ddTarget = $null - if ($cellDrillOverride) { - $ddTarget = $cellDrillOverride - } elseif ($drilldownMap.ContainsKey($paramName)) { - $ddTarget = "Расшифровка_$($drilldownMap[$paramName])" - } - if ($ddTarget) { - $cellExtraItems += "`t`t`t`t`t`t<dcscor:item>" - $cellExtraItems += "`t`t`t`t`t`t`t<dcscor:parameter>Расшифровка</dcscor:parameter>" - $cellExtraItems += "`t`t`t`t`t`t`t<dcscor:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $ddTarget)</dcscor:value>" - $cellExtraItems += "`t`t`t`t`t`t</dcscor:item>" - } - } else { - # Static text - X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" - Emit-MLText -tag "dcsat:value" -text $cellStr -indent "`t`t`t`t`t`t" - X "`t`t`t`t`t</dcsat:item>" - } - } - # Appearance - $h = if ($r -eq 0) { $minHeight } else { 0 } - if (-not $cellExtraItems) { $cellExtraItems = @() } - Emit-CellAppearance $cellStyle $w $false $false $h $cellExtraItems - $cellExtraItems = @() - } - X "`t`t`t`t</dcsat:tableCell>" - } - X "`t`t`t</dcsat:item>" - } - - X "`t`t</template>" - # Parameters (reuse existing logic) - if ($t.parameters) { - foreach ($tp in $t.parameters) { - Emit-AreaTemplateParameter -tp $tp -indent "`t`t" - } - } - X "`t</template>" -} - -# Эмиссия одного параметра шаблона. Различает три формы: -# A. { name, expression } → ExpressionAreaTemplateParameter -# B. { name, expression, drilldown: "X" } → Expression + Details(Расшифровка_X, ИмяРесурса, DrillDown) [shortcut] -# C. { name, drilldown: { field, expression, action? } } → DetailsAreaTemplateParameter с произвольным name -function Emit-AreaTemplateParameter { - param($tp, [string]$indent) - # Определяем форму C: drilldown — объект с полем field или expression. - $dd = $tp.drilldown - $ddIsObject = $false - if ($null -ne $dd) { - if ($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) { $ddIsObject = $true } - elseif ($dd -is [PSCustomObject]) { $ddIsObject = $true } - } - if ($ddIsObject) { - # Форма C - $ddField = if ($dd -is [PSCustomObject]) { "$($dd.field)" } else { "$($dd['field'])" } - $ddExpr = if ($dd -is [PSCustomObject]) { "$($dd.expression)" } else { "$($dd['expression'])" } - $ddActV = $null - if ($dd -is [PSCustomObject] -and $dd.PSObject.Properties['action']) { $ddActV = "$($dd.action)" } - elseif (($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) -and $dd.Contains('action')) { $ddActV = "$($dd['action'])" } - $ddAct = if ($ddActV) { $ddActV } else { 'DrillDown' } - X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" - X "$indent`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>" - X "$indent`t<dcsat:fieldExpression>" - X "$indent`t`t<dcsat:field>$(Esc-Xml $ddField)</dcsat:field>" - X "$indent`t`t<dcsat:expression>$(Esc-Xml $ddExpr)</dcsat:expression>" - X "$indent`t</dcsat:fieldExpression>" - X "$indent`t<dcsat:mainAction>$(Esc-Xml $ddAct)</dcsat:mainAction>" - X "$indent</parameter>" - return - } - # Форма A или B - X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:ExpressionAreaTemplateParameter`">" - X "$indent`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>" - X "$indent`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>" - X "$indent</parameter>" - if ($dd -and ($dd -is [string])) { - # Форма B: shortcut Расшифровка_<X> + ИмяРесурса + DrillDown - $ddVal = "$dd" - X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" - X "$indent`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>" - X "$indent`t<dcsat:fieldExpression>" - X "$indent`t`t<dcsat:field>ИмяРесурса</dcsat:field>" - X "$indent`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>" - X "$indent`t</dcsat:fieldExpression>" - X "$indent`t<dcsat:mainAction>DrillDown</dcsat:mainAction>" - X "$indent</parameter>" - } -} - -# === Templates === -function Emit-Templates { - if (-not $def.templates) { return } - foreach ($t in $def.templates) { - if ($t.rows) { - # Compact DSL mode - Emit-AreaTemplateDSL $t - } else { - # Raw XML mode - X "`t<template>" - X "`t`t<name>$(Esc-Xml "$($t.name)")</name>" - if ($t.template) { - X "`t`t$($t.template)" - } - if ($t.parameters) { - foreach ($tp in $t.parameters) { - Emit-AreaTemplateParameter -tp $tp -indent "`t`t" - } - } - X "`t</template>" - } - } -} - -# === FieldTemplates === -# Привязка <fieldTemplate><field/><template/></fieldTemplate> поля к именованному area-template. -# DSL: "fieldTemplates": [{ "field": "X", "template": "Макет1" }, ...] -function Emit-FieldTemplates { - if (-not $def.fieldTemplates) { return } - foreach ($ft in $def.fieldTemplates) { - X "`t<fieldTemplate>" - X "`t`t<field>$(Esc-Xml "$($ft.field)")</field>" - X "`t`t<template>$(Esc-Xml "$($ft.template)")</template>" - X "`t</fieldTemplate>" - } -} - -# === GroupTemplates === -function Emit-GroupTemplates { - if (-not $def.groupTemplates) { return } - foreach ($gt in $def.groupTemplates) { - $ttype = if ($gt.templateType) { "$($gt.templateType)" } else { "Header" } - $isHeader = ($ttype -eq 'GroupHeader') - $tag = if ($isHeader) { 'groupHeaderTemplate' } else { 'groupTemplate' } - $xmlTType = if ($isHeader) { 'Header' } else { $ttype } - - X "`t<$tag>" - if ($gt.groupName) { - X "`t`t<groupName>$(Esc-Xml "$($gt.groupName)")</groupName>" - } elseif ($gt.groupField) { - X "`t`t<groupField>$(Esc-Xml "$($gt.groupField)")</groupField>" - } - X "`t`t<templateType>$(Esc-Xml $xmlTType)</templateType>" - X "`t`t<template>$(Esc-Xml "$($gt.template)")</template>" - X "`t</$tag>" - } -} - -# === Settings Variants === - -function Emit-SelectionItem { - param($item, [string]$indent) - if ($item -is [string]) { - if ($item -eq "Auto") { - X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" - } else { - X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" - X "$indent`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>" - X "$indent</dcsset:item>" - } - return - } - # Object form: { auto: true, use: false } — отключённый Auto в selection - if ($item.auto -eq $true) { - X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`">" - if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } - X "$indent</dcsset:item>" - return - } - if ($item.folder -or (Has-JsonProp $item 'folder')) { - X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">" - # Optional <dcsset:field> на folder (редкий случай, для round-trip-целостности) - if ($item.field) { - X "$indent`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" - } - Emit-MLText -tag "dcsset:lwsTitle" -text $item.folder -indent "$indent`t" -NoXsiType - foreach ($sub in $item.items) { - Emit-SelectionItem -item $sub -indent "$indent`t" - } - $pl = if ($item.placement) { "$($item.placement)" } else { 'Auto' } - X "$indent`t<dcsset:placement>$(Esc-Xml $pl)</dcsset:placement>" - X "$indent</dcsset:item>" - return - } - # field with optional title / use=false / viewMode - X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" - if ($item.use -eq $false) { - X "$indent`t<dcsset:use>false</dcsset:use>" - } - X "$indent`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" - if ($item.title) { - Emit-MLText -tag "dcsset:lwsTitle" -text $item.title -indent "$indent`t" -NoXsiType - } - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - X "$indent</dcsset:item>" -} - -function Emit-Selection { - param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) - - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - - X "$indent<dcsset:selection>" - foreach ($item in $items) { - if ($skipAuto -and ($item -is [string]) -and $item -eq 'Auto') { continue } - Emit-SelectionItem -item $item -indent "$indent`t" - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - X "$indent</dcsset:selection>" -} - -function Emit-FilterItem { - param($item, [string]$indent) - - if ($item.group) { - # FilterItemGroup - $groupType = switch ("$($item.group)") { - "And" { "AndGroup" } - "Or" { "OrGroup" } - "Not" { "NotGroup" } - default { "$($item.group)Group" } - } - X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" - X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>" - if ($item.items) { - foreach ($sub in $item.items) { - if ($sub -is [string]) { - $parsed = Parse-FilterShorthand $sub - $obj = @{ field = $parsed.field; op = $parsed.op } - if ($parsed.use -eq $false) { $obj.use = $false } - if ($null -ne $parsed.value) { $obj.value = $parsed.value } - if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } - if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } - if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } - $sub = [pscustomobject]$obj - } - Emit-FilterItem -item $sub -indent "$indent`t" - } - } - if ($item.presentation) { - Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" - } - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - if ($item.userSettingID) { - $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>" - } - if ($item.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" - } - X "$indent</dcsset:item>" - return - } - - # FilterItemComparison - X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" - - if ($item.use -eq $false) { - X "$indent`t<dcsset:use>false</dcsset:use>" - } - - X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>" - - $compType = $script:comparisonTypes["$($item.op)"] - if (-not $compType) { $compType = "$($item.op)" } - X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>" - - # Right value: один, несколько (InList) или ValueListType (пустой list-placeholder) - $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) - if ($valIsArray) { - # Пустой массив → пустой ValueListType placeholder - if (@($item.value).Count -eq 0) { - X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">" - X "$indent`t`t<v8:valueType/>" - X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>" - X "$indent`t</dcsset:right>" - } else { - # Несколько <right> подряд (multi-value InList) - foreach ($v in $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - if ($v -is [bool]) { $vt = 'xs:boolean' } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } - elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } - elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } - elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } - else { $vt = 'xs:string' } - } - $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } - X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" - } - } - } elseif ($null -ne $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - $v = $item.value - if ($v -is [bool]) { - $vt = "xs:boolean" - } elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { - $vt = "xs:decimal" - } elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { - $vt = "xs:dateTime" - } elseif ("$v" -match '^-?\d+(\.\d+)?$') { - $vt = "xs:decimal" - } elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - $vt = "dcscor:DesignTimeValue" - } else { - $vt = "xs:string" - } - } - $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } - X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" - } - - if ($item.presentation) { - Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" - } - - # viewMode эмитим только если явно задан — присутствие в XML контекстно - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - - if ($item.userSettingID) { - $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - - if ($item.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" - } - - X "$indent</dcsset:item>" -} - -function Emit-Filter { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) - - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - - X "$indent<dcsset:filter>" - foreach ($item in $items) { - if ($item -is [string]) { - # Parse shorthand: "Организация = _ @off @user" - $parsed = Parse-FilterShorthand $item - $filterObj = New-Object PSObject - $filterObj | Add-Member -NotePropertyName "field" -NotePropertyValue $parsed.field - $filterObj | Add-Member -NotePropertyName "op" -NotePropertyValue $parsed.op - if ($parsed.use -eq $false) { - $filterObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false - } - if ($null -ne $parsed.value) { - $filterObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value - } - if ($parsed["valueType"]) { - $filterObj | Add-Member -NotePropertyName "valueType" -NotePropertyValue $parsed["valueType"] - } - if ($parsed.userSettingID) { - $filterObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID - } - if ($parsed.viewMode) { - $filterObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode - } - Emit-FilterItem -item $filterObj -indent "$indent`t" - } else { - Emit-FilterItem -item $item -indent "$indent`t" - } - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - X "$indent</dcsset:filter>" -} - -function Emit-Order { - param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) - - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - - X "$indent<dcsset:order>" - foreach ($item in $items) { - if ($item -is [string]) { - if ($item -eq "Auto") { - if (-not $skipAuto) { - X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" - } - } else { - $parts = $item -split '\s+' - $field = $parts[0] - $dir = "Asc" - if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)desc$') { $dir = "Desc" } - elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)asc$') { $dir = "Asc" } - X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" - X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" - X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" - X "$indent`t</dcsset:item>" - } - } else { - # Object form: { field, direction, viewMode } - if ($item.field -eq "Auto" -or $item.type -eq "auto") { - if (-not $skipAuto) { - X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" - } - continue - } - $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } - if ($dir -match '^(?i)desc$') { $dir = "Desc" } elseif ($dir -match '^(?i)asc$') { $dir = "Asc" } - X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" - if ($item.use -eq $false) { - X "$indent`t`t<dcsset:use>false</dcsset:use>" - } - X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" - X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" - if ($item.viewMode) { - X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - X "$indent`t</dcsset:item>" - } - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - X "$indent</dcsset:order>" -} - -function Emit-AppearanceValue { - param([string]$key, $val, [string]$indent) - - X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - - # Helper для проверки property/key на PSCustomObject/IDictionary - function _HasKey { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } - if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } - return $false - } - function _Get { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return $o.$k } - if ($o -is [System.Collections.IDictionary]) { return $o[$k] } - return $null - } - - # Распознаём wrapper {value:..., use?:false, items?:{}}. - # Top-level Line-value хранится плоско ({@type:Line, width, gap, style, use?, items?}) — - # отличаем от wrapper по наличию @type на самом val. - $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') - $useWrapper = $false - $innerVal = $val - $nestedItems = $null - if ($isTopLevelLine) { - # items/use лежат рядом с @type - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { - # Обычный wrapper {value, use?, items?} - $innerVal = (_Get $val 'value') - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } - - if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" } - X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" - - # Font dict ({@type: "Font", ref, faceName, height, bold, ...}) → <dcscor:value xsi:type="v8ui:Font" .../> - $isFontDict = $false - if ($innerVal -is [PSCustomObject]) { - $tProp = $innerVal.PSObject.Properties['@type'] - if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } - } elseif ($innerVal -is [System.Collections.IDictionary]) { - if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } - } - # Line dict ({@type: "Line", width, gap, style}) → <dcscor:value xsi:type="v8ui:Line" ...><v8ui:style>... - $isLineDict = $false - if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } - $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) - if ($isLineDict) { - $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } - $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } - $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } - X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">" - X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>" - X "$indent`t</dcscor:value>" - } elseif ($isFontDict) { - $attrParts = @() - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $null - if ($innerVal -is [PSCustomObject]) { - $ap = $innerVal.PSObject.Properties[$attrName] - if ($ap) { $av = $ap.Value } - } else { - if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } - } - if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } - } - X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" - } elseif ($isDict) { - # Multilang dict ({"ru": "...", "en": "..."}) → LocalStringType независимо от ключа. - Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" - } else { - $actualVal = "$innerVal" - # Параметр-специфичный тип для известных appearance keys - $keyTypeMap = @{ - 'Размещение' = 'dcscor:DataCompositionTextPlacementType' - 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' - 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' - 'ОриентацияТекста' = 'xs:decimal' - 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' - 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' - } - $keyType = $keyTypeMap[$key] - if ($keyType) { - X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" - } elseif ($actualVal -match '^(style|web|win):') { - # Внутри <dcsset:settings> префиксы style:/web:/win:/sys: уже объявлены на корне, - # локальный xmlns не нужен — эмитим short form. - X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" - } elseif ($actualVal -eq "true" -or $actualVal -eq "false") { - X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" - } elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { - # Строковые ключи, традиционно эмитятся как LocalStringType (даже если только ru). - Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" - } elseif ($actualVal -match '^-?\d+(\.\d+)?$') { - # Number → xs:decimal (МинимальнаяШирина=40, ОриентацияТекста и т.п. — но не key-typed) - X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" - } elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { - # Color без явного префикса (auto, #FFC8C8) - X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" - } else { - X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" - } - } - # Nested SettingsParameterValue items (например СтильГраницы.Сверху/.Снизу/.Слева/.Справа). - # Эмитим как siblings <dcscor:item> внутри родительского <dcscor:item>. - if ($nestedItems) { - $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } - if ($niProps) { - foreach ($np in $niProps) { - Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" - } - } elseif ($nestedItems -is [System.Collections.IDictionary]) { - foreach ($nk in $nestedItems.Keys) { - Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" - } - } - } - X "$indent</dcscor:item>" -} - -function Emit-ConditionalAppearance { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) - - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - - X "$indent<dcsset:conditionalAppearance>" - foreach ($ca in $items) { - X "$indent`t<dcsset:item>" - - # use=false — отключённое правило (эмитим до selection — XML-порядок) - if ($ca.use -eq $false) { - X "$indent`t`t<dcsset:use>false</dcsset:use>" - } - - # Selection (which fields to apply to; empty = all) - if ($ca.selection -and $ca.selection.Count -gt 0) { - X "$indent`t`t<dcsset:selection>" - foreach ($sel in $ca.selection) { - X "$indent`t`t`t<dcsset:item>" - X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>" - X "$indent`t`t`t</dcsset:item>" - } - X "$indent`t`t</dcsset:selection>" - } else { - X "$indent`t`t<dcsset:selection/>" - } - - # Filter (reuse existing Emit-Filter logic) - if ($ca.filter -and $ca.filter.Count -gt 0) { - Emit-Filter -items $ca.filter -indent "$indent`t`t" - } else { - # Платформа эмитит пустой <dcsset:filter/> на каждом condApp item - X "$indent`t`t<dcsset:filter/>" - } - - # Appearance (parameter-value pairs) - if ($ca.appearance) { - X "$indent`t`t<dcsset:appearance>" - foreach ($prop in $ca.appearance.PSObject.Properties) { - Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" - } - X "$indent`t`t</dcsset:appearance>" - } - - # Presentation - if ($ca.presentation) { - # Multilang dict {ru, en, ...} → LocalStringType; иначе — xs:string - if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { - Emit-MLText -tag "dcsset:presentation" -text $ca.presentation -indent "$indent`t`t" - } else { - X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" - } - } - - if ($ca.viewMode) { - X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" - } - - # UserSettingID - if ($ca.userSettingID) { - $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } - X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - - if ($ca.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $ca.userSettingPresentation -indent "$indent`t`t" - } - - # useInXxx — список областей где правило НЕ применяется (DontUse). - # Порядок имитирует платформенный (group → hierarchicalGroup → overall → ...). - if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { - $useInOrder = @('group','hierarchicalGroup','overall', - 'fieldsHeader','header','parameters','filter', - 'resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') - $set = @{} - foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } - foreach ($n in $useInOrder) { - if ($set.ContainsKey($n)) { - $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) - X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>" - } - } - } - - X "$indent`t</dcsset:item>" - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - X "$indent</dcsset:conditionalAppearance>" -} - -# Эмиссия nested sub-item внутри SettingsParameterValue (chart-параметры типа -# ТипДиаграммы.СоединениеЗначенийПоСериям). Поддерживает use=false и valueType -# либо строкой ("xs:string", "dN:Foo" если префикс известен в корне), либо -# объектом {uri, name} — эмитим локальный xmlns на value. -function Emit-OutputParametersSubItem { - param([string]$subName, $subWrap, [string]$indent) - $subVal = $subWrap - $subVT = 'xs:string' - $subUseFalse = $false - $subUri = $null - $subLocalName = $null - if ($subWrap -is [PSCustomObject]) { - if ($subWrap.PSObject.Properties['value']) { $subVal = $subWrap.value } - if ($subWrap.PSObject.Properties['valueType']) { - $vt = $subWrap.valueType - if ($vt -is [PSCustomObject] -and $vt.PSObject.Properties['uri']) { - $subUri = "$($vt.uri)"; $subLocalName = "$($vt.name)" - } elseif ($vt -is [System.Collections.IDictionary] -and $vt.Contains('uri')) { - $subUri = "$($vt['uri'])"; $subLocalName = "$($vt['name'])" - } else { - $subVT = "$vt" - } - } - if ($subWrap.PSObject.Properties['use'] -and $subWrap.use -eq $false) { $subUseFalse = $true } - } elseif ($subWrap -is [System.Collections.IDictionary]) { - if ($subWrap.Contains('value')) { $subVal = $subWrap['value'] } - if ($subWrap.Contains('valueType')) { - $vt = $subWrap['valueType'] - if ($vt -is [PSCustomObject] -and $vt.PSObject.Properties['uri']) { - $subUri = "$($vt.uri)"; $subLocalName = "$($vt.name)" - } elseif ($vt -is [System.Collections.IDictionary] -and $vt.Contains('uri')) { - $subUri = "$($vt['uri'])"; $subLocalName = "$($vt['name'])" - } else { - $subVT = "$vt" - } - } - if ($subWrap.Contains('use') -and $subWrap['use'] -eq $false) { $subUseFalse = $true } - } - X "$indent`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - if ($subUseFalse) { X "$indent`t`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t`t<dcscor:parameter>$(Esc-Xml $subName)</dcscor:parameter>" - if ($subUri) { - X "$indent`t`t`t<dcscor:value xmlns:dN=`"$subUri`" xsi:type=`"dN:$subLocalName`">$(Esc-Xml "$subVal")</dcscor:value>" - } else { - X "$indent`t`t`t<dcscor:value xsi:type=`"$subVT`">$(Esc-Xml "$subVal")</dcscor:value>" - } - X "$indent`t`t</dcscor:item>" -} - -function Emit-OutputParameters { - param($params, [string]$indent, $blockViewMode = $null) - - if (-not $params) { return } - - X "$indent<dcsset:outputParameters>" - foreach ($prop in $params.PSObject.Properties) { - $key = $prop.Name - $rawVal = $prop.Value - # Распознаём wrapper {value, valueType?, use?, items?, viewMode?, userSettingID?, userSettingPresentation?} - # отличая от multilang dict ({ru, en, ...}). Wrapper всегда имеет ключ 'value'. - $useFalse = $false - $wrapVM = $null - $wrapUSID = $null - $wrapUSP = $null - $wrapVT = $null - $wrapItems = $null - $hasValueKey = $false - if ($rawVal -is [PSCustomObject] -and $rawVal.PSObject.Properties['value']) { - $hasValueKey = $true - if ($rawVal.PSObject.Properties['valueType']) { $wrapVT = "$($rawVal.valueType)" } - if ($rawVal.PSObject.Properties['use'] -and $rawVal.use -eq $false) { $useFalse = $true } - if ($rawVal.PSObject.Properties['items']) { $wrapItems = $rawVal.items } - if ($rawVal.PSObject.Properties['viewMode']) { $wrapVM = "$($rawVal.viewMode)" } - if ($rawVal.PSObject.Properties['userSettingID']) { $wrapUSID = "$($rawVal.userSettingID)" } - if ($rawVal.PSObject.Properties['userSettingPresentation']) { $wrapUSP = $rawVal.userSettingPresentation } - $rawVal = $rawVal.value - } elseif (($rawVal -is [hashtable] -or $rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('value')) { - $hasValueKey = $true - if ($rawVal.Contains('valueType')) { $wrapVT = "$($rawVal['valueType'])" } - if ($rawVal.Contains('use') -and $rawVal['use'] -eq $false) { $useFalse = $true } - if ($rawVal.Contains('items')) { $wrapItems = $rawVal['items'] } - if ($rawVal.Contains('viewMode')) { $wrapVM = "$($rawVal['viewMode'])" } - if ($rawVal.Contains('userSettingID')) { $wrapUSID = "$($rawVal['userSettingID'])" } - if ($rawVal.Contains('userSettingPresentation')) { $wrapUSP = $rawVal['userSettingPresentation'] } - $rawVal = $rawVal['value'] - } - # Font dict внутри значения - $isFontDict = $false - if ($rawVal -is [PSCustomObject]) { - $tProp = $rawVal.PSObject.Properties['@type'] - if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } - } elseif ($rawVal -is [System.Collections.IDictionary]) { - if ($rawVal.Contains('@type') -and "$($rawVal['@type'])" -eq 'Font') { $isFontDict = $true } - } - # Приоритет: явный wrapVT > известный тип ключа > xs:string - if ($wrapVT) { $ptype = $wrapVT } - else { - $ptype = $script:outputParamTypes[$key] - if (-not $ptype) { $ptype = "xs:string" } - } - # Auto-promote to mltext if value is a multilang dict (но не Font/wrapper) - if (-not $isFontDict -and ($rawVal -is [System.Management.Automation.PSCustomObject] -or $rawVal -is [hashtable] -or $rawVal -is [System.Collections.IDictionary])) { - $ptype = "mltext" - } - - X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - if ($useFalse) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" - if ($isFontDict) { - $attrParts = @() - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $null - if ($rawVal -is [PSCustomObject]) { - $ap = $rawVal.PSObject.Properties[$attrName] - if ($ap) { $av = $ap.Value } - } else { - if ($rawVal.Contains($attrName)) { $av = $rawVal[$attrName] } - } - if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } - } - X "$indent`t`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" - } elseif ($ptype -eq "mltext") { - Emit-MLText -tag "dcscor:value" -text $rawVal -indent "$indent`t`t" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml "$rawVal")</dcscor:value>" - } - # Nested sub-параметры (ТипДиаграммы.ВидПодписей и т.п.) — эмитим между value и extras. - # valueType: строка → xsi:type=string, объект {uri, name} → локальный xmlns:dN + xsi:type=dN:name. - if ($wrapItems) { - $itemProps = if ($wrapItems -is [PSCustomObject]) { $wrapItems.PSObject.Properties } else { $null } - if ($itemProps) { - foreach ($ip in $itemProps) { - Emit-OutputParametersSubItem -subName $ip.Name -subWrap $ip.Value -indent $indent - } - } elseif ($wrapItems -is [System.Collections.IDictionary]) { - foreach ($k in $wrapItems.Keys) { - Emit-OutputParametersSubItem -subName $k -subWrap $wrapItems[$k] -indent $indent - } - } - } - if ($wrapVM) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml $wrapVM)</dcsset:viewMode>" } - if ($wrapUSID) { - $uid = if ("$wrapUSID" -eq 'auto') { New-Guid-String } else { "$wrapUSID" } - X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - if ($wrapUSP) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $wrapUSP -indent "$indent`t`t" - } - X "$indent`t</dcscor:item>" - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - X "$indent</dcsset:outputParameters>" -} - -function Emit-DataParameters { - param($items, [string]$indent, $blockViewMode = $null) - - if (-not $items -or $items.Count -eq 0) { return } - - X "$indent<dcsset:dataParameters>" - foreach ($dp in $items) { - # Support string shorthand - if ($dp -is [string]) { - $parsed = Parse-DataParamShorthand $dp - $dpObj = New-Object PSObject - $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter - if ($null -ne $parsed.value) { - $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value - } - if ($parsed.use -eq $false) { - $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false - } - if ($parsed.userSettingID) { - $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID - } - if ($parsed.viewMode) { - $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode - } - $dp = $dpObj - } - - X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - - if ($dp.use -eq $false) { - X "$indent`t`t<dcscor:use>false</dcscor:use>" - } - - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" - - # Value - if ($dp.nilValue -eq $true) { - X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" - } elseif (Test-EmptyValue $dp.value) { - Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false - } elseif ($null -ne $dp.value) { - $vtype = "$($dp.valueType)" - if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable]) -and ($dp.value.variant)) { - # Standard{Period,BeginningDate} — различаем по форме value: - # {variant, date} → SBD - # {variant, startDate, endDate} → SP с датами - # {variant} only → инференс по имени (BeginningOf* → SBD, иначе SP) - $_hasDate = $false; $_hasSD = $false - if ($dp.value -is [PSCustomObject]) { - $_hasDate = [bool]$dp.value.PSObject.Properties['date'] - $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] - } else { - $_hasDate = $dp.value.Contains('date') - $_hasSD = $dp.value.Contains('startDate') - } - $_variantStr = "$($dp.value.variant)" - $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') - if ($_isSBD) { - $_d = $null - if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } - elseif ($dp.value -is [System.Collections.IDictionary] -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { - if (-not $_d) { $_d = '0001-01-01T00:00:00' } - X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" - } - X "$indent`t`t</dcscor:value>" - } else { - # StandardPeriod — platform-pattern: startDate/endDate ТОЛЬКО для variant=Custom. - $_sd = $null; $_ed = $null - if ($dp.value -is [PSCustomObject]) { - if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" } - if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } - } else { - if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" } - if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } - } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { - if (-not $_sd) { $_sd = '0001-01-01T00:00:00' } - if (-not $_ed) { $_ed = '0001-01-01T00:00:00' } - X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>" - X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif ($vtype -match '^[a-zA-Z]+:') { - # Полный xsi:type из decompile (например "xs:boolean", "dcscor:DesignTimeValue"). - $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } - X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" - } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { - $bv = "$($dp.value)".ToLower() - X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $bv)</dcscor:value>" - } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^decimal') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^string') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } - } - - if ($dp.viewMode) { - X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" - } - - if ($dp.userSettingID) { - $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } - X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - - if ($dp.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t" - } - - X "$indent`t</dcscor:item>" - } - if ($null -ne $blockViewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" - } - X "$indent</dcsset:dataParameters>" -} - -# === Structure items (recursive) === - -function Emit-GroupItems { - param($groupBy, [string]$indent) - - if (-not $groupBy -or $groupBy.Count -eq 0) { return } - - X "$indent<dcsset:groupItems>" - foreach ($field in $groupBy) { - if ($field -is [string]) { - if ($field -eq 'Auto') { - # Auto-группировка (по аналогии с "Auto" в selection) - X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemAuto`"/>" - continue - } - X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" - X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" - X "$indent`t`t<dcsset:groupType>Items</dcsset:groupType>" - X "$indent`t`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" - X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" - X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" - X "$indent`t</dcsset:item>" - } else { - # Object form - X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" - X "$indent`t`t<dcsset:field>$(Esc-Xml "$($field.field)")</dcsset:field>" - $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } - X "$indent`t`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>" - $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } - X "$indent`t`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>" - # Auto-detect: ISO date → xs:dateTime, иначе → dcscor:Field (path). - $pab = if ($field.periodAdditionBegin) { "$($field.periodAdditionBegin)" } else { '0001-01-01T00:00:00' } - $pae = if ($field.periodAdditionEnd) { "$($field.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } - $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } - $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } - X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>" - X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>" - X "$indent`t</dcsset:item>" - } - } - X "$indent</dcsset:groupItems>" -} - -# Parse structure string shorthand: "Организация > Номенклатура > details" -function Parse-StructureShorthand { - param([string]$s) - - $segments = $s -split '\s*>\s*' - $result = @() - - # Build nested groups from right to left - $innermost = $null - for ($i = $segments.Count - 1; $i -ge 0; $i--) { - $seg = $segments[$i].Trim() - $group = New-Object PSObject - $group | Add-Member -NotePropertyName "type" -NotePropertyValue "group" - - if ($seg -match '^(?i)(details|детали)$') { - # Empty groupBy = detailed records - $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @() - } elseif ($seg -match '^(.+)\[(.+)\]$') { - # Named group: "ИмяГруппы[Поле]" - $group | Add-Member -NotePropertyName "name" -NotePropertyValue $Matches[1].Trim() - $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($Matches[2].Trim()) - } else { - $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg) - } - - # Платформа в каждую группировку кладёт авто-поле выбора и авто-порядок; - # shorthand должен соответствовать ручному добавлению группировки в конфигураторе. - $group | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto") - $group | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto") - - if ($null -ne $innermost) { - $group | Add-Member -NotePropertyName "children" -NotePropertyValue @($innermost) - } - $innermost = $group - } - - if ($innermost) { $result += $innermost } - return ,$result -} - -function Emit-UserFields { - param($items, [string]$indent) - if (-not $items -or $items.Count -eq 0) { return } - X "$indent<dcsset:userFields>" - foreach ($uf in $items) { - # Type detection: cases → UserFieldCase, otherwise UserFieldExpression - $uType = if ($uf.cases) { "UserFieldCase" } else { "UserFieldExpression" } - X "$indent`t<dcsset:item xsi:type=`"dcsset:$uType`">" - if ($uf.dataPath) { - X "$indent`t`t<dcsset:dataPath>$(Esc-Xml "$($uf.dataPath)")</dcsset:dataPath>" - } - if ($uf.title) { - Emit-MLText -tag "dcsset:lwsTitle" -text $uf.title -indent "$indent`t`t" -NoXsiType - } - if ($uType -eq "UserFieldExpression") { - if ($uf.detail) { - if ($uf.detail.PSObject.Properties.Match('expression').Count -gt 0) { - $_v = "$($uf.detail.expression)" - if ($_v) { X "$indent`t`t<dcsset:detailExpression>$(Esc-Xml $_v)</dcsset:detailExpression>" } - else { X "$indent`t`t<dcsset:detailExpression/>" } - } - if ($uf.detail.PSObject.Properties.Match('presentation').Count -gt 0) { - $_v = "$($uf.detail.presentation)" - if ($_v) { X "$indent`t`t<dcsset:detailExpressionPresentation>$(Esc-Xml $_v)</dcsset:detailExpressionPresentation>" } - else { X "$indent`t`t<dcsset:detailExpressionPresentation/>" } - } - } - if ($uf.total) { - if ($uf.total.PSObject.Properties.Match('expression').Count -gt 0) { - $_v = "$($uf.total.expression)" - if ($_v) { X "$indent`t`t<dcsset:totalExpression>$(Esc-Xml $_v)</dcsset:totalExpression>" } - else { X "$indent`t`t<dcsset:totalExpression/>" } - } - if ($uf.total.PSObject.Properties.Match('presentation').Count -gt 0) { - $_v = "$($uf.total.presentation)" - if ($_v) { X "$indent`t`t<dcsset:totalExpressionPresentation>$(Esc-Xml $_v)</dcsset:totalExpressionPresentation>" } - else { X "$indent`t`t<dcsset:totalExpressionPresentation/>" } - } - } - } else { - # UserFieldCase - if ($uf.cases.Count -eq 0) { - X "$indent`t`t<dcsset:cases/>" - } else { - X "$indent`t`t<dcsset:cases>" - foreach ($c in $uf.cases) { - X "$indent`t`t`t<dcsset:item>" - if ($c.filter) { - Emit-Filter -items $c.filter -indent "$indent`t`t`t`t" - } - if ($null -ne $c.value) { - $cv = $c.value - if ($cv -is [bool]) { - X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:boolean`">$(("$cv").ToLower())</dcsset:value>" - } elseif ($cv -is [int] -or $cv -is [long] -or $cv -is [double]) { - X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:decimal`">$cv</dcsset:value>" - } else { - X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:string`">$(Esc-Xml "$cv")</dcsset:value>" - } - } - if ($c.presentation) { - Emit-MLText -tag "dcsset:lwsPresentationValue" -text $c.presentation -indent "$indent`t`t`t`t" -NoXsiType - } - X "$indent`t`t`t</dcsset:item>" - } - X "$indent`t`t</dcsset:cases>" - } - } - X "$indent`t</dcsset:item>" - } - X "$indent</dcsset:userFields>" -} - -# Shared emitter for table column/row and chart point/series. -# Emits name?, groupItems, filter, order, selection, outputParameters, viewMode?, -# userSettingID?, userSettingPresentation? — каждое условно по присутствию в JSON. -# Параметр $emitName управляет тем, эмитить ли <name> внутри блока: для row caller -# уже эмитит name отдельно (исторический порядок), для остальных — здесь. -function Emit-TableAxisBlock { - param($block, [string]$indent, [bool]$emitName = $true) - if ($emitName -and $block.name) { - X "$indent<dcsset:name>$(Esc-Xml "$($block.name)")</dcsset:name>" - } - $gb = if ($block.groupBy) { $block.groupBy } else { $block.groupFields } - Emit-GroupItems -groupBy $gb -indent $indent - if ($block.filter) { - Emit-Filter -items $block.filter -indent $indent - } - if ($block.order) { - Emit-Order -items $block.order -indent $indent - } - if ($block.selection) { - Emit-Selection -items $block.selection -indent $indent - } - if ($block.conditionalAppearance) { - Emit-ConditionalAppearance -items $block.conditionalAppearance -indent $indent - } - if ($block.outputParameters) { - Emit-OutputParameters -params $block.outputParameters -indent $indent - } - # nested children (StructureItemGroup внутри table row/column или chart axis). - # Platform-pattern: items внутри row/column/points/series — ВСЕГДА short form (без xsi:type). - if ($block.children) { - foreach ($child in $block.children) { - Emit-StructureItem -item $child -indent $indent -shortGroup - } - } - if ($block.viewMode) { - X "$indent<dcsset:viewMode>$(Esc-Xml "$($block.viewMode)")</dcsset:viewMode>" - } - if ($block.userSettingID) { - $uid = if ("$($block.userSettingID)" -eq "auto") { New-Guid-String } else { "$($block.userSettingID)" } - X "$indent<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - if ($block.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $block.userSettingPresentation -indent $indent - } - if ($block.itemsViewMode) { - X "$indent<dcsset:itemsViewMode>$(Esc-Xml "$($block.itemsViewMode)")</dcsset:itemsViewMode>" - } -} - -function Emit-StructureItem { - param($item, [string]$indent, [switch]$shortGroup) - - $type = if ($item.type) { "$($item.type)" } else { "group" } - - if ($type -eq "group") { - # Platform пишет короткую форму (без xsi:type) для groups внутри table row/column, - # explicit StructureItemGroup в остальных случаях. - if ($shortGroup) { - X "$indent<dcsset:item>" - } else { - X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" - } - - # use=false — отключённая ветка структуры - if ($item.use -eq $false) { - X "$indent`t<dcsset:use>false</dcsset:use>" - } - - if ($item.name) { - X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" - } - - $gb = if ($item.groupBy) { $item.groupBy } else { $item.groupFields } - Emit-GroupItems -groupBy $gb -indent "$indent`t" - - # Emit order/selection only if specified — platform doesn't always emit them on group - if ($item.order) { - Emit-Order -items $item.order -indent "$indent`t" -blockViewMode $item.orderViewMode -blockUserSettingID $item.orderUserSettingID - } - if ($item.selection) { - Emit-Selection -items $item.selection -indent "$indent`t" - } - - Emit-Filter -items $item.filter -indent "$indent`t" - - if ($item.conditionalAppearance) { - Emit-ConditionalAppearance -items $item.conditionalAppearance -indent "$indent`t" - } - - if ($item.outputParameters) { - Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" - } - - # Nested children — наследуем shortGroup от родителя (если родитель в short form, - # то и дети остаются short, как делает platform внутри row/column). - if ($item.children) { - foreach ($child in $item.children) { - if ($shortGroup) { - Emit-StructureItem -item $child -indent "$indent`t" -shortGroup - } else { - Emit-StructureItem -item $child -indent "$indent`t" - } - } - } - - # viewMode/itemsViewMode/userSettingID/userSettingPresentation on - # StructureItemGroup are context-dependent — emit only when explicitly set. - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - if ($item.userSettingID) { - $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" - } - if ($item.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" - } - if ($item.itemsViewMode) { - X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" - } - - X "$indent</dcsset:item>" - } - elseif ($type -eq "table") { - X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemTable`">" - - # use=false — отключённая таблица - if ($item.use -eq $false) { - X "$indent`t<dcsset:use>false</dcsset:use>" - } - - if ($item.name) { - X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" - } - - # Columns - if ($item.columns) { - foreach ($col in $item.columns) { - X "$indent`t<dcsset:column>" - Emit-TableAxisBlock -block $col -indent "$indent`t`t" - X "$indent`t</dcsset:column>" - } - } - - # Rows - if ($item.rows) { - foreach ($row in $item.rows) { - X "$indent`t<dcsset:row>" - Emit-TableAxisBlock -block $row -indent "$indent`t`t" - X "$indent`t</dcsset:row>" - } - } - - # Top-level: selection / conditionalAppearance / outputParameters на самой таблице - if ($item.selection) { - Emit-Selection -items $item.selection -indent "$indent`t" - } - if ($item.conditionalAppearance) { - Emit-ConditionalAppearance -items $item.conditionalAppearance -indent "$indent`t" - } - if ($item.outputParameters) { - Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" - } - # columnsViewMode / rowsViewMode — axis-level режим доступности (после rows/columns) - if ($item.columnsViewMode) { - X "$indent`t<dcsset:columnsViewMode>$(Esc-Xml "$($item.columnsViewMode)")</dcsset:columnsViewMode>" - } - if ($item.rowsViewMode) { - X "$indent`t<dcsset:rowsViewMode>$(Esc-Xml "$($item.rowsViewMode)")</dcsset:rowsViewMode>" - } - # viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой таблице - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - if ($item.userSettingID) { - $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" - } - if ($item.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" - } - if ($item.itemsViewMode) { - X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" - } - - X "$indent</dcsset:item>" - } - elseif ($type -eq "chart") { - X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemChart`">" - - # use=false — отключённая диаграмма - if ($item.use -eq $false) { - X "$indent`t<dcsset:use>false</dcsset:use>" - } - - if ($item.name) { - X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" - } - - # Points — single object или массив (multi-series диаграмма) - if ($item.points) { - $pBlocks = if ($item.points -is [array] -or ($item.points -is [System.Collections.IList] -and $item.points -isnot [string])) { - @($item.points) - } else { @($item.points) } - # Эвристика: если это массив объектов (а не одиночный объект-с-полями) → multi. - $isPointsArray = ($item.points -is [array]) -or ($item.points -is [System.Collections.IList] -and $item.points -isnot [string] -and $item.points -isnot [System.Collections.IDictionary] -and $item.points -isnot [PSCustomObject]) - if ($isPointsArray) { - foreach ($pb in $pBlocks) { - X "$indent`t<dcsset:point>" - Emit-TableAxisBlock -block $pb -indent "$indent`t`t" - X "$indent`t</dcsset:point>" - } - } else { - X "$indent`t<dcsset:point>" - Emit-TableAxisBlock -block $item.points -indent "$indent`t`t" - X "$indent`t</dcsset:point>" - } - } - - # Series — single object или массив - if ($item.series) { - $isSeriesArray = ($item.series -is [array]) -or ($item.series -is [System.Collections.IList] -and $item.series -isnot [string] -and $item.series -isnot [System.Collections.IDictionary] -and $item.series -isnot [PSCustomObject]) - if ($isSeriesArray) { - foreach ($sb in @($item.series)) { - X "$indent`t<dcsset:series>" - Emit-TableAxisBlock -block $sb -indent "$indent`t`t" - X "$indent`t</dcsset:series>" - } - } else { - X "$indent`t<dcsset:series>" - Emit-TableAxisBlock -block $item.series -indent "$indent`t`t" - X "$indent`t</dcsset:series>" - } - } - - # Selection (chart values) - Emit-Selection -items $item.selection -indent "$indent`t" - - if ($item.outputParameters) { - Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" - } - - # pointsViewMode / seriesViewMode — axis-level режим доступности (после points/series) - if ($item.pointsViewMode) { - X "$indent`t<dcsset:pointsViewMode>$(Esc-Xml "$($item.pointsViewMode)")</dcsset:pointsViewMode>" - } - if ($item.seriesViewMode) { - X "$indent`t<dcsset:seriesViewMode>$(Esc-Xml "$($item.seriesViewMode)")</dcsset:seriesViewMode>" - } - # viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой диаграмме - if ($item.viewMode) { - X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" - } - if ($item.userSettingID) { - $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" - } - if ($item.userSettingPresentation) { - Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" - } - if ($item.itemsViewMode) { - X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" - } - - X "$indent</dcsset:item>" - } - elseif ($type -eq "nestedObject") { - X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemNestedObject`">" - if ($item.objectID) { X "$indent`t<dcsset:objectID>$(Esc-Xml "$($item.objectID)")</dcsset:objectID>" } - X "$indent`t<dcsset:settings>" - $s = $item.settings - if ($s) { - if ($s.selection) { Emit-Selection -items $s.selection -indent "$indent`t`t" } - if ($s.filter) { Emit-Filter -items $s.filter -indent "$indent`t`t" } - if ($s.order) { Emit-Order -items $s.order -indent "$indent`t`t" } - if ($s.conditionalAppearance) { Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "$indent`t`t" } - if ($s.outputParameters) { Emit-OutputParameters -params $s.outputParameters -indent "$indent`t`t" } - } - X "$indent`t</dcsset:settings>" - X "$indent</dcsset:item>" - } -} - -function Emit-SettingsVariants { - $variants = $def.settingsVariants - - # Default variant if none specified - if (-not $variants -or $variants.Count -eq 0) { - $variants = @(@{ - name = "Основной" - presentation = "Основной" - settings = @{ - selection = @("Auto") - structure = @(@{ - type = "group" - order = @("Auto") - selection = @("Auto") - }) - } - }) - # Convert to PSCustomObject-like structure - $variants = @($variants | ForEach-Object { - $v = New-Object PSObject - $v | Add-Member -NotePropertyName "name" -NotePropertyValue $_.name - $v | Add-Member -NotePropertyName "presentation" -NotePropertyValue $_.presentation - $settingsObj = New-Object PSObject - $settingsObj | Add-Member -NotePropertyName "selection" -NotePropertyValue $_.settings.selection - $structItem = New-Object PSObject - $structItem | Add-Member -NotePropertyName "type" -NotePropertyValue "group" - $structItem | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto") - $structItem | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto") - $settingsObj | Add-Member -NotePropertyName "structure" -NotePropertyValue @($structItem) - $v | Add-Member -NotePropertyName "settings" -NotePropertyValue $settingsObj - $v - }) - } - - foreach ($v in $variants) { - X "`t<settingsVariant>" - X "`t`t<dcsset:name>$(Esc-Xml "$($v.name)")</dcsset:name>" - - $pres = if ($v.presentation) { $v.presentation } elseif ($v.title) { $v.title } else { "$($v.name)" } - Emit-MLText -tag "dcsset:presentation" -text $pres -indent "`t`t" - - X "`t`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">" - - $s = $v.settings - - # Helper: resolve XViewMode/XUserSettingID from settings — emit only if explicitly set - function Get-BlockVM([string]$key) { - $prop = "${key}ViewMode" - if ($s.PSObject.Properties[$prop]) { return "$($s.$prop)" } - return $null - } - function Get-BlockUSID([string]$key) { - $prop = "${key}UserSettingID" - if ($s.PSObject.Properties[$prop]) { return "$($s.$prop)" } - return $null - } - - # userFields — пользовательские вычисляемые поля (Expression / Case) - if ($s.userFields -and $s.userFields.Count -gt 0) { - Emit-UserFields -items $s.userFields -indent "`t`t`t" - } - - # Selection — эмитим даже если items пустые, но есть block-level viewMode/userSettingID. - # Platform может содержать Auto-items на top-level (вместе с явными полями). - $svm = Get-BlockVM 'selection'; $susid = Get-BlockUSID 'selection' - if ($s.selection -or $null -ne $svm -or $null -ne $susid) { - Emit-Selection -items $s.selection -indent "`t`t`t" -blockViewMode $svm -blockUserSettingID $susid - } - - # Filter - $fvm = Get-BlockVM 'filter'; $fusid = Get-BlockUSID 'filter' - if ($s.filter -or $null -ne $fvm -or $null -ne $fusid) { - Emit-Filter -items $s.filter -indent "`t`t`t" -blockViewMode $fvm -blockUserSettingID $fusid - } - - # Order - $ovm = Get-BlockVM 'order'; $ousid = Get-BlockUSID 'order' - if ($s.order -or $null -ne $ovm -or $null -ne $ousid) { - Emit-Order -items $s.order -indent "`t`t`t" -blockViewMode $ovm -blockUserSettingID $ousid - } - - # ConditionalAppearance - $cavm = Get-BlockVM 'conditionalAppearance'; $causid = Get-BlockUSID 'conditionalAppearance' - if ($s.conditionalAppearance -or $null -ne $cavm -or $null -ne $causid) { - Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "`t`t`t" -blockViewMode $cavm -blockUserSettingID $causid - } - - # OutputParameters (platform does NOT emit <viewMode> on this block) - if ($s.outputParameters) { - Emit-OutputParameters -params $s.outputParameters -indent "`t`t`t" - } - - # DataParameters - if ($s.dataParameters -eq 'auto') { - # Auto-generate dataParameters for all non-hidden params. - # Pattern follows 1C Designer / ERP persistence: - # - value set (non-default) → emit value, use=true (implicit) - # - value missing / Custom period → <use>false</use> + <value xsi:nil="true"/> - $autoDP = @() - foreach ($ap in $script:allParams) { - if ($ap.hidden) { continue } - $dpItem = New-Object PSObject - $dpItem | Add-Member -NotePropertyName "parameter" -NotePropertyValue $ap.name - $dpItem | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue "auto" - - $hasMeaningfulValue = $false - - if ($ap.type -eq 'StandardPeriod') { - # Inherit variant; Custom is treated as "empty" - $variant = 'Custom' - $av = $ap.value - if ($null -ne $av) { - if (($av -is [PSCustomObject] -or $av -is [hashtable]) -and $av.variant) { - $variant = "$($av.variant)" - } elseif ("$av") { - $variant = "$av" - } - } - $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue @{ variant = $variant } - if ($variant -ne 'Custom') { $hasMeaningfulValue = $true } - } elseif (-not (Test-EmptyValue $ap.value)) { - $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue $ap.value - $dpItem | Add-Member -NotePropertyName "valueType" -NotePropertyValue "$($ap.type)" - $hasMeaningfulValue = $true - } else { - $dpItem | Add-Member -NotePropertyName "nilValue" -NotePropertyValue $true - } - - if (-not $hasMeaningfulValue) { - $dpItem | Add-Member -NotePropertyName "use" -NotePropertyValue $false - } - - $autoDP += $dpItem - } - if ($autoDP.Count -gt 0) { - Emit-DataParameters -items $autoDP -indent "`t`t`t" - } - } elseif ($s.dataParameters) { - Emit-DataParameters -items $s.dataParameters -indent "`t`t`t" - } - - # Structure (supports string shorthand: "Организация > details") - if ($s.structure) { - $structItems = $s.structure - if ($structItems -is [string]) { - $structItems = Parse-StructureShorthand $structItems - } - foreach ($item in $structItems) { - Emit-StructureItem -item $item -indent "`t`t`t" - } - } - - # <dcsset:itemsViewMode> on <dcsset:settings> — emit only if explicitly set - if ($s.itemsViewMode) { - X "`t`t`t<dcsset:itemsViewMode>$(Esc-Xml "$($s.itemsViewMode)")</dcsset:itemsViewMode>" - } - - # <dcsset:additionalProperties> — key/value свойства варианта - if ($s.additionalProperties) { - X "`t`t`t<dcsset:additionalProperties>" - foreach ($prop in $s.additionalProperties.PSObject.Properties) { - X "`t`t`t`t<v8:Property name=`"$(Esc-Xml $prop.Name)`">" - X "`t`t`t`t`t<v8:Value xsi:type=`"xs:string`">$(Esc-Xml "$($prop.Value)")</v8:Value>" - X "`t`t`t`t</v8:Property>" - } - X "`t`t`t</dcsset:additionalProperties>" - } - - X "`t`t</dcsset:settings>" - X "`t</settingsVariant>" - } -} - -# --- 12. Assemble XML --- - -X "<?xml version=`"1.0`" encoding=`"UTF-8`"?>" -X ("<DataCompositionSchema xmlns=`"http://v8.1c.ru/8.1/data-composition-system/schema`"" + - " xmlns:dcscom=`"http://v8.1c.ru/8.1/data-composition-system/common`"" + - " xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`"" + - " xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`"" + - " xmlns:v8=`"http://v8.1c.ru/8.1/data/core`"" + - " xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`"" + - " xmlns:xs=`"http://www.w3.org/2001/XMLSchema`"" + - " xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`">") - -Emit-DataSources -Emit-DataSets -Emit-DataSetLinks -Emit-CalcFields -Emit-TotalFields -Emit-Parameters -Emit-Templates -Emit-FieldTemplates -Emit-GroupTemplates -Emit-SettingsVariants - -X '</DataCompositionSchema>' - -# --- 13. Write output --- - -Assert-EditAllowed $OutputPath 'editable' - -$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath) -if ($parentDir -and -not (Test-Path $parentDir)) { - New-Item -ItemType Directory -Force $parentDir | Out-Null -} - -$content = $script:xml.ToString() -$utf8Bom = New-Object System.Text.UTF8Encoding $true -[System.IO.File]::WriteAllText($OutputPath, $content, $utf8Bom) - -# --- 14. Statistics --- - -$dsCount = $def.dataSets.Count -$fieldCount = 0 -foreach ($ds in $def.dataSets) { - if ($ds.fields) { $fieldCount += $ds.fields.Count } -} -$calcCount = if ($def.calculatedFields) { $def.calculatedFields.Count } else { 0 } -$totalCount = if ($def.totalFields) { $def.totalFields.Count } else { 0 } -$paramCount = if ($def.parameters) { $def.parameters.Count } else { 0 } -$variantCount = if ($def.settingsVariants) { $def.settingsVariants.Count } else { 1 } -$fileSize = (Get-Item $OutputPath).Length - -Write-Host "OK $OutputPath" -Write-Host " DataSets: $dsCount Fields: $fieldCount Calculated: $calcCount Totals: $totalCount Params: $paramCount Variants: $variantCount" -Write-Host " Size: $fileSize bytes" +# skd-compile v1.107 — Compile 1C DCS from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [string]$DefinitionFile, + [string]$Value, + [Parameter(Mandatory)] + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +# --- 1. Load and validate JSON --- + +if ($DefinitionFile -and $Value) { + Write-Error "Cannot use both -DefinitionFile and -Value" + exit 1 +} +if (-not $DefinitionFile -and -not $Value) { + Write-Error "Either -DefinitionFile or -Value is required" + exit 1 +} + +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + if (-not (Test-Path $DefinitionFile)) { + Write-Error "Definition file not found: $DefinitionFile" + exit 1 + } + $json = Get-Content -Raw -Encoding UTF8 $DefinitionFile +} else { + $json = $Value +} + +$def = $json | ConvertFrom-Json + +# --- Sentinel check: refuse to compile if JSON contains skd-decompile sentinels --- +# These mark places the decompiler couldn't reverse cleanly; user must resolve +# them manually before compile (see <basename>.warnings.md alongside the JSON). +$script:foundSentinels = @() +function Scan-Sentinels { + param($obj, [string]$path) + if ($null -eq $obj) { return } + if ($obj -is [System.Collections.IDictionary]) { + foreach ($k in @($obj.Keys)) { + if ($k -eq '__unsupported__') { + $u = $obj[$k] + $id = $u.id; $kind = $u.kind; $loc = $u.loc + $script:foundSentinels += " $id [$kind] at $path → $loc" + } else { + Scan-Sentinels -obj $obj[$k] -path "$path/$k" + } + } + } elseif ($obj -is [System.Management.Automation.PSCustomObject]) { + foreach ($p in $obj.PSObject.Properties) { + if ($p.Name -eq '__unsupported__') { + $u = $p.Value + $id = $u.id; $kind = $u.kind; $loc = $u.loc + $script:foundSentinels += " $id [$kind] at $path → $loc" + } else { + Scan-Sentinels -obj $p.Value -path "$path/$($p.Name)" + } + } + } elseif ($obj -is [System.Collections.IEnumerable] -and -not ($obj -is [string])) { + $i = 0 + foreach ($item in $obj) { Scan-Sentinels -obj $item -path "$path[$i]"; $i++ } + } +} +Scan-Sentinels -obj $def -path '' +if ($script:foundSentinels.Count -gt 0) { + [Console]::Error.WriteLine("skd-compile: JSON содержит __unsupported__ маркеры от skd-decompile.") + [Console]::Error.WriteLine("Это конструкции, которые декомпиляция не смогла обратить — нужно разрешить вручную перед компиляцией.") + [Console]::Error.WriteLine("См. <basename>.warnings.md рядом с JSON. Найдено:") + foreach ($s in $script:foundSentinels) { [Console]::Error.WriteLine($s) } + exit 4 +} + +if (-not $def.dataSets -or $def.dataSets.Count -eq 0) { + Write-Error "JSON must have at least one entry in 'dataSets'" + exit 1 +} + +# Base directory for resolving @file references in query +$script:queryBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName($DefinitionFile) } else { (Get-Location).Path } + +# --- 2. XML helpers --- + +$script:xml = New-Object System.Text.StringBuilder 16384 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>') +} + +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) + } + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + +function Emit-MLText { + param([string]$tag, $text, [string]$indent, [switch]$NoXsiType) + # Empty value → self-closing tag (matches platform output) + if ($null -eq $text -or ($text -is [string] -and $text -eq '')) { + if ($NoXsiType) { + X "$indent<$tag/>" + } else { + X "$indent<$tag xsi:type=`"v8:LocalStringType`"/>" + } + return + } + if ($NoXsiType) { + X "$indent<$tag>" + } else { + X "$indent<$tag xsi:type=`"v8:LocalStringType`">" + } + # Multi-lang: object form { ru: "...", en: "..." } → one <v8:item> per language + if ($text -is [System.Management.Automation.PSCustomObject] -or $text -is [hashtable] -or $text -is [System.Collections.IDictionary]) { + $props = if ($text -is [System.Management.Automation.PSCustomObject]) { $text.PSObject.Properties } else { $text.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } } + foreach ($p in $props) { + $lang = if ($p -is [hashtable]) { $p.Name } else { $p.Name } + $content = if ($p -is [hashtable]) { $p.Value } else { $p.Value } + X "$indent`t<v8:item>" + X "$indent`t`t<v8:lang>$(Esc-Xml "$lang")</v8:lang>" + X "$indent`t`t<v8:content>$(Esc-Xml "$content")</v8:content>" + X "$indent`t</v8:item>" + } + } else { + X "$indent`t<v8:item>" + X "$indent`t`t<v8:lang>ru</v8:lang>" + X "$indent`t`t<v8:content>$(Esc-Xml "$text")</v8:content>" + X "$indent`t</v8:item>" + } + X "$indent</$tag>" +} + +function New-Guid-String { + return [System.Guid]::NewGuid().ToString() +} + +# --- 3. Resolve defaults --- + +# DataSources +$dataSources = @() +if ($def.dataSources) { + foreach ($ds in $def.dataSources) { + $dataSources += @{ + name = "$($ds.name)" + type = if ($ds.type) { "$($ds.type)" } else { "Local" } + } + } +} else { + $dataSources += @{ name = "ИсточникДанных1"; type = "Local" } +} + +$defaultSource = $dataSources[0].name + +# Auto-name dataSets +$dsIndex = 1 +foreach ($ds in $def.dataSets) { + if (-not $ds.name) { + $ds | Add-Member -NotePropertyName "name" -NotePropertyValue "НаборДанных$dsIndex" -Force + } + $dsIndex++ +} + +# --- 4. Type system --- + +# Type synonyms — normalize Russian/common names to canonical DSL types +# Use case-sensitive hashtable to avoid PS 5.1 DuplicateKeyInHashLiteral +$script:typeSynonyms = New-Object System.Collections.Hashtable +# Russian names (case doesn't matter — we'll also do case-insensitive lookup) +$script:typeSynonyms["число"] = "decimal" +$script:typeSynonyms["строка"] = "string" +$script:typeSynonyms["булево"] = "boolean" +$script:typeSynonyms["дата"] = "date" +$script:typeSynonyms["датавремя"] = "dateTime" +$script:typeSynonyms["время"] = "time" +$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" +# English canonical (lowercase for lookup) +$script:typeSynonyms["bool"] = "boolean" +$script:typeSynonyms["str"] = "string" +$script:typeSynonyms["int"] = "decimal" +$script:typeSynonyms["integer"] = "decimal" +$script:typeSynonyms["number"] = "decimal" +$script:typeSynonyms["num"] = "decimal" +# Reference synonyms (Russian, lowercase) +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + # Check for parameterized types: число(15,2), строка(100), etc. + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + + # Resolve base name (case-insensitive via .ToLower()) + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + + return $typeStr + } + + # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) # includes the dot + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + # Simple name lookup (case-insensitive) + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + + return $typeStr +} + +function Emit-ValueType { + param($typeStr, [string]$indent) + + if (-not $typeStr) { return } + + # Multi-type: iterate and emit each type with its qualifiers + if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { + foreach ($t in $typeStr) { Emit-SingleValueType -typeStr "$t" -indent $indent } + return + } + + Emit-SingleValueType -typeStr "$typeStr" -indent $indent +} + +function Emit-SingleValueType { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return } + + # Resolve synonyms first + $typeStr = Resolve-TypeStr $typeStr + + # boolean + if ($typeStr -eq "boolean") { + X "$indent<v8:Type>xs:boolean</v8:Type>" + return + } + + # string, string(N), string(N,fix) — fix → AllowedLength=Fixed + if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $al = if ($Matches[4]) { "Fixed" } else { "Variable" } + X "$indent<v8:Type>xs:string</v8:Type>" + X "$indent<v8:StringQualifiers>" + X "$indent`t<v8:Length>$len</v8:Length>" + X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" + X "$indent</v8:StringQualifiers>" + return + } + + # decimal forms (defaults — bare decimal = money 10,2; decimal(N) = integer N,0): + # decimal → 10,2,Any + # decimal(N) → N,0,Any + # decimal(N,nonneg) → N,0,Nonnegative + # decimal(N,M) → N,M,Any + # decimal(N,M,nonneg) → N,M,Nonnegative + if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { + if (-not $Matches[1]) { + $digits = "10"; $fraction = "2"; $sign = "Any" + } else { + $digits = $Matches[2] + $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } + $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } + } + X "$indent<v8:Type>xs:decimal</v8:Type>" + X "$indent<v8:NumberQualifiers>" + X "$indent`t<v8:Digits>$digits</v8:Digits>" + X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" + X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" + X "$indent</v8:NumberQualifiers>" + return + } + + # date / dateTime / time — all use xs:dateTime, differ only in DateFractions + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + X "$indent<v8:Type>xs:dateTime</v8:Type>" + X "$indent<v8:DateQualifiers>" + X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" + X "$indent</v8:DateQualifiers>" + return + } + + # StandardPeriod + if ($typeStr -eq "StandardPeriod") { + X "$indent<v8:Type>v8:StandardPeriod</v8:Type>" + return + } + + # Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc. + # Real DCS files use inline namespace d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" + return + } + + # TypeSet (композитный тип-набор): голое имя без точки, типа DocumentRef / CatalogRef / + # EnumRef / ChartOfAccountsRef / etc. (все ссылки указанного класса). + # Эмитим <v8:TypeSet xmlns:dN="..."> вместо <v8:Type>. + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef|InformationRegisterRef|AnyRef)$') { + X "$indent<v8:TypeSet xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:TypeSet>" + return + } + + # Fallback — assume dot-qualified types are also config references + if ($typeStr.Contains('.')) { + X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" + return + } + + X "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>" +} + +# --- 5. Field shorthand parser --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @(); appearance = [ordered]@{} + roleExtras = [ordered]@{} + } + + # Extract @roles + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + # Extract #restrictions + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + # Extract role kv=value (e.g. balanceGroupName=Сумма balanceType=OpeningBalance) + $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') + foreach ($m in $kvMatches) { $result.roleExtras[$m.Groups[1].Value] = $m.Groups[2].Value } + $s = [regex]::Replace($s, '\s*\w+=\S+', '') + + # Split name: type + $s = $s.Trim() + if ($s.Contains(':')) { + $parts = $s -split ':', 2 + $result.dataPath = $parts[0].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) + } else { + $result.dataPath = $s + } + + $result.field = $result.dataPath + return $result +} + +# Universal role spec parser: string / array / object / null +# Returns @{ tokens = @(...); extras = [ordered]@{...} } +function Parse-RoleSpec { + param($spec) + $tokens = @() + $extras = [ordered]@{} + + if ($null -ne $spec) { + if ($spec -is [string]) { + if ($spec -notmatch '\s' -and $spec -notmatch '=') { + $tokens += $spec + } else { + $s = $spec.Trim() + foreach ($m in [regex]::Matches($s, '@(\w+)')) { $tokens += $m.Groups[1].Value } + $s = [regex]::Replace($s, '\s*@\w+', '').Trim() + foreach ($m in [regex]::Matches($s, '(\w+)=(\S+)')) { $extras[$m.Groups[1].Value] = $m.Groups[2].Value } + } + } elseif ($spec -is [array] -or $spec -is [System.Collections.IList]) { + foreach ($t in $spec) { $tokens += "$t" } + } elseif ($spec.PSObject -and $spec.PSObject.Properties) { + foreach ($prop in $spec.PSObject.Properties) { + $val = $prop.Value + if ($val -is [bool]) { + if ($val) { $tokens += $prop.Name } + } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { + $extras[$prop.Name] = "$val" + } + } + } elseif ($spec -is [hashtable] -or $spec -is [System.Collections.IDictionary]) { + foreach ($k in $spec.Keys) { + $val = $spec[$k] + if ($val -is [bool]) { + if ($val) { $tokens += "$k" } + } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { + $extras["$k"] = "$val" + } + } + } + } + + # Deprecated alias: balanceGroup → balanceGroupName (старое имя в коде compile, в реальном XML — Name) + if ($extras.Contains('balanceGroup') -and -not $extras.Contains('balanceGroupName')) { + $extras['balanceGroupName'] = $extras['balanceGroup'] + $extras.Remove('balanceGroup') + } + + return @{ tokens = $tokens; extras = $extras } +} + +# --- 6. Total field shorthand parser --- + +function Parse-TotalShorthand { + param([string]$s) + + # "DataPath: Func" or "DataPath: Func(expr)" + $parts = $s -split ':', 2 + $dataPath = $parts[0].Trim() + $funcPart = $parts[1].Trim() + + # Known DCS aggregate functions (ru + en) + $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', + 'Sum','Count','Min','Max','Avg', + 'Minimum','Maximum','Average') + + if ($funcPart -match '^\w+\(') { + # Already has expression form: Func(expr) + return @{ dataPath = $dataPath; expression = $funcPart } + } elseif ($funcPart -in $aggFuncs) { + # Short: Func → Func(DataPath) + return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } + } else { + # Identity or custom expression — use as-is + return @{ dataPath = $dataPath; expression = $funcPart } + } +} + +# --- 7. Parameter shorthand parser --- + +function Split-ValueListCsv { + # Split on top-level commas (respecting 'single'/"double" quotes), strip quotes, + # drop empties. No ':' handling — values may contain colons (dateTime). + param([string]$s) + $result = @() + if ($null -eq $s) { return ,$result } + $items = @() + $buf = New-Object System.Text.StringBuilder + $inQuote = $null + for ($i = 0; $i -lt $s.Length; $i++) { + $ch = $s[$i] + if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } + elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } + elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } + else { [void]$buf.Append($ch) } + } + if ($buf.Length -gt 0) { $items += $buf.ToString() } + foreach ($raw in $items) { + $t = $raw.Trim() + if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } + if ($t -ne "") { $result += $t } + } + return ,$result +} + +function Parse-ParamShorthand { + param([string]$s) + + $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null } + + # Extract @autoDates flag + if ($s -match '@autoDates') { + $result.autoDates = $true + $s = $s -replace '\s*@autoDates', '' + } + + # Extract @valueList flag + if ($s -match '@valueList') { + $result.valueListAllowed = $true + $s = $s -replace '\s*@valueList', '' + } + + # Extract @hidden flag + if ($s -match '@hidden') { + $result.hidden = $true + $s = $s -replace '\s*@hidden', '' + } + + # Extract optional [Title] (mirrors Parse-FieldShorthand) + if ($s -match '\[([^\]]*)\]') { + $result.title = $Matches[1].Trim() + $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() + } + + # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { + $result.name = $Matches[1].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) + if ($Matches[4]) { + $rhs = $Matches[4].Trim() + $items = Split-ValueListCsv $rhs + if ($items.Count -ge 2) { + # Multi-value default → list; valueListAllowed implied + $result.value = $items + $result.valueListAllowed = $true + } elseif ($items.Count -eq 1) { + $result.value = $items[0] + } else { + $result.value = $rhs + } + } + } else { + $result.name = $s.Trim() + } + + return $result +} + +# --- 8. Calculated field shorthand parser --- + +function Parse-CalcShorthand { + param([string]$s) + + # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". + # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside + # an expression (e.g. index access) isn't interpreted as a title. + # - `#restrict` flags use a known-names pattern and are extracted globally — + # the docs put them after `=`, and the closed flag set avoids matching + # `#word` that happens to appear inside a string literal. + $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' + + $restrict = @() + foreach ($m in [regex]::Matches($s, $restrictPattern)) { + $restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, "\s*$restrictPattern", '') + + $eqIdx = $s.IndexOf('=') + if ($eqIdx -gt 0) { + $lhs = $s.Substring(0, $eqIdx) + $rhs = $s.Substring($eqIdx + 1).Trim() + } else { + $lhs = $s + $rhs = "" + } + + $title = "" + if ($lhs -match '\[([^\]]+)\]') { + $title = $Matches[1] + $lhs = $lhs -replace '\s*\[[^\]]+\]', '' + } + $lhs = $lhs.Trim() + + $type = "" + $dataPath = $lhs + if ($lhs.Contains(':')) { + $parts = $lhs -split ':', 2 + $dataPath = $parts[0].Trim() + $type = Resolve-TypeStr ($parts[1].Trim()) + } + + return @{ + dataPath = $dataPath + expression = $rhs + type = $type + title = $title + restrict = $restrict + } +} + +# --- 8b. DataParameter shorthand parser --- +# Formats: "Период = LastMonth @user", "Организация @off @user", "Период @user" +function Parse-DataParamShorthand { + param([string]$s) + + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + + # Extract @flags + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + + $s = $s.Trim() + + # Split "Name = Value" + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + + # Detect StandardPeriod variants + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { + $result.value = @{ variant = $valStr } + } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valStr + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + $result.value = [bool]($valStr -eq "true") + } else { + $result.value = $valStr + } + } else { + $result.parameter = $s + } + + return $result +} + +# --- 8c. Filter item shorthand parser --- +# Formats: "Организация = _ @off @user", "Дата >= 2024-01-01T00:00:00", "Статус filled" +function Parse-FilterShorthand { + param([string]$s) + + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } + + # Extract @flags + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + if ($s -match '@inaccessible') { + $result.viewMode = "Inaccessible" + $s = $s -replace '\s*@inaccessible', '' + } + + $s = $s.Trim() + + # Try to match: Field op Value, or Field op (no value for filled/notFilled) + # Operators sorted longest first to match >= before > + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $opRaw = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + + # Map op + $opMap = @{ + "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" + } + $mapped = $opMap[$opRaw] + if ($mapped) { $result.op = $opRaw } else { $result.op = $opRaw } + + # Parse value (skip "_" which means empty/placeholder) + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { + $result.value = [bool]($valPart -eq "true") + $result["valueType"] = "xs:boolean" + } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valPart + $result["valueType"] = "xs:dateTime" + } elseif ($valPart -match '^\d+(\.\d+)?$') { + $result.value = $valPart + $result["valueType"] = "xs:decimal" + } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { + $result.value = $valPart + $result["valueType"] = "dcscor:DesignTimeValue" + } else { + $result.value = $valPart + $result["valueType"] = "xs:string" + } + } + } else { + # No operator found — just a field name + $result.field = $s + } + + return $result +} + +# --- 9. Comparison type mapper --- + +$script:comparisonTypes = @{ + "=" = "Equal"; "<>" = "NotEqual" + ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual" + "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" +} + +# --- 10. Output parameter type detection --- + +$script:outputParamTypes = @{ + "Заголовок" = "mltext" + "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" + "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" + "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" + "МакетОформления" = "xs:string" + "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" + "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" + "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "РасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "РасположениеИтогов" = "dcscor:DataCompositionTotalPlacement" + "РасположениеГруппировки" = "dcsset:DataCompositionFieldGroupPlacement" + "РасположениеРесурсов" = "dcsset:DataCompositionResourcesPlacement" + "ТипМакета" = "dcsset:DataCompositionGroupTemplateType" +} + +# --- 11. Emit sections --- + +# === DataSources === +function Emit-DataSources { + foreach ($ds in $dataSources) { + X "`t<dataSource>" + X "`t`t<name>$(Esc-Xml $ds.name)</name>" + X "`t`t<dataSourceType>$(Esc-Xml $ds.type)</dataSourceType>" + X "`t</dataSource>" + } +} + +# === Fields === +function Has-JsonProp { + param($obj, [string]$name) + if ($null -eq $obj) { return $false } + if ($obj.PSObject -and $obj.PSObject.Properties) { + return $null -ne $obj.PSObject.Properties[$name] + } + if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } + return $false +} + +function Emit-InputParameters { + param($ip, [string]$indent) + if ($null -eq $ip) { return } + $items = @($ip) + if ($items.Count -eq 0) { return } + X "$indent<inputParameters>" + foreach ($item in $items) { + X "$indent`t<dcscor:item>" + if ((Has-JsonProp $item 'use') -and $null -ne $item.use -and -not $item.use) { + X "$indent`t`t<dcscor:use>false</dcscor:use>" + } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" + if (Has-JsonProp $item 'choiceParameters') { + $cp = $item.choiceParameters + $cpItems = if ($null -ne $cp) { @($cp) } else { @() } + if ($cpItems.Count -eq 0) { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" + foreach ($cpItem in $cpItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" + foreach ($v in @($cpItem.values)) { + if ($v -is [bool]) { + $vStr = if ($v) { 'true' } else { 'false' } + X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$vStr</dcscor:value>" + } elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { + X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" + } else { + X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" + } + } + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-JsonProp $item 'choiceParameterLinks') { + $cpl = $item.choiceParameterLinks + $cplItems = if ($null -ne $cpl) { @($cpl) } else { @() } + if ($cplItems.Count -eq 0) { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" + foreach ($cplItem in $cplItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" + X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" + $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } + X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-JsonProp $item 'value') { + # Simple typed value — определяем xsi:type из JSON-типа + $val = $item.value + # Явный кастомный type из decompile: {uri, name} → <value xmlns:dN="uri" xsi:type="dN:name"> + $customType = $null + if (Has-JsonProp $item 'valueType') { + $vtSrc = $item.valueType + $uri = $null; $tName = $null + if ($vtSrc -is [PSCustomObject]) { + if ($vtSrc.PSObject.Properties['uri']) { $uri = "$($vtSrc.uri)" } + if ($vtSrc.PSObject.Properties['name']) { $tName = "$($vtSrc.name)" } + } elseif ($vtSrc -is [System.Collections.IDictionary]) { + if ($vtSrc.Contains('uri')) { $uri = "$($vtSrc['uri'])" } + if ($vtSrc.Contains('name')) { $tName = "$($vtSrc['name'])" } + } + if ($uri -and $tName) { $customType = @{ uri = $uri; name = $tName } } + } + if ($customType) { + X "$indent`t`t<dcscor:value xmlns:dN=`"$($customType.uri)`" xsi:type=`"dN:$($customType.name)`">$(Esc-Xml "$val")</dcscor:value>" + } elseif ($val -is [bool]) { + $vStr = if ($val) { 'true' } else { 'false' } + X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$vStr</dcscor:value>" + } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { + X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" + } elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { + # Multilang dict {ru, en, ...} → LocalStringType + Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" + } + } + X "$indent`t</dcscor:item>" + } + X "$indent</inputParameters>" +} + +function Emit-Field { + param($fieldDef, [string]$indent) + + if ($fieldDef -is [string]) { + $f = Parse-FieldShorthand $fieldDef + } else { + $f = @{ + dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" } + field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" } + title = if ($fieldDef.title) { $fieldDef.title } else { "" } + type = if ($fieldDef.type) { + if ($fieldDef.type -is [array] -or $fieldDef.type -is [System.Collections.IList]) { + @($fieldDef.type | ForEach-Object { Resolve-TypeStr "$_" }) + } else { + Resolve-TypeStr "$($fieldDef.type)" + } + } else { "" } + roles = @() + restrict = @() + appearance = [ordered]@{} + roleExtras = [ordered]@{} + } + # Parse role (string shorthand / array / object — единый формат с /skd-edit set-field-role) + if ($fieldDef.role) { + $parsed = Parse-RoleSpec $fieldDef.role + $f.roles = $parsed.tokens + $f.roleExtras = $parsed.extras + } + # Parse restrictions + if ($fieldDef.restrict) { + $f.restrict = @($fieldDef.restrict) + } + # Parse appearance (сохраняем значение как есть — может быть string или multilang dict) + if ($fieldDef.appearance) { + foreach ($prop in $fieldDef.appearance.PSObject.Properties) { + $f.appearance[$prop.Name] = $prop.Value + } + } + if ($fieldDef.presentationExpression) { + $f["presentationExpression"] = "$($fieldDef.presentationExpression)" + } + # attrRestrict + if ($fieldDef.attrRestrict) { + $f["attrRestrict"] = @($fieldDef.attrRestrict) + } + # availableValues — array of {value, presentation} + if ($fieldDef.availableValues) { + $f["availableValues"] = $fieldDef.availableValues + } + # orderExpression — {expression, orderType, autoOrder} + if ($fieldDef.orderExpression) { + $f["orderExpression"] = $fieldDef.orderExpression + } + # inputParameters — массив элементов, типизированных по форме value + if ($null -ne $fieldDef.inputParameters) { + $f["inputParameters"] = $fieldDef.inputParameters + } + # folder: true → DataSetFieldFolder (поле-папка для UI-группировки, только dataPath+title) + if ($fieldDef.folder -eq $true) { + $f["folder"] = $true + } + } + + # DataSetFieldFolder — только dataPath + title (для UI-группировки полей в композиторе) + if ($f["folder"]) { + X "$indent<field xsi:type=`"DataSetFieldFolder`">" + X "$indent`t<dataPath>$(Esc-Xml $f.dataPath)</dataPath>" + if ($f.title) { Emit-MLText -tag "title" -text $f.title -indent "$indent`t" } + X "$indent</field>" + return + } + + X "$indent<field xsi:type=`"DataSetFieldField`">" + X "$indent`t<dataPath>$(Esc-Xml $f.dataPath)</dataPath>" + X "$indent`t<field>$(Esc-Xml $f.field)</field>" + + # Title + if ($f.title) { + Emit-MLText -tag "title" -text $f.title -indent "$indent`t" + } + + # UseRestriction + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + if ($f.restrict.Count -gt 0) { + X "$indent`t<useRestriction>" + foreach ($r in $f.restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + X "$indent`t`t<$xmlName>true</$xmlName>" + } + } + X "$indent`t</useRestriction>" + } + + # AttributeUseRestriction + if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) { + X "$indent`t<attributeUseRestriction>" + foreach ($r in $f["attrRestrict"]) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + X "$indent`t`t<$xmlName>true</$xmlName>" + } + } + X "$indent`t</attributeUseRestriction>" + } + + # Role + $hasExtras = $f["roleExtras"] -and $f["roleExtras"].Count -gt 0 + if ($f.roles.Count -gt 0 -or $hasExtras) { + X "$indent`t<role>" + foreach ($role in $f.roles) { + if ($role -eq "period") { + # @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить. + $pnInExtras = $hasExtras -and $f["roleExtras"].Contains('periodNumber') + $ptInExtras = $hasExtras -and $f["roleExtras"].Contains('periodType') + if (-not $pnInExtras) { X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>" } + if (-not $ptInExtras) { X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>" } + } else { + X "$indent`t`t<dcscom:$role>true</dcscom:$role>" + } + } + if ($hasExtras) { + foreach ($k in $f["roleExtras"].Keys) { + X "$indent`t`t<dcscom:$k>$(Esc-Xml "$($f["roleExtras"][$k])")</dcscom:$k>" + } + } + X "$indent`t</role>" + } + + # OrderExpression — после role, до valueType. Допустим массив (multi-sort). + if ($f["orderExpression"]) { + $oeRaw = $f["orderExpression"] + if ($oeRaw -is [System.Collections.IDictionary]) { + $oeList = @($oeRaw) + } elseif ($oeRaw -is [System.Collections.IList]) { + $oeList = $oeRaw + } else { + $oeList = @($oeRaw) + } + foreach ($oe in $oeList) { + $expr = if ($oe.expression) { "$($oe.expression)" } else { '' } + $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' } + $autoOrder = if ($null -ne $oe.autoOrder) { $(if ($oe.autoOrder) { 'true' } else { 'false' }) } else { 'false' } + X "$indent`t<orderExpression>" + X "$indent`t`t<dcscom:expression>$(Esc-Xml $expr)</dcscom:expression>" + X "$indent`t`t<dcscom:orderType>$oType</dcscom:orderType>" + X "$indent`t`t<dcscom:autoOrder>$autoOrder</dcscom:autoOrder>" + X "$indent`t</orderExpression>" + } + } + + # ValueType + if ($f.type) { + X "$indent`t<valueType>" + Emit-ValueType -typeStr $f.type -indent "$indent`t`t" + X "$indent`t</valueType>" + } + + # AvailableValues — list of allowed values with optional multilang presentation + if ($f["availableValues"]) { + foreach ($av in $f["availableValues"]) { + X "$indent`t<availableValue>" + $avVal = $av.value + $avType = if ($av.valueType) { "$($av.valueType)" } else { '' } + if (-not $avType) { + if ($avVal -is [bool]) { $avType = 'xs:boolean' } + elseif ($avVal -is [int] -or $avVal -is [long] -or $avVal -is [double]) { $avType = 'xs:decimal' } + elseif ("$avVal" -match '^\d{4}-\d{2}-\d{2}T') { $avType = 'xs:dateTime' } + else { $avType = 'xs:string' } + } + $avStr = if ($avVal -is [bool]) { "$avVal".ToLower() } else { Esc-Xml "$avVal" } + X "$indent`t`t<value xsi:type=`"$avType`">$avStr</value>" + if ($av.presentation) { + Emit-MLText -tag "presentation" -text $av.presentation -indent "$indent`t`t" + } + X "$indent`t</availableValue>" + } + } + + # Appearance + if ($f.appearance -and $f.appearance.Count -gt 0) { + X "$indent`t<appearance>" + foreach ($key in $f.appearance.Keys) { + $val = $f.appearance[$key] + # ГоризонтальноеПоложение требует специального xsi:type (v8ui:HorizontalAlign), не строка + if ($key -eq "ГоризонтальноеПоложение" -and -not ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject])) { + X "$indent`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + X "$indent`t`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + X "$indent`t`t`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml "$val")</dcscor:value>" + X "$indent`t`t</dcscor:item>" + } else { + Emit-AppearanceValue -key $key -val $val -indent "$indent`t`t" + } + } + X "$indent`t</appearance>" + } + + # PresentationExpression + if ($f["presentationExpression"]) { + X "$indent`t<presentationExpression>$(Esc-Xml $f["presentationExpression"])</presentationExpression>" + } + + # InputParameters — в конце field + if ($f["inputParameters"]) { + Emit-InputParameters -ip $f["inputParameters"] -indent "$indent`t" + } + + X "$indent</field>" +} + +# === DataSets === +function Emit-DataSet { + param($ds, [string]$indent, [string]$tagName = "dataSet") + + # Determine type + if ($ds.items) { + $dsType = "DataSetUnion" + } elseif ($ds.objectName) { + $dsType = "DataSetObject" + } else { + $dsType = "DataSetQuery" + } + + X "$indent<$tagName xsi:type=`"$dsType`">" + X "$indent`t<name>$(Esc-Xml "$($ds.name)")</name>" + + # Fields + if ($ds.fields) { + foreach ($f in $ds.fields) { + Emit-Field -fieldDef $f -indent "$indent`t" + } + } + + # DataSource (not for Union) + if ($dsType -ne "DataSetUnion") { + $src = if ($ds.source) { "$($ds.source)" } else { $defaultSource } + X "$indent`t<dataSource>$(Esc-Xml $src)</dataSource>" + } + + # Type-specific content + if ($dsType -eq "DataSetQuery") { + $queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir + X "$indent`t<query>$(Esc-Xml $queryText)</query>" + if ($ds.autoFillFields -eq $false) { + X "$indent`t<autoFillFields>false</autoFillFields>" + } + } elseif ($dsType -eq "DataSetObject") { + X "$indent`t<objectName>$(Esc-Xml "$($ds.objectName)")</objectName>" + } elseif ($dsType -eq "DataSetUnion") { + foreach ($item in $ds.items) { + # Union inner items are wrapped as <item xsi:type="..."> + Emit-DataSet -ds $item -indent "$indent`t" -tagName "item" | Out-Null + } + } + + X "$indent</$tagName>" +} + +function Emit-DataSets { + foreach ($ds in $def.dataSets) { + Emit-DataSet -ds $ds -indent "`t" + } +} + +# === DataSetLinks === +function Emit-DataSetLinks { + if (-not $def.dataSetLinks) { return } + foreach ($link in $def.dataSetLinks) { + X "`t<dataSetLink>" + $srcDS = if ($link.source) { "$($link.source)" } elseif ($link.sourceDataSet) { "$($link.sourceDataSet)" } else { "" } + $dstDS = if ($link.dest) { "$($link.dest)" } elseif ($link.destinationDataSet) { "$($link.destinationDataSet)" } else { "" } + $srcEx = if ($link.sourceExpr) { "$($link.sourceExpr)" } elseif ($link.sourceExpression) { "$($link.sourceExpression)" } else { "" } + $dstEx = if ($link.destExpr) { "$($link.destExpr)" } elseif ($link.destinationExpression) { "$($link.destinationExpression)" } else { "" } + X "`t`t<sourceDataSet>$(Esc-Xml $srcDS)</sourceDataSet>" + X "`t`t<destinationDataSet>$(Esc-Xml $dstDS)</destinationDataSet>" + X "`t`t<sourceExpression>$(Esc-Xml $srcEx)</sourceExpression>" + X "`t`t<destinationExpression>$(Esc-Xml $dstEx)</destinationExpression>" + if ($link.parameter) { + X "`t`t<parameter>$(Esc-Xml "$($link.parameter)")</parameter>" + } + if ($link.PSObject.Properties.Match('parameterListAllowed').Count -gt 0 -and $link.parameterListAllowed) { + X "`t`t<parameterListAllowed>true</parameterListAllowed>" + } + if ($link.PSObject.Properties.Match('startExpression').Count -gt 0 -and $null -ne $link.startExpression) { + X "`t`t<startExpression>$(Esc-Xml "$($link.startExpression)")</startExpression>" + } + if ($link.PSObject.Properties.Match('linkConditionExpression').Count -gt 0 -and $null -ne $link.linkConditionExpression) { + X "`t`t<linkConditionExpression>$(Esc-Xml "$($link.linkConditionExpression)")</linkConditionExpression>" + } + X "`t</dataSetLink>" + } +} + +# === CalculatedFields === +function Emit-CalcFields { + if (-not $def.calculatedFields) { return } + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + foreach ($cf in $def.calculatedFields) { + # Collect dataPath/expression/title/type/restrict/appearance from either + # shorthand string or object form. Object form accepts dataPath/field/name + # as synonyms; useRestriction/restrict accepts object, array, or flag string. + $title = "" + $typeStr = "" + $restrictTokens = @() + $restrictObj = $null + $appearance = $null + + if ($cf -is [string]) { + $parsed = Parse-CalcShorthand $cf + $dataPath = "$($parsed.dataPath)" + $expression = "$($parsed.expression)" + $title = $parsed.title + $typeStr = "$($parsed.type)" + if ($parsed.restrict) { $restrictTokens = @($parsed.restrict) } + } else { + $dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } + elseif ($cf.field) { "$($cf.field)" } + else { "$($cf.name)" } + $expression = "$($cf.expression)" + if ($cf.title) { $title = $cf.title } + if ($cf.type) { $typeStr = Resolve-TypeStr "$($cf.type)" } + + $restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null } + if ($restrictVal) { + if ($restrictVal -is [System.Management.Automation.PSCustomObject] -or $restrictVal -is [hashtable]) { + $restrictObj = $restrictVal + } elseif ($restrictVal -is [string]) { + # Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`) + foreach ($tok in ($restrictVal -split '\s+')) { + $t = $tok.Trim().TrimStart('#') + if ($t) { $restrictTokens += $t } + } + } else { + # Array form: ["noField", "noFilter", ...] + foreach ($r in $restrictVal) { $restrictTokens += "$r" } + } + } + if ($cf.appearance) { $appearance = $cf.appearance } + } + + X "`t<calculatedField>" + X "`t`t<dataPath>$(Esc-Xml $dataPath)</dataPath>" + X "`t`t<expression>$(Esc-Xml $expression)</expression>" + + if ($title) { + Emit-MLText -tag "title" -text $title -indent "`t`t" + } + if ($typeStr) { + X "`t`t<valueType>" + Emit-ValueType -typeStr $typeStr -indent "`t`t`t" + X "`t`t</valueType>" + } + if ($restrictObj -or $restrictTokens.Count -gt 0) { + X "`t`t<useRestriction>" + if ($restrictObj) { + foreach ($prop in $restrictObj.PSObject.Properties) { + if ($prop.Value -eq $true) { + X "`t`t`t<$($prop.Name)>true</$($prop.Name)>" + } + } + } else { + foreach ($r in $restrictTokens) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { X "`t`t`t<$xmlName>true</$xmlName>" } + } + } + X "`t`t</useRestriction>" + } + if ($appearance) { + X "`t`t<appearance>" + foreach ($prop in $appearance.PSObject.Properties) { + # ГоризонтальноеПоложение — особый xsi:type (если не multilang) + if ($prop.Name -eq "ГоризонтальноеПоложение" -and -not ($prop.Value -is [hashtable] -or $prop.Value -is [System.Collections.IDictionary] -or $prop.Value -is [PSCustomObject])) { + X "`t`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + X "`t`t`t`t<dcscor:parameter>$(Esc-Xml $prop.Name)</dcscor:parameter>" + X "`t`t`t`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml "$($prop.Value)")</dcscor:value>" + X "`t`t`t</dcscor:item>" + } else { + Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "`t`t`t" + } + } + X "`t`t</appearance>" + } + + X "`t</calculatedField>" + } +} + +# === TotalFields === +function Emit-TotalFields { + if (-not $def.totalFields) { return } + foreach ($tf in $def.totalFields) { + if ($tf -is [string]) { + $parsed = Parse-TotalShorthand $tf + } else { + $parsed = @{ + dataPath = "$($tf.dataPath)" + expression = "$($tf.expression)" + } + if ($tf.group) { $parsed.groups = @($tf.group) } + } + + X "`t<totalField>" + X "`t`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" + X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>" + if ($parsed.groups) { + foreach ($g in $parsed.groups) { + X "`t`t<group>$(Esc-Xml "$g")</group>" + } + } + X "`t</totalField>" + } +} + +# === Parameters === + +function Emit-SingleParam { + param($p, $parsed) + + X "`t<parameter>" + X "`t`t<name>$(Esc-Xml $parsed.name)</name>" + + # Title (from parsed first, then from object form; accept `presentation` as + # a synonym — 1C UI labels a parameter's caption "Представление"). + $title = "" + if ($parsed.title) { + $title = $parsed.title + } elseif ($p -isnot [string] -and $p.title) { + $title = $p.title + } elseif ($p -isnot [string] -and $p.presentation) { + $title = $p.presentation + } + if ($title) { + Emit-MLText -tag "title" -text $title -indent "`t`t" + } + + # ValueType + if ($parsed.type) { + X "`t`t<valueType>" + Emit-ValueType -typeStr $parsed.type -indent "`t`t`t" + X "`t`t</valueType>" + } + + # Value — for valueListAllowed params Designer omits <value> when empty + $vla = [bool]$parsed.valueListAllowed + # Multi-value (массив значений по умолчанию для valueListAllowed-параметра) — эмитим + # каждый отдельным <value>. Различаем массив значений от composite type (тоже array, + # но в parsed.type). + $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) + if ($parsed.type -is [array] -or $parsed.type -is [System.Collections.IList]) { + # Composite type — Designer writes xsi:nil for any empty composite; + # non-empty composite values are uncommon and would need per-type tagging. + if (Test-EmptyValue $parsed.value) { + if (-not $vla) { X "`t`t<value xsi:nil=`"true`"/>" } + } + } elseif ($parsed.nilValue -eq $true) { + # Принудительный xsi:nil даже когда тип известен (для bit-perfect round-trip). + if (-not $vla) { X "`t`t<value xsi:nil=`"true`"/>" } + } elseif ($valIsArray) { + foreach ($v in @($parsed.value)) { + Emit-ParamValue -type $parsed.type -val $v -indent "`t`t" -valueListAllowed $false + } + } else { + Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla + } + + # Hidden implies useRestriction=true + availableAsField=false + if ($parsed.hidden -eq $true) { + $parsed.availableAsField = $false + $parsed.useRestriction = $true + } + + # UseRestriction — платформа всегда эмитит этот тег у параметра (true/false) + $urEmit = $false + if ($parsed.useRestriction -eq $true) { $urEmit = $true } + elseif ($p -isnot [string] -and $p.useRestriction -eq $true) { $urEmit = $true } + X ("`t`t<useRestriction>" + $(if ($urEmit) { 'true' } else { 'false' }) + "</useRestriction>") + + # Expression + if ($parsed.expression) { + X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>" + } + + # AvailableAsField + if ($parsed.availableAsField -eq $false) { + X "`t`t<availableAsField>false</availableAsField>" + } + + # ValueListAllowed + if ($parsed.valueListAllowed -eq $true) { + X "`t`t<valueListAllowed>true</valueListAllowed>" + } + + # AvailableValues + if ($p -isnot [string] -and $p.availableValues) { + foreach ($av in $p.availableValues) { + X "`t`t<availableValue>" + if (Test-EmptyValue $av.value) { + Emit-EmptyValue -type $parsed.type -indent "`t`t`t" -tagPrefix "" -valueListAllowed $false + } else { + $av_v = $av.value + if ($av_v -is [bool]) { + $bv = "$av_v".ToLower() + X "`t`t`t<value xsi:type=`"xs:boolean`">$bv</value>" + } elseif ($av_v -is [int] -or $av_v -is [long] -or $av_v -is [double]) { + X "`t`t`t<value xsi:type=`"xs:decimal`">$av_v</value>" + } else { + $avVal = "$av_v" + $avType = "xs:string" + if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { + $avType = "dcscor:DesignTimeValue" + } + X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>" + } + } + # `title` accepted as synonym of `presentation` — both map to the same UI label. + $avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" } + if ($avPres) { + Emit-MLText -tag "presentation" -text $avPres -indent "`t`t`t" + } + X "`t`t</availableValue>" + } + } + + # DenyIncompleteValues + $deny = $parsed.denyIncompleteValues -eq $true -or ( + $null -ne $p -and $p -isnot [string] -and $p.denyIncompleteValues -eq $true) + if ($deny) { + X "`t`t<denyIncompleteValues>true</denyIncompleteValues>" + } + + # Use — object form wins, else parsed (set by @autoDates default) + $useVal = $null + if ($null -ne $p -and $p -isnot [string] -and $p.use) { $useVal = "$($p.use)" } + elseif ($parsed.use) { $useVal = "$($parsed.use)" } + if ($useVal) { + X "`t`t<use>$(Esc-Xml $useVal)</use>" + } + + # InputParameters на параметре (ФорматРедактирования и т.п.) + if ($null -ne $p -and $p -isnot [string] -and $p.inputParameters) { + Emit-InputParameters -ip $p.inputParameters -indent "`t`t" + } + + X "`t</parameter>" +} + +$script:allParams = @() + +function Emit-Parameters { + if (-not $def.parameters) { return } + foreach ($p in $def.parameters) { + if ($p -is [string]) { + $parsed = Parse-ParamShorthand $p + } else { + # Composite type: ["string(10,fix)", "CatalogRef.X"] → array of resolved + # strings; emit-valueType handles arrays, empty value falls through to nil. + $resolvedType = "" + if ($p.type) { + if ($p.type -is [array] -or $p.type -is [System.Collections.IList]) { + $resolvedType = @($p.type | ForEach-Object { Resolve-TypeStr "$_" }) + } else { + $resolvedType = Resolve-TypeStr "$($p.type)" + } + } + $parsed = @{ + name = "$($p.name)" + type = $resolvedType + value = $p.value + autoDates = $false + } + if ($p.expression) { $parsed.expression = "$($p.expression)" } + if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false } + if ($p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } + if ($p.hidden -eq $true) { $parsed.hidden = $true } + if ($p.autoDates -eq $true) { $parsed.autoDates = $true } + if ($p.nilValue -eq $true) { $parsed.nilValue = $true } + } + + # @autoDates implies use=Always + denyIncompleteValues=true by default + # (derived &НачалоПериода/&КонецПериода need a populated period). + # Explicit values in object form override these defaults. + if ($parsed.autoDates) { + $isObj = ($p -isnot [string]) -and ($null -ne $p) + if (-not ($isObj -and $null -ne $p.use)) { $parsed.use = 'Always' } + if (-not ($isObj -and $null -ne $p.denyIncompleteValues)) { $parsed.denyIncompleteValues = $true } + } + + Emit-SingleParam -p $p -parsed $parsed + + # Track parameter for auto dataParameters + $script:allParams += @{ name = $parsed.name; hidden = [bool]$parsed.hidden; type = "$($parsed.type)"; value = $parsed.value } + + # @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern). + # type=dateTime + DateFractions=DateTime — иначе КонецПериода обрезается до 00:00:00 + # и запрос `Дата МЕЖДУ &НачалоПериода И &КонецПериода` теряет данные за последний день. + if ($parsed.autoDates) { + $paramName = $parsed.name + $beginParsed = @{ + name = "НачалоПериода"; title = "Начало периода" + type = "dateTime"; value = "0001-01-01T00:00:00" + useRestriction = $true + expression = "&$paramName.ДатаНачала" + } + Emit-SingleParam -p $null -parsed $beginParsed + $endParsed = @{ + name = "КонецПериода"; title = "Конец периода" + type = "dateTime"; value = "0001-01-01T00:00:00" + useRestriction = $true + expression = "&$paramName.ДатаОкончания" + } + Emit-SingleParam -p $null -parsed $endParsed + } + } +} + +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} + +function Emit-EmptyValue { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) + + if ($valueListAllowed) { return } + $t = if ($null -eq $type) { "" } else { "$type" } + # Нормализация: убираем префикс xs: (валидный для valueType из decompile/DSL) + $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } + $pf = $tagPrefix + + if ($t -eq "") { + X "$indent<${pf}value xsi:nil=`"true`"/>" + } elseif ($t -eq "StandardPeriod") { + X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" + X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + X "$indent</${pf}value>" + } elseif ($tBare -match '^string') { + X "$indent<${pf}value xsi:type=`"xs:string`"/>" + } elseif ($tBare -match '^(date|time)') { + X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" + } elseif ($tBare -match '^decimal') { + X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" + } elseif ($tBare -eq "boolean") { + X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" + } else { + # Ref types or unknown — safe nil + X "$indent<${pf}value xsi:nil=`"true`"/>" + } +} + +function Emit-ParamValue { + param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) + + if (Test-EmptyValue $val) { + Emit-EmptyValue -type $type -indent $indent -tagPrefix "" -valueListAllowed $valueListAllowed + return + } + + # val может быть строкой (variant only) или объектом {variant, startDate?, endDate?}. + $valIsDict = ($val -is [hashtable]) -or ($val -is [System.Collections.IDictionary]) -or ($val -is [PSCustomObject]) + $variantStr = $null + $sdStr = $null + $edStr = $null + if ($valIsDict) { + if ($val -is [PSCustomObject]) { + if ($val.PSObject.Properties['variant']) { $variantStr = "$($val.variant)" } + if ($val.PSObject.Properties['startDate']) { $sdStr = "$($val.startDate)" } + if ($val.PSObject.Properties['endDate']) { $edStr = "$($val.endDate)" } + } else { + if ($val.Contains('variant')) { $variantStr = "$($val['variant'])" } + if ($val.Contains('startDate')) { $sdStr = "$($val['startDate'])" } + if ($val.Contains('endDate')) { $edStr = "$($val['endDate'])" } + } + } + $valStr = if ($variantStr) { $variantStr } else { "$val" } + + if ($type -eq "StandardPeriod") { + # Platform-pattern: startDate/endDate эмитятся ТОЛЬКО для variant=Custom. + # Для всех остальных вариантов (ThisMonth, LastYear, Today, ...) — без дат. + X "$indent<value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>" + if ($valStr -eq 'Custom') { + $sdOut = if ($sdStr) { $sdStr } else { '0001-01-01T00:00:00' } + $edOut = if ($edStr) { $edStr } else { '0001-01-01T00:00:00' } + X "$indent`t<v8:startDate>$(Esc-Xml $sdOut)</v8:startDate>" + X "$indent`t<v8:endDate>$(Esc-Xml $edOut)</v8:endDate>" + } + X "$indent</value>" + } elseif ($type -match '^date') { + X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>" + } elseif ($type -eq "boolean") { + X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>" + } elseif ($type -match '^decimal') { + X "$indent<value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</value>" + } elseif ($type -match '^string') { + X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>" + } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { + X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>" + } else { + # Guess from value + if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>" + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>" + } elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>" + } else { + X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>" + } + } +} + +# === AreaTemplate DSL === + +# Built-in style presets +$script:areaStylePresets = @{ + none = @{ + font = $null; fontSize = $null; bold = $false; italic = $false + hAlign = $null; vAlign = $null; wrap = $false + bgColor = $null; textColor = $null + borderColor = $null; borders = $false + } + data = @{ + font = 'Arial'; fontSize = 10; bold = $false; italic = $false + hAlign = $null; vAlign = $null; wrap = $false + bgColor = 'style:ReportGroup1BackColor'; textColor = $null + borderColor = 'style:ReportLineColor'; borders = $true + } + header = @{ + font = 'Arial'; fontSize = 10; bold = $false; italic = $false + hAlign = 'Center'; vAlign = $null; wrap = $true + bgColor = 'style:ReportHeaderBackColor'; textColor = $null + borderColor = 'style:ReportLineColor'; borders = $true + } + subheader = @{ + font = 'Arial'; fontSize = 10; bold = $false; italic = $false + hAlign = 'Center'; vAlign = $null; wrap = $true + bgColor = $null; textColor = $null + borderColor = 'style:ReportLineColor'; borders = $true + } + total = @{ + font = 'Arial'; fontSize = 10; bold = $false; italic = $false + hAlign = $null; vAlign = $null; wrap = $false + bgColor = $null; textColor = $null + borderColor = 'style:ReportLineColor'; borders = $true + } +} + +# Load user presets from skd-styles.json +# Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/ +$script:userStylesLoaded = $false +$searchPaths = @( + (Join-Path $script:queryBaseDir "skd-styles.json"), + (Join-Path (Get-Location).Path "skd-styles.json") +) +$outResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location).Path $OutputPath } +$scanDir = [System.IO.Path]::GetDirectoryName($outResolved) +while ($scanDir) { + $searchPaths += Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "skd") "skd-styles.json" + $parentDir = Split-Path $scanDir -Parent + if ($parentDir -eq $scanDir) { break } + $scanDir = $parentDir +} +foreach ($stylesFile in $searchPaths) { + if (Test-Path $stylesFile) { + $userStyles = Get-Content -Raw -Encoding UTF8 $stylesFile | ConvertFrom-Json + foreach ($prop in $userStyles.PSObject.Properties) { + $preset = @{} + # Start from 'data' defaults + foreach ($k in $script:areaStylePresets['data'].Keys) { + $preset[$k] = $script:areaStylePresets['data'][$k] + } + # If overriding existing preset, start from it instead + if ($script:areaStylePresets.ContainsKey($prop.Name)) { + foreach ($k in $script:areaStylePresets[$prop.Name].Keys) { + $preset[$k] = $script:areaStylePresets[$prop.Name][$k] + } + } + # Apply user overrides + foreach ($up in $prop.Value.PSObject.Properties) { + $preset[$up.Name] = $up.Value + } + $script:areaStylePresets[$prop.Name] = $preset + } + $script:userStylesLoaded = $true + break + } +} + +function Emit-ColorValue { + param([string]$color, [string]$indent) + # Префиксы style:/web:/win: → соответствующий xmlns + dN:Name + $colorPrefixToUri = @{ + 'style:' = 'http://v8.1c.ru/8.1/data/ui/style' + 'web:' = 'http://v8.1c.ru/8.1/data/ui/colors/web' + 'win:' = 'http://v8.1c.ru/8.1/data/ui/colors/windows' + } + foreach ($pfx in $colorPrefixToUri.Keys) { + if ($color.StartsWith($pfx)) { + $name = $color.Substring($pfx.Length) + $uri = $colorPrefixToUri[$pfx] + X "$indent<dcscor:value xmlns:d8p1=`"$uri`" xsi:type=`"v8ui:Color`">d8p1:$name</dcscor:value>" + return + } + } + X "$indent<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $color)</dcscor:value>" +} + +function Emit-CellAppearance { + param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @()) + $ind = "`t`t`t`t`t`t" + # Если ничего внутри appearance не будет — не эмитим блок вовсе + # (оригинал платформы для cells без атрибутов не пишет <appearance></appearance>). + $hasContent = $style.bgColor -or $style.textColor -or $style.borders -or $style.font -or ` + $style.hAlign -or $style.vAlign -or $style.wrap -or ` + ($width -gt 0) -or ($minHeight -gt 0) -or $vMerge -or $hMerge -or ` + ($extraItems -and @($extraItems).Count -gt 0) + if (-not $hasContent) { return } + X "`t`t`t`t`t<dcsat:appearance>" + # Background color + if ($style.bgColor) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ЦветФона</dcscor:parameter>" + Emit-ColorValue $style.bgColor "$ind`t" + X "$ind</dcscor:item>" + } + # Text color + if ($style.textColor) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ЦветТекста</dcscor:parameter>" + Emit-ColorValue $style.textColor "$ind`t" + X "$ind</dcscor:item>" + } + # Border color + border style (4 sides) + if ($style.borders) { + if ($style.borderColor) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ЦветГраницы</dcscor:parameter>" + Emit-ColorValue $style.borderColor "$ind`t" + X "$ind</dcscor:item>" + } + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>СтильГраницы</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"0`" gap=`"false`">" + X "$ind`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">None</v8ui:style>" + X "$ind`t</dcscor:value>" + foreach ($side in @('Слева','Сверху','Справа','Снизу')) { + X "$ind`t<dcscor:item>" + X "$ind`t`t<dcscor:parameter>СтильГраницы.$side</dcscor:parameter>" + X "$ind`t`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"1`" gap=`"false`">" + X "$ind`t`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>" + X "$ind`t`t</dcscor:value>" + X "$ind`t</dcscor:item>" + } + X "$ind</dcscor:item>" + } + # Font (skip if style has no font configured — for "none" preset) + if ($style.font) { + $boldStr = if ($style.bold) { "true" } else { "false" } + $italicStr = if ($style.italic) { "true" } else { "false" } + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>Шрифт</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"v8ui:Font`" faceName=`"$($style.font)`" height=`"$($style.fontSize)`" bold=`"$boldStr`" italic=`"$italicStr`" underline=`"false`" strikeout=`"false`" kind=`"Absolute`" scale=`"100`"/>" + X "$ind</dcscor:item>" + } + # Horizontal alignment + if ($style.hAlign) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ГоризонтальноеПоложение</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml $style.hAlign)</dcscor:value>" + X "$ind</dcscor:item>" + } + # Vertical alignment + if ($style.vAlign) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ВертикальноеПоложение</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"v8ui:VerticalAlign`">$(Esc-Xml $style.vAlign)</dcscor:value>" + X "$ind</dcscor:item>" + } + # Text placement (wrap) + if ($style.wrap) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>Размещение</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"dcscor:DataCompositionTextPlacementType`">Wrap</dcscor:value>" + X "$ind</dcscor:item>" + } + # Width + if ($width -gt 0) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>МинимальнаяШирина</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>" + X "$ind</dcscor:item>" + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>МаксимальнаяШирина</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>" + X "$ind</dcscor:item>" + } + # Min height + if ($minHeight -gt 0) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>МинимальнаяВысота</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$minHeight</dcscor:value>" + X "$ind</dcscor:item>" + } + # Vertical merge + if ($vMerge) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ОбъединятьПоВертикали</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>" + X "$ind</dcscor:item>" + } + # Horizontal merge + if ($hMerge) { + X "$ind<dcscor:item>" + X "$ind`t<dcscor:parameter>ОбъединятьПоГоризонтали</dcscor:parameter>" + X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>" + X "$ind</dcscor:item>" + } + # Extra appearance items (e.g. drilldown Расшифровка) + foreach ($ei in $extraItems) { X $ei } + X "`t`t`t`t`t</dcsat:appearance>" +} + +# Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}. +# Helpers извлекают значение и эффективный стиль ячейки. +function Get-CellValue { + param($cell) + if ($null -eq $cell) { return $null } + if ($cell -is [string]) { return $cell } + if ($cell -is [hashtable] -or $cell -is [System.Collections.IDictionary]) { + if ($cell.Contains('value')) { return $cell['value'] } + return $cell # multilang dict без обёртки + } + if ($cell.PSObject -and $cell.PSObject.Properties['value']) { return $cell.value } + # PSCustomObject без 'value' — это multilang dict ({ru, en, ...}), отдаём как есть + if ($cell -is [PSCustomObject]) { return $cell } + return $null +} + +function Get-CellStyleOrDefault { + param($cell, $defaultStyle) + if ($null -ne $cell -and -not ($cell -is [string]) -and $cell.PSObject -and $cell.PSObject.Properties['style']) { + $sName = "$($cell.style)" + if ($script:areaStylePresets.ContainsKey($sName)) { + return $script:areaStylePresets[$sName] + } + Write-Warning "Unknown cell style preset '$sName', falling back to template default" + } + return $defaultStyle +} + +function Emit-AreaTemplateDSL { + param($t) + $styleName = if ($t.style) { "$($t.style)" } else { "data" } + if (-not $script:areaStylePresets.ContainsKey($styleName)) { + Write-Warning "Unknown area style preset '$styleName', falling back to 'data'" + $styleName = "data" + } + $style = $script:areaStylePresets[$styleName] + + $rows = @($t.rows) + # PS-quirk: if-expression unwraps single-element @() результат + # (`$x = if (...) { @($arr) }` даёт скаляр при одном элементе). + # Используем обычный if вместо if-expression. + $widths = @() + if ($t.widths) { $widths = @($t.widths) } + $minHeight = if ($t.minHeight) { [double]$t.minHeight } else { 0 } + $colCount = if ($widths.Count -gt 0) { $widths.Count } else { $rows[0].Count } + + # Build vertical merge map: vMerge[row][col] = $true if cell is merged with above + $vMerge = @{} + for ($r = $rows.Count - 1; $r -ge 1; $r--) { + $vMerge[$r] = @{} + for ($c = 0; $c -lt $colCount; $c++) { + $cellValStr = Get-CellValue $rows[$r][$c] + if ($cellValStr -eq '|') { $vMerge[$r][$c] = $true } + } + } + if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} } + + # Build horizontal merge map: hMerge[row][col] = $true if cell is merged with left + $hMerge = @{} + for ($r = 0; $r -lt $rows.Count; $r++) { + $hMerge[$r] = @{} + for ($c = 0; $c -lt $colCount; $c++) { + $cellValStr = Get-CellValue $rows[$r][$c] + if ($cellValStr -eq '>') { $hMerge[$r][$c] = $true } + } + } + + # Build drilldown map: param_name -> drilldown_value (только для shortcut-формы — drilldown:string). + # Форма C (drilldown:object) — DetailsAreaTemplateParameter с произвольным именем, в map не идёт. + $drilldownMap = @{} + if ($t.parameters) { + foreach ($tp in $t.parameters) { + if ($tp.drilldown -and ($tp.drilldown -is [string])) { + $drilldownMap["$($tp.name)"] = "$($tp.drilldown)" + } + } + } + + X "`t<template>" + X "`t`t<name>$(Esc-Xml "$($t.name)")</name>" + X "`t`t<template xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:AreaTemplate`">" + + for ($r = 0; $r -lt $rows.Count; $r++) { + X "`t`t`t<dcsat:item xsi:type=`"dcsat:TableRow`">" + for ($c = 0; $c -lt $colCount; $c++) { + $cellRaw = $rows[$r][$c] + $cellVal = Get-CellValue $cellRaw + $cellStyle = Get-CellStyleOrDefault $cellRaw $style + $w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 } + $isVMerged = $vMerge[$r][$c] -eq $true + $isHMerged = $hMerge[$r][$c] -eq $true + X "`t`t`t`t<dcsat:tableCell>" + if ($isVMerged) { + # Vertically merged cell — only appearance with vMerge flag + width + Emit-CellAppearance $cellStyle $w $true + } elseif ($isHMerged) { + # Horizontally merged cell — only appearance with hMerge flag + width + Emit-CellAppearance $cellStyle $w $false $true + } else { + # Cell value + $cellIsDict = ($cellVal -is [hashtable]) -or ($cellVal -is [System.Collections.IDictionary]) -or ($cellVal -is [PSCustomObject]) + if ($cellIsDict) { + # Multilang static text — эмитим напрямую с lwsTitle-подобной структурой + X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" + Emit-MLText -tag "dcsat:value" -text $cellVal -indent "`t`t`t`t`t`t" + X "`t`t`t`t`t</dcsat:item>" + $cellExtraItems = @() + } elseif ($null -ne $cellVal -and $cellVal -ne '') { + $cellStr = "$cellVal" + # Unescape \| and \> + if ($cellStr -eq '\|') { $cellStr = '|' } + elseif ($cellStr -eq '\>') { $cellStr = '>' } + if ($cellStr -match '^\{(.+)\}$') { + # Parameter reference + $paramName = $Matches[1] + X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" + X "`t`t`t`t`t`t<dcsat:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $paramName)</dcsat:value>" + X "`t`t`t`t`t</dcsat:item>" + # Build drilldown appearance extra items. + # Приоритет: per-cell override (cell={value, drilldown}) → drilldownMap (shortcut form B). + $cellExtraItems = @() + $cellDrillOverride = $null + if ($cellRaw -is [PSCustomObject] -and $cellRaw.PSObject.Properties['drilldown']) { + $cellDrillOverride = "$($cellRaw.drilldown)" + } elseif (($cellRaw -is [hashtable] -or $cellRaw -is [System.Collections.IDictionary]) -and $cellRaw.Contains('drilldown')) { + $cellDrillOverride = "$($cellRaw['drilldown'])" + } + $ddTarget = $null + if ($cellDrillOverride) { + $ddTarget = $cellDrillOverride + } elseif ($drilldownMap.ContainsKey($paramName)) { + $ddTarget = "Расшифровка_$($drilldownMap[$paramName])" + } + if ($ddTarget) { + $cellExtraItems += "`t`t`t`t`t`t<dcscor:item>" + $cellExtraItems += "`t`t`t`t`t`t`t<dcscor:parameter>Расшифровка</dcscor:parameter>" + $cellExtraItems += "`t`t`t`t`t`t`t<dcscor:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $ddTarget)</dcscor:value>" + $cellExtraItems += "`t`t`t`t`t`t</dcscor:item>" + } + } else { + # Static text + X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">" + Emit-MLText -tag "dcsat:value" -text $cellStr -indent "`t`t`t`t`t`t" + X "`t`t`t`t`t</dcsat:item>" + } + } + # Appearance + $h = if ($r -eq 0) { $minHeight } else { 0 } + if (-not $cellExtraItems) { $cellExtraItems = @() } + Emit-CellAppearance $cellStyle $w $false $false $h $cellExtraItems + $cellExtraItems = @() + } + X "`t`t`t`t</dcsat:tableCell>" + } + X "`t`t`t</dcsat:item>" + } + + X "`t`t</template>" + # Parameters (reuse existing logic) + if ($t.parameters) { + foreach ($tp in $t.parameters) { + Emit-AreaTemplateParameter -tp $tp -indent "`t`t" + } + } + X "`t</template>" +} + +# Эмиссия одного параметра шаблона. Различает три формы: +# A. { name, expression } → ExpressionAreaTemplateParameter +# B. { name, expression, drilldown: "X" } → Expression + Details(Расшифровка_X, ИмяРесурса, DrillDown) [shortcut] +# C. { name, drilldown: { field, expression, action? } } → DetailsAreaTemplateParameter с произвольным name +function Emit-AreaTemplateParameter { + param($tp, [string]$indent) + # Определяем форму C: drilldown — объект с полем field или expression. + $dd = $tp.drilldown + $ddIsObject = $false + if ($null -ne $dd) { + if ($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) { $ddIsObject = $true } + elseif ($dd -is [PSCustomObject]) { $ddIsObject = $true } + } + if ($ddIsObject) { + # Форма C + $ddField = if ($dd -is [PSCustomObject]) { "$($dd.field)" } else { "$($dd['field'])" } + $ddExpr = if ($dd -is [PSCustomObject]) { "$($dd.expression)" } else { "$($dd['expression'])" } + $ddActV = $null + if ($dd -is [PSCustomObject] -and $dd.PSObject.Properties['action']) { $ddActV = "$($dd.action)" } + elseif (($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) -and $dd.Contains('action')) { $ddActV = "$($dd['action'])" } + $ddAct = if ($ddActV) { $ddActV } else { 'DrillDown' } + X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" + X "$indent`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>" + X "$indent`t<dcsat:fieldExpression>" + X "$indent`t`t<dcsat:field>$(Esc-Xml $ddField)</dcsat:field>" + X "$indent`t`t<dcsat:expression>$(Esc-Xml $ddExpr)</dcsat:expression>" + X "$indent`t</dcsat:fieldExpression>" + X "$indent`t<dcsat:mainAction>$(Esc-Xml $ddAct)</dcsat:mainAction>" + X "$indent</parameter>" + return + } + # Форма A или B + X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:ExpressionAreaTemplateParameter`">" + X "$indent`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>" + X "$indent`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>" + X "$indent</parameter>" + if ($dd -and ($dd -is [string])) { + # Форма B: shortcut Расшифровка_<X> + ИмяРесурса + DrillDown + $ddVal = "$dd" + X "$indent<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" + X "$indent`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>" + X "$indent`t<dcsat:fieldExpression>" + X "$indent`t`t<dcsat:field>ИмяРесурса</dcsat:field>" + X "$indent`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>" + X "$indent`t</dcsat:fieldExpression>" + X "$indent`t<dcsat:mainAction>DrillDown</dcsat:mainAction>" + X "$indent</parameter>" + } +} + +# === Templates === +function Emit-Templates { + if (-not $def.templates) { return } + foreach ($t in $def.templates) { + if ($t.rows) { + # Compact DSL mode + Emit-AreaTemplateDSL $t + } else { + # Raw XML mode + X "`t<template>" + X "`t`t<name>$(Esc-Xml "$($t.name)")</name>" + if ($t.template) { + X "`t`t$($t.template)" + } + if ($t.parameters) { + foreach ($tp in $t.parameters) { + Emit-AreaTemplateParameter -tp $tp -indent "`t`t" + } + } + X "`t</template>" + } + } +} + +# === FieldTemplates === +# Привязка <fieldTemplate><field/><template/></fieldTemplate> поля к именованному area-template. +# DSL: "fieldTemplates": [{ "field": "X", "template": "Макет1" }, ...] +function Emit-FieldTemplates { + if (-not $def.fieldTemplates) { return } + foreach ($ft in $def.fieldTemplates) { + X "`t<fieldTemplate>" + X "`t`t<field>$(Esc-Xml "$($ft.field)")</field>" + X "`t`t<template>$(Esc-Xml "$($ft.template)")</template>" + X "`t</fieldTemplate>" + } +} + +# === GroupTemplates === +function Emit-GroupTemplates { + if (-not $def.groupTemplates) { return } + foreach ($gt in $def.groupTemplates) { + $ttype = if ($gt.templateType) { "$($gt.templateType)" } else { "Header" } + $isHeader = ($ttype -eq 'GroupHeader') + $tag = if ($isHeader) { 'groupHeaderTemplate' } else { 'groupTemplate' } + $xmlTType = if ($isHeader) { 'Header' } else { $ttype } + + X "`t<$tag>" + if ($gt.groupName) { + X "`t`t<groupName>$(Esc-Xml "$($gt.groupName)")</groupName>" + } elseif ($gt.groupField) { + X "`t`t<groupField>$(Esc-Xml "$($gt.groupField)")</groupField>" + } + X "`t`t<templateType>$(Esc-Xml $xmlTType)</templateType>" + X "`t`t<template>$(Esc-Xml "$($gt.template)")</template>" + X "`t</$tag>" + } +} + +# === Settings Variants === + +function Emit-SelectionItem { + param($item, [string]$indent) + if ($item -is [string]) { + if ($item -eq "Auto") { + X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" + } else { + X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" + X "$indent`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>" + X "$indent</dcsset:item>" + } + return + } + # Object form: { auto: true, use: false } — отключённый Auto в selection + if ($item.auto -eq $true) { + X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`">" + if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } + X "$indent</dcsset:item>" + return + } + if ($item.folder -or (Has-JsonProp $item 'folder')) { + X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">" + # Optional <dcsset:field> на folder (редкий случай, для round-trip-целостности) + if ($item.field) { + X "$indent`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" + } + Emit-MLText -tag "dcsset:lwsTitle" -text $item.folder -indent "$indent`t" -NoXsiType + foreach ($sub in $item.items) { + Emit-SelectionItem -item $sub -indent "$indent`t" + } + $pl = if ($item.placement) { "$($item.placement)" } else { 'Auto' } + X "$indent`t<dcsset:placement>$(Esc-Xml $pl)</dcsset:placement>" + X "$indent</dcsset:item>" + return + } + # field with optional title / use=false / viewMode + X "$indent<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" + if ($item.use -eq $false) { + X "$indent`t<dcsset:use>false</dcsset:use>" + } + X "$indent`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" + if ($item.title) { + Emit-MLText -tag "dcsset:lwsTitle" -text $item.title -indent "$indent`t" -NoXsiType + } + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + X "$indent</dcsset:item>" +} + +function Emit-Selection { + param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) + + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + + X "$indent<dcsset:selection>" + foreach ($item in $items) { + if ($skipAuto -and ($item -is [string]) -and $item -eq 'Auto') { continue } + Emit-SelectionItem -item $item -indent "$indent`t" + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:selection>" +} + +function Emit-FilterItem { + param($item, [string]$indent) + + if ($item.group) { + # FilterItemGroup + $groupType = switch ("$($item.group)") { + "And" { "AndGroup" } + "Or" { "OrGroup" } + "Not" { "NotGroup" } + default { "$($item.group)Group" } + } + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" + X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>" + if ($item.items) { + foreach ($sub in $item.items) { + if ($sub -is [string]) { + $parsed = Parse-FilterShorthand $sub + $obj = @{ field = $parsed.field; op = $parsed.op } + if ($parsed.use -eq $false) { $obj.use = $false } + if ($null -ne $parsed.value) { $obj.value = $parsed.value } + if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } + if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } + if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } + $sub = [pscustomobject]$obj + } + Emit-FilterItem -item $sub -indent "$indent`t" + } + } + if ($item.presentation) { + Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" + } + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + if ($item.userSettingID) { + $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" + } + X "$indent</dcsset:item>" + return + } + + # FilterItemComparison + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" + + if ($item.use -eq $false) { + X "$indent`t<dcsset:use>false</dcsset:use>" + } + + X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>" + + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>" + + # Right value: один, несколько (InList) или ValueListType (пустой list-placeholder) + $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) + if ($valIsArray) { + # Пустой массив → пустой ValueListType placeholder + if (@($item.value).Count -eq 0) { + X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">" + X "$indent`t`t<v8:valueType/>" + X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>" + X "$indent`t</dcsset:right>" + } else { + # Несколько <right> подряд (multi-value InList) + foreach ($v in $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + if ($v -is [bool]) { $vt = 'xs:boolean' } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } + elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } + elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } + elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } + else { $vt = 'xs:string' } + } + $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } + X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" + } + } + } elseif ($null -ne $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + $v = $item.value + if ($v -is [bool]) { + $vt = "xs:boolean" + } elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { + $vt = "xs:decimal" + } elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { + $vt = "xs:dateTime" + } elseif ("$v" -match '^-?\d+(\.\d+)?$') { + $vt = "xs:decimal" + } elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + $vt = "dcscor:DesignTimeValue" + } else { + $vt = "xs:string" + } + } + $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } + X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" + } + + if ($item.presentation) { + Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" + } + + # viewMode эмитим только если явно задан — присутствие в XML контекстно + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + + if ($item.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" + } + + X "$indent</dcsset:item>" +} + +function Emit-Filter { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) + + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + + X "$indent<dcsset:filter>" + foreach ($item in $items) { + if ($item -is [string]) { + # Parse shorthand: "Организация = _ @off @user" + $parsed = Parse-FilterShorthand $item + $filterObj = New-Object PSObject + $filterObj | Add-Member -NotePropertyName "field" -NotePropertyValue $parsed.field + $filterObj | Add-Member -NotePropertyName "op" -NotePropertyValue $parsed.op + if ($parsed.use -eq $false) { + $filterObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false + } + if ($null -ne $parsed.value) { + $filterObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value + } + if ($parsed["valueType"]) { + $filterObj | Add-Member -NotePropertyName "valueType" -NotePropertyValue $parsed["valueType"] + } + if ($parsed.userSettingID) { + $filterObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID + } + if ($parsed.viewMode) { + $filterObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode + } + Emit-FilterItem -item $filterObj -indent "$indent`t" + } else { + Emit-FilterItem -item $item -indent "$indent`t" + } + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:filter>" +} + +function Emit-Order { + param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) + + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + + X "$indent<dcsset:order>" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + if (-not $skipAuto) { + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" + } + } else { + $parts = $item -split '\s+' + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)desc$') { $dir = "Desc" } + elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)asc$') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + X "$indent`t</dcsset:item>" + } + } else { + # Object form: { field, direction, viewMode } + if ($item.field -eq "Auto" -or $item.type -eq "auto") { + if (-not $skipAuto) { + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" + } + continue + } + $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } + if ($dir -match '^(?i)desc$') { $dir = "Desc" } elseif ($dir -match '^(?i)asc$') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + if ($item.use -eq $false) { + X "$indent`t`t<dcsset:use>false</dcsset:use>" + } + X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + if ($item.viewMode) { + X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + X "$indent`t</dcsset:item>" + } + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:order>" +} + +function Emit-AppearanceValue { + param([string]$key, $val, [string]$indent) + + X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + + # Helper для проверки property/key на PSCustomObject/IDictionary + function _HasKey { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } + if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } + return $false + } + function _Get { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return $o.$k } + if ($o -is [System.Collections.IDictionary]) { return $o[$k] } + return $null + } + + # Распознаём wrapper {value:..., use?:false, items?:{}}. + # Top-level Line-value хранится плоско ({@type:Line, width, gap, style, use?, items?}) — + # отличаем от wrapper по наличию @type на самом val. + $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') + $useWrapper = $false + $innerVal = $val + $nestedItems = $null + if ($isTopLevelLine) { + # items/use лежат рядом с @type + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { + # Обычный wrapper {value, use?, items?} + $innerVal = (_Get $val 'value') + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } + + if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" } + X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + + # Font dict ({@type: "Font", ref, faceName, height, bold, ...}) → <dcscor:value xsi:type="v8ui:Font" .../> + $isFontDict = $false + if ($innerVal -is [PSCustomObject]) { + $tProp = $innerVal.PSObject.Properties['@type'] + if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } + } elseif ($innerVal -is [System.Collections.IDictionary]) { + if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } + } + # Line dict ({@type: "Line", width, gap, style}) → <dcscor:value xsi:type="v8ui:Line" ...><v8ui:style>... + $isLineDict = $false + if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } + $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) + if ($isLineDict) { + $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } + $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } + $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">" + X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>" + X "$indent`t</dcscor:value>" + } elseif ($isFontDict) { + $attrParts = @() + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $null + if ($innerVal -is [PSCustomObject]) { + $ap = $innerVal.PSObject.Properties[$attrName] + if ($ap) { $av = $ap.Value } + } else { + if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } + } + if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } + } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" + } elseif ($isDict) { + # Multilang dict ({"ru": "...", "en": "..."}) → LocalStringType независимо от ключа. + Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" + } else { + $actualVal = "$innerVal" + # Параметр-специфичный тип для известных appearance keys + $keyTypeMap = @{ + 'Размещение' = 'dcscor:DataCompositionTextPlacementType' + 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' + 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' + 'ОриентацияТекста' = 'xs:decimal' + 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' + 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' + } + $keyType = $keyTypeMap[$key] + if ($keyType) { + X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" + } elseif ($actualVal -match '^(style|web|win):') { + # Внутри <dcsset:settings> префиксы style:/web:/win:/sys: уже объявлены на корне, + # локальный xmlns не нужен — эмитим short form. + X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" + } elseif ($actualVal -eq "true" -or $actualVal -eq "false") { + X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" + } elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { + # Строковые ключи, традиционно эмитятся как LocalStringType (даже если только ru). + Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" + } elseif ($actualVal -match '^-?\d+(\.\d+)?$') { + # Number → xs:decimal (МинимальнаяШирина=40, ОриентацияТекста и т.п. — но не key-typed) + X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" + } elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { + # Color без явного префикса (auto, #FFC8C8) + X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" + } else { + X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" + } + } + # Nested SettingsParameterValue items (например СтильГраницы.Сверху/.Снизу/.Слева/.Справа). + # Эмитим как siblings <dcscor:item> внутри родительского <dcscor:item>. + if ($nestedItems) { + $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } + if ($niProps) { + foreach ($np in $niProps) { + Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" + } + } elseif ($nestedItems -is [System.Collections.IDictionary]) { + foreach ($nk in $nestedItems.Keys) { + Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" + } + } + } + X "$indent</dcscor:item>" +} + +function Emit-ConditionalAppearance { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) + + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + + X "$indent<dcsset:conditionalAppearance>" + foreach ($ca in $items) { + X "$indent`t<dcsset:item>" + + # use=false — отключённое правило (эмитим до selection — XML-порядок) + if ($ca.use -eq $false) { + X "$indent`t`t<dcsset:use>false</dcsset:use>" + } + + # Selection (which fields to apply to; empty = all) + if ($ca.selection -and $ca.selection.Count -gt 0) { + X "$indent`t`t<dcsset:selection>" + foreach ($sel in $ca.selection) { + X "$indent`t`t`t<dcsset:item>" + X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>" + X "$indent`t`t`t</dcsset:item>" + } + X "$indent`t`t</dcsset:selection>" + } else { + X "$indent`t`t<dcsset:selection/>" + } + + # Filter (reuse existing Emit-Filter logic) + if ($ca.filter -and $ca.filter.Count -gt 0) { + Emit-Filter -items $ca.filter -indent "$indent`t`t" + } else { + # Платформа эмитит пустой <dcsset:filter/> на каждом condApp item + X "$indent`t`t<dcsset:filter/>" + } + + # Appearance (parameter-value pairs) + if ($ca.appearance) { + X "$indent`t`t<dcsset:appearance>" + foreach ($prop in $ca.appearance.PSObject.Properties) { + Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" + } + X "$indent`t`t</dcsset:appearance>" + } + + # Presentation + if ($ca.presentation) { + # Multilang dict {ru, en, ...} → LocalStringType; иначе — xs:string + if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { + Emit-MLText -tag "dcsset:presentation" -text $ca.presentation -indent "$indent`t`t" + } else { + X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" + } + } + + if ($ca.viewMode) { + X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" + } + + # UserSettingID + if ($ca.userSettingID) { + $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } + X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + + if ($ca.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $ca.userSettingPresentation -indent "$indent`t`t" + } + + # useInXxx — список областей где правило НЕ применяется (DontUse). + # Порядок имитирует платформенный (group → hierarchicalGroup → overall → ...). + if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { + $useInOrder = @('group','hierarchicalGroup','overall', + 'fieldsHeader','header','parameters','filter', + 'resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') + $set = @{} + foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } + foreach ($n in $useInOrder) { + if ($set.ContainsKey($n)) { + $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) + X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>" + } + } + } + + X "$indent`t</dcsset:item>" + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:conditionalAppearance>" +} + +# Эмиссия nested sub-item внутри SettingsParameterValue (chart-параметры типа +# ТипДиаграммы.СоединениеЗначенийПоСериям). Поддерживает use=false и valueType +# либо строкой ("xs:string", "dN:Foo" если префикс известен в корне), либо +# объектом {uri, name} — эмитим локальный xmlns на value. +function Emit-OutputParametersSubItem { + param([string]$subName, $subWrap, [string]$indent) + $subVal = $subWrap + $subVT = 'xs:string' + $subUseFalse = $false + $subUri = $null + $subLocalName = $null + if ($subWrap -is [PSCustomObject]) { + if ($subWrap.PSObject.Properties['value']) { $subVal = $subWrap.value } + if ($subWrap.PSObject.Properties['valueType']) { + $vt = $subWrap.valueType + if ($vt -is [PSCustomObject] -and $vt.PSObject.Properties['uri']) { + $subUri = "$($vt.uri)"; $subLocalName = "$($vt.name)" + } elseif ($vt -is [System.Collections.IDictionary] -and $vt.Contains('uri')) { + $subUri = "$($vt['uri'])"; $subLocalName = "$($vt['name'])" + } else { + $subVT = "$vt" + } + } + if ($subWrap.PSObject.Properties['use'] -and $subWrap.use -eq $false) { $subUseFalse = $true } + } elseif ($subWrap -is [System.Collections.IDictionary]) { + if ($subWrap.Contains('value')) { $subVal = $subWrap['value'] } + if ($subWrap.Contains('valueType')) { + $vt = $subWrap['valueType'] + if ($vt -is [PSCustomObject] -and $vt.PSObject.Properties['uri']) { + $subUri = "$($vt.uri)"; $subLocalName = "$($vt.name)" + } elseif ($vt -is [System.Collections.IDictionary] -and $vt.Contains('uri')) { + $subUri = "$($vt['uri'])"; $subLocalName = "$($vt['name'])" + } else { + $subVT = "$vt" + } + } + if ($subWrap.Contains('use') -and $subWrap['use'] -eq $false) { $subUseFalse = $true } + } + X "$indent`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + if ($subUseFalse) { X "$indent`t`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t`t<dcscor:parameter>$(Esc-Xml $subName)</dcscor:parameter>" + if ($subUri) { + X "$indent`t`t`t<dcscor:value xmlns:dN=`"$subUri`" xsi:type=`"dN:$subLocalName`">$(Esc-Xml "$subVal")</dcscor:value>" + } else { + X "$indent`t`t`t<dcscor:value xsi:type=`"$subVT`">$(Esc-Xml "$subVal")</dcscor:value>" + } + X "$indent`t`t</dcscor:item>" +} + +function Emit-OutputParameters { + param($params, [string]$indent, $blockViewMode = $null) + + if (-not $params) { return } + + X "$indent<dcsset:outputParameters>" + foreach ($prop in $params.PSObject.Properties) { + $key = $prop.Name + $rawVal = $prop.Value + # Распознаём wrapper {value, valueType?, use?, items?, viewMode?, userSettingID?, userSettingPresentation?} + # отличая от multilang dict ({ru, en, ...}). Wrapper всегда имеет ключ 'value'. + $useFalse = $false + $wrapVM = $null + $wrapUSID = $null + $wrapUSP = $null + $wrapVT = $null + $wrapItems = $null + $hasValueKey = $false + if ($rawVal -is [PSCustomObject] -and $rawVal.PSObject.Properties['value']) { + $hasValueKey = $true + if ($rawVal.PSObject.Properties['valueType']) { $wrapVT = "$($rawVal.valueType)" } + if ($rawVal.PSObject.Properties['use'] -and $rawVal.use -eq $false) { $useFalse = $true } + if ($rawVal.PSObject.Properties['items']) { $wrapItems = $rawVal.items } + if ($rawVal.PSObject.Properties['viewMode']) { $wrapVM = "$($rawVal.viewMode)" } + if ($rawVal.PSObject.Properties['userSettingID']) { $wrapUSID = "$($rawVal.userSettingID)" } + if ($rawVal.PSObject.Properties['userSettingPresentation']) { $wrapUSP = $rawVal.userSettingPresentation } + $rawVal = $rawVal.value + } elseif (($rawVal -is [hashtable] -or $rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('value')) { + $hasValueKey = $true + if ($rawVal.Contains('valueType')) { $wrapVT = "$($rawVal['valueType'])" } + if ($rawVal.Contains('use') -and $rawVal['use'] -eq $false) { $useFalse = $true } + if ($rawVal.Contains('items')) { $wrapItems = $rawVal['items'] } + if ($rawVal.Contains('viewMode')) { $wrapVM = "$($rawVal['viewMode'])" } + if ($rawVal.Contains('userSettingID')) { $wrapUSID = "$($rawVal['userSettingID'])" } + if ($rawVal.Contains('userSettingPresentation')) { $wrapUSP = $rawVal['userSettingPresentation'] } + $rawVal = $rawVal['value'] + } + # Font dict внутри значения + $isFontDict = $false + if ($rawVal -is [PSCustomObject]) { + $tProp = $rawVal.PSObject.Properties['@type'] + if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } + } elseif ($rawVal -is [System.Collections.IDictionary]) { + if ($rawVal.Contains('@type') -and "$($rawVal['@type'])" -eq 'Font') { $isFontDict = $true } + } + # Приоритет: явный wrapVT > известный тип ключа > xs:string + if ($wrapVT) { $ptype = $wrapVT } + else { + $ptype = $script:outputParamTypes[$key] + if (-not $ptype) { $ptype = "xs:string" } + } + # Auto-promote to mltext if value is a multilang dict (но не Font/wrapper) + if (-not $isFontDict -and ($rawVal -is [System.Management.Automation.PSCustomObject] -or $rawVal -is [hashtable] -or $rawVal -is [System.Collections.IDictionary])) { + $ptype = "mltext" + } + + X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + if ($useFalse) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + if ($isFontDict) { + $attrParts = @() + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $null + if ($rawVal -is [PSCustomObject]) { + $ap = $rawVal.PSObject.Properties[$attrName] + if ($ap) { $av = $ap.Value } + } else { + if ($rawVal.Contains($attrName)) { $av = $rawVal[$attrName] } + } + if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } + } + X "$indent`t`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" + } elseif ($ptype -eq "mltext") { + Emit-MLText -tag "dcscor:value" -text $rawVal -indent "$indent`t`t" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml "$rawVal")</dcscor:value>" + } + # Nested sub-параметры (ТипДиаграммы.ВидПодписей и т.п.) — эмитим между value и extras. + # valueType: строка → xsi:type=string, объект {uri, name} → локальный xmlns:dN + xsi:type=dN:name. + if ($wrapItems) { + $itemProps = if ($wrapItems -is [PSCustomObject]) { $wrapItems.PSObject.Properties } else { $null } + if ($itemProps) { + foreach ($ip in $itemProps) { + Emit-OutputParametersSubItem -subName $ip.Name -subWrap $ip.Value -indent $indent + } + } elseif ($wrapItems -is [System.Collections.IDictionary]) { + foreach ($k in $wrapItems.Keys) { + Emit-OutputParametersSubItem -subName $k -subWrap $wrapItems[$k] -indent $indent + } + } + } + if ($wrapVM) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml $wrapVM)</dcsset:viewMode>" } + if ($wrapUSID) { + $uid = if ("$wrapUSID" -eq 'auto') { New-Guid-String } else { "$wrapUSID" } + X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($wrapUSP) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $wrapUSP -indent "$indent`t`t" + } + X "$indent`t</dcscor:item>" + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + X "$indent</dcsset:outputParameters>" +} + +function Emit-DataParameters { + param($items, [string]$indent, $blockViewMode = $null) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent<dcsset:dataParameters>" + foreach ($dp in $items) { + # Support string shorthand + if ($dp -is [string]) { + $parsed = Parse-DataParamShorthand $dp + $dpObj = New-Object PSObject + $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter + if ($null -ne $parsed.value) { + $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value + } + if ($parsed.use -eq $false) { + $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false + } + if ($parsed.userSettingID) { + $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID + } + if ($parsed.viewMode) { + $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode + } + $dp = $dpObj + } + + X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + + if ($dp.use -eq $false) { + X "$indent`t`t<dcscor:use>false</dcscor:use>" + } + + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" + + # Value + if ($dp.nilValue -eq $true) { + X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" + } elseif (Test-EmptyValue $dp.value) { + Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false + } elseif ($null -ne $dp.value) { + $vtype = "$($dp.valueType)" + if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable]) -and ($dp.value.variant)) { + # Standard{Period,BeginningDate} — различаем по форме value: + # {variant, date} → SBD + # {variant, startDate, endDate} → SP с датами + # {variant} only → инференс по имени (BeginningOf* → SBD, иначе SP) + $_hasDate = $false; $_hasSD = $false + if ($dp.value -is [PSCustomObject]) { + $_hasDate = [bool]$dp.value.PSObject.Properties['date'] + $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] + } else { + $_hasDate = $dp.value.Contains('date') + $_hasSD = $dp.value.Contains('startDate') + } + $_variantStr = "$($dp.value.variant)" + $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') + if ($_isSBD) { + $_d = $null + if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } + elseif ($dp.value -is [System.Collections.IDictionary] -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { + if (-not $_d) { $_d = '0001-01-01T00:00:00' } + X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" + } + X "$indent`t`t</dcscor:value>" + } else { + # StandardPeriod — platform-pattern: startDate/endDate ТОЛЬКО для variant=Custom. + $_sd = $null; $_ed = $null + if ($dp.value -is [PSCustomObject]) { + if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" } + if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } + } else { + if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" } + if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } + } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { + if (-not $_sd) { $_sd = '0001-01-01T00:00:00' } + if (-not $_ed) { $_ed = '0001-01-01T00:00:00' } + X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>" + X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif ($vtype -match '^[a-zA-Z]+:') { + # Полный xsi:type из decompile (например "xs:boolean", "dcscor:DesignTimeValue"). + $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } + X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" + } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { + $bv = "$($dp.value)".ToLower() + X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $bv)</dcscor:value>" + } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^decimal') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^string') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } + } + + if ($dp.viewMode) { + X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" + } + + if ($dp.userSettingID) { + $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } + X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + + if ($dp.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t" + } + + X "$indent`t</dcscor:item>" + } + if ($null -ne $blockViewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" + } + X "$indent</dcsset:dataParameters>" +} + +# === Structure items (recursive) === + +function Emit-GroupItems { + param($groupBy, [string]$indent) + + if (-not $groupBy -or $groupBy.Count -eq 0) { return } + + X "$indent<dcsset:groupItems>" + foreach ($field in $groupBy) { + if ($field -is [string]) { + if ($field -eq 'Auto') { + # Auto-группировка (по аналогии с "Auto" в selection) + X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemAuto`"/>" + continue + } + X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" + X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + X "$indent`t`t<dcsset:groupType>Items</dcsset:groupType>" + X "$indent`t`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" + X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" + X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" + X "$indent`t</dcsset:item>" + } else { + # Object form + X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" + X "$indent`t`t<dcsset:field>$(Esc-Xml "$($field.field)")</dcsset:field>" + $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } + X "$indent`t`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>" + $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } + X "$indent`t`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>" + # Auto-detect: ISO date → xs:dateTime, иначе → dcscor:Field (path). + $pab = if ($field.periodAdditionBegin) { "$($field.periodAdditionBegin)" } else { '0001-01-01T00:00:00' } + $pae = if ($field.periodAdditionEnd) { "$($field.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } + $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>" + X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>" + X "$indent`t</dcsset:item>" + } + } + X "$indent</dcsset:groupItems>" +} + +# Parse structure string shorthand: "Организация > Номенклатура > details" +function Parse-StructureShorthand { + param([string]$s) + + $segments = $s -split '\s*>\s*' + $result = @() + + # Build nested groups from right to left + $innermost = $null + for ($i = $segments.Count - 1; $i -ge 0; $i--) { + $seg = $segments[$i].Trim() + $group = New-Object PSObject + $group | Add-Member -NotePropertyName "type" -NotePropertyValue "group" + + if ($seg -match '^(?i)(details|детали)$') { + # Empty groupBy = detailed records + $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @() + } elseif ($seg -match '^(.+)\[(.+)\]$') { + # Named group: "ИмяГруппы[Поле]" + $group | Add-Member -NotePropertyName "name" -NotePropertyValue $Matches[1].Trim() + $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($Matches[2].Trim()) + } else { + $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg) + } + + # Платформа в каждую группировку кладёт авто-поле выбора и авто-порядок; + # shorthand должен соответствовать ручному добавлению группировки в конфигураторе. + $group | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto") + $group | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto") + + if ($null -ne $innermost) { + $group | Add-Member -NotePropertyName "children" -NotePropertyValue @($innermost) + } + $innermost = $group + } + + if ($innermost) { $result += $innermost } + return ,$result +} + +function Emit-UserFields { + param($items, [string]$indent) + if (-not $items -or $items.Count -eq 0) { return } + X "$indent<dcsset:userFields>" + foreach ($uf in $items) { + # Type detection: cases → UserFieldCase, otherwise UserFieldExpression + $uType = if ($uf.cases) { "UserFieldCase" } else { "UserFieldExpression" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:$uType`">" + if ($uf.dataPath) { + X "$indent`t`t<dcsset:dataPath>$(Esc-Xml "$($uf.dataPath)")</dcsset:dataPath>" + } + if ($uf.title) { + Emit-MLText -tag "dcsset:lwsTitle" -text $uf.title -indent "$indent`t`t" -NoXsiType + } + if ($uType -eq "UserFieldExpression") { + if ($uf.detail) { + if ($uf.detail.PSObject.Properties.Match('expression').Count -gt 0) { + $_v = "$($uf.detail.expression)" + if ($_v) { X "$indent`t`t<dcsset:detailExpression>$(Esc-Xml $_v)</dcsset:detailExpression>" } + else { X "$indent`t`t<dcsset:detailExpression/>" } + } + if ($uf.detail.PSObject.Properties.Match('presentation').Count -gt 0) { + $_v = "$($uf.detail.presentation)" + if ($_v) { X "$indent`t`t<dcsset:detailExpressionPresentation>$(Esc-Xml $_v)</dcsset:detailExpressionPresentation>" } + else { X "$indent`t`t<dcsset:detailExpressionPresentation/>" } + } + } + if ($uf.total) { + if ($uf.total.PSObject.Properties.Match('expression').Count -gt 0) { + $_v = "$($uf.total.expression)" + if ($_v) { X "$indent`t`t<dcsset:totalExpression>$(Esc-Xml $_v)</dcsset:totalExpression>" } + else { X "$indent`t`t<dcsset:totalExpression/>" } + } + if ($uf.total.PSObject.Properties.Match('presentation').Count -gt 0) { + $_v = "$($uf.total.presentation)" + if ($_v) { X "$indent`t`t<dcsset:totalExpressionPresentation>$(Esc-Xml $_v)</dcsset:totalExpressionPresentation>" } + else { X "$indent`t`t<dcsset:totalExpressionPresentation/>" } + } + } + } else { + # UserFieldCase + if ($uf.cases.Count -eq 0) { + X "$indent`t`t<dcsset:cases/>" + } else { + X "$indent`t`t<dcsset:cases>" + foreach ($c in $uf.cases) { + X "$indent`t`t`t<dcsset:item>" + if ($c.filter) { + Emit-Filter -items $c.filter -indent "$indent`t`t`t`t" + } + if ($null -ne $c.value) { + $cv = $c.value + if ($cv -is [bool]) { + X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:boolean`">$(("$cv").ToLower())</dcsset:value>" + } elseif ($cv -is [int] -or $cv -is [long] -or $cv -is [double]) { + X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:decimal`">$cv</dcsset:value>" + } else { + X "$indent`t`t`t`t<dcsset:value xsi:type=`"xs:string`">$(Esc-Xml "$cv")</dcsset:value>" + } + } + if ($c.presentation) { + Emit-MLText -tag "dcsset:lwsPresentationValue" -text $c.presentation -indent "$indent`t`t`t`t" -NoXsiType + } + X "$indent`t`t`t</dcsset:item>" + } + X "$indent`t`t</dcsset:cases>" + } + } + X "$indent`t</dcsset:item>" + } + X "$indent</dcsset:userFields>" +} + +# Shared emitter for table column/row and chart point/series. +# Emits name?, groupItems, filter, order, selection, outputParameters, viewMode?, +# userSettingID?, userSettingPresentation? — каждое условно по присутствию в JSON. +# Параметр $emitName управляет тем, эмитить ли <name> внутри блока: для row caller +# уже эмитит name отдельно (исторический порядок), для остальных — здесь. +function Emit-TableAxisBlock { + param($block, [string]$indent, [bool]$emitName = $true) + if ($emitName -and $block.name) { + X "$indent<dcsset:name>$(Esc-Xml "$($block.name)")</dcsset:name>" + } + $gb = if ($block.groupBy) { $block.groupBy } else { $block.groupFields } + Emit-GroupItems -groupBy $gb -indent $indent + if ($block.filter) { + Emit-Filter -items $block.filter -indent $indent + } + if ($block.order) { + Emit-Order -items $block.order -indent $indent + } + if ($block.selection) { + Emit-Selection -items $block.selection -indent $indent + } + if ($block.conditionalAppearance) { + Emit-ConditionalAppearance -items $block.conditionalAppearance -indent $indent + } + if ($block.outputParameters) { + Emit-OutputParameters -params $block.outputParameters -indent $indent + } + # nested children (StructureItemGroup внутри table row/column или chart axis). + # Platform-pattern: items внутри row/column/points/series — ВСЕГДА short form (без xsi:type). + if ($block.children) { + foreach ($child in $block.children) { + Emit-StructureItem -item $child -indent $indent -shortGroup + } + } + if ($block.viewMode) { + X "$indent<dcsset:viewMode>$(Esc-Xml "$($block.viewMode)")</dcsset:viewMode>" + } + if ($block.userSettingID) { + $uid = if ("$($block.userSettingID)" -eq "auto") { New-Guid-String } else { "$($block.userSettingID)" } + X "$indent<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($block.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $block.userSettingPresentation -indent $indent + } + if ($block.itemsViewMode) { + X "$indent<dcsset:itemsViewMode>$(Esc-Xml "$($block.itemsViewMode)")</dcsset:itemsViewMode>" + } +} + +function Emit-StructureItem { + param($item, [string]$indent, [switch]$shortGroup) + + $type = if ($item.type) { "$($item.type)" } else { "group" } + + if ($type -eq "group") { + # Platform пишет короткую форму (без xsi:type) для groups внутри table row/column, + # explicit StructureItemGroup в остальных случаях. + if ($shortGroup) { + X "$indent<dcsset:item>" + } else { + X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" + } + + # use=false — отключённая ветка структуры + if ($item.use -eq $false) { + X "$indent`t<dcsset:use>false</dcsset:use>" + } + + if ($item.name) { + X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" + } + + $gb = if ($item.groupBy) { $item.groupBy } else { $item.groupFields } + Emit-GroupItems -groupBy $gb -indent "$indent`t" + + # Emit order/selection only if specified — platform doesn't always emit them on group + if ($item.order) { + Emit-Order -items $item.order -indent "$indent`t" -blockViewMode $item.orderViewMode -blockUserSettingID $item.orderUserSettingID + } + if ($item.selection) { + Emit-Selection -items $item.selection -indent "$indent`t" + } + + Emit-Filter -items $item.filter -indent "$indent`t" + + if ($item.conditionalAppearance) { + Emit-ConditionalAppearance -items $item.conditionalAppearance -indent "$indent`t" + } + + if ($item.outputParameters) { + Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" + } + + # Nested children — наследуем shortGroup от родителя (если родитель в short form, + # то и дети остаются short, как делает platform внутри row/column). + if ($item.children) { + foreach ($child in $item.children) { + if ($shortGroup) { + Emit-StructureItem -item $child -indent "$indent`t" -shortGroup + } else { + Emit-StructureItem -item $child -indent "$indent`t" + } + } + } + + # viewMode/itemsViewMode/userSettingID/userSettingPresentation on + # StructureItemGroup are context-dependent — emit only when explicitly set. + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + if ($item.userSettingID) { + $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" + } + if ($item.itemsViewMode) { + X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" + } + + X "$indent</dcsset:item>" + } + elseif ($type -eq "table") { + X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemTable`">" + + # use=false — отключённая таблица + if ($item.use -eq $false) { + X "$indent`t<dcsset:use>false</dcsset:use>" + } + + if ($item.name) { + X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" + } + + # Columns + if ($item.columns) { + foreach ($col in $item.columns) { + X "$indent`t<dcsset:column>" + Emit-TableAxisBlock -block $col -indent "$indent`t`t" + X "$indent`t</dcsset:column>" + } + } + + # Rows + if ($item.rows) { + foreach ($row in $item.rows) { + X "$indent`t<dcsset:row>" + Emit-TableAxisBlock -block $row -indent "$indent`t`t" + X "$indent`t</dcsset:row>" + } + } + + # Top-level: selection / conditionalAppearance / outputParameters на самой таблице + if ($item.selection) { + Emit-Selection -items $item.selection -indent "$indent`t" + } + if ($item.conditionalAppearance) { + Emit-ConditionalAppearance -items $item.conditionalAppearance -indent "$indent`t" + } + if ($item.outputParameters) { + Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" + } + # columnsViewMode / rowsViewMode — axis-level режим доступности (после rows/columns) + if ($item.columnsViewMode) { + X "$indent`t<dcsset:columnsViewMode>$(Esc-Xml "$($item.columnsViewMode)")</dcsset:columnsViewMode>" + } + if ($item.rowsViewMode) { + X "$indent`t<dcsset:rowsViewMode>$(Esc-Xml "$($item.rowsViewMode)")</dcsset:rowsViewMode>" + } + # viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой таблице + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + if ($item.userSettingID) { + $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" + } + if ($item.itemsViewMode) { + X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" + } + + X "$indent</dcsset:item>" + } + elseif ($type -eq "chart") { + X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemChart`">" + + # use=false — отключённая диаграмма + if ($item.use -eq $false) { + X "$indent`t<dcsset:use>false</dcsset:use>" + } + + if ($item.name) { + X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>" + } + + # Points — single object или массив (multi-series диаграмма) + if ($item.points) { + $pBlocks = if ($item.points -is [array] -or ($item.points -is [System.Collections.IList] -and $item.points -isnot [string])) { + @($item.points) + } else { @($item.points) } + # Эвристика: если это массив объектов (а не одиночный объект-с-полями) → multi. + $isPointsArray = ($item.points -is [array]) -or ($item.points -is [System.Collections.IList] -and $item.points -isnot [string] -and $item.points -isnot [System.Collections.IDictionary] -and $item.points -isnot [PSCustomObject]) + if ($isPointsArray) { + foreach ($pb in $pBlocks) { + X "$indent`t<dcsset:point>" + Emit-TableAxisBlock -block $pb -indent "$indent`t`t" + X "$indent`t</dcsset:point>" + } + } else { + X "$indent`t<dcsset:point>" + Emit-TableAxisBlock -block $item.points -indent "$indent`t`t" + X "$indent`t</dcsset:point>" + } + } + + # Series — single object или массив + if ($item.series) { + $isSeriesArray = ($item.series -is [array]) -or ($item.series -is [System.Collections.IList] -and $item.series -isnot [string] -and $item.series -isnot [System.Collections.IDictionary] -and $item.series -isnot [PSCustomObject]) + if ($isSeriesArray) { + foreach ($sb in @($item.series)) { + X "$indent`t<dcsset:series>" + Emit-TableAxisBlock -block $sb -indent "$indent`t`t" + X "$indent`t</dcsset:series>" + } + } else { + X "$indent`t<dcsset:series>" + Emit-TableAxisBlock -block $item.series -indent "$indent`t`t" + X "$indent`t</dcsset:series>" + } + } + + # Selection (chart values) + Emit-Selection -items $item.selection -indent "$indent`t" + + if ($item.outputParameters) { + Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" + } + + # pointsViewMode / seriesViewMode — axis-level режим доступности (после points/series) + if ($item.pointsViewMode) { + X "$indent`t<dcsset:pointsViewMode>$(Esc-Xml "$($item.pointsViewMode)")</dcsset:pointsViewMode>" + } + if ($item.seriesViewMode) { + X "$indent`t<dcsset:seriesViewMode>$(Esc-Xml "$($item.seriesViewMode)")</dcsset:seriesViewMode>" + } + # viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой диаграмме + if ($item.viewMode) { + X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" + } + if ($item.userSettingID) { + $gid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $gid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" + } + if ($item.itemsViewMode) { + X "$indent`t<dcsset:itemsViewMode>$(Esc-Xml "$($item.itemsViewMode)")</dcsset:itemsViewMode>" + } + + X "$indent</dcsset:item>" + } + elseif ($type -eq "nestedObject") { + X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemNestedObject`">" + if ($item.objectID) { X "$indent`t<dcsset:objectID>$(Esc-Xml "$($item.objectID)")</dcsset:objectID>" } + X "$indent`t<dcsset:settings>" + $s = $item.settings + if ($s) { + if ($s.selection) { Emit-Selection -items $s.selection -indent "$indent`t`t" } + if ($s.filter) { Emit-Filter -items $s.filter -indent "$indent`t`t" } + if ($s.order) { Emit-Order -items $s.order -indent "$indent`t`t" } + if ($s.conditionalAppearance) { Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "$indent`t`t" } + if ($s.outputParameters) { Emit-OutputParameters -params $s.outputParameters -indent "$indent`t`t" } + } + X "$indent`t</dcsset:settings>" + X "$indent</dcsset:item>" + } +} + +function Emit-SettingsVariants { + $variants = $def.settingsVariants + + # Default variant if none specified + if (-not $variants -or $variants.Count -eq 0) { + $variants = @(@{ + name = "Основной" + presentation = "Основной" + settings = @{ + selection = @("Auto") + structure = @(@{ + type = "group" + order = @("Auto") + selection = @("Auto") + }) + } + }) + # Convert to PSCustomObject-like structure + $variants = @($variants | ForEach-Object { + $v = New-Object PSObject + $v | Add-Member -NotePropertyName "name" -NotePropertyValue $_.name + $v | Add-Member -NotePropertyName "presentation" -NotePropertyValue $_.presentation + $settingsObj = New-Object PSObject + $settingsObj | Add-Member -NotePropertyName "selection" -NotePropertyValue $_.settings.selection + $structItem = New-Object PSObject + $structItem | Add-Member -NotePropertyName "type" -NotePropertyValue "group" + $structItem | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto") + $structItem | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto") + $settingsObj | Add-Member -NotePropertyName "structure" -NotePropertyValue @($structItem) + $v | Add-Member -NotePropertyName "settings" -NotePropertyValue $settingsObj + $v + }) + } + + foreach ($v in $variants) { + X "`t<settingsVariant>" + X "`t`t<dcsset:name>$(Esc-Xml "$($v.name)")</dcsset:name>" + + $pres = if ($v.presentation) { $v.presentation } elseif ($v.title) { $v.title } else { "$($v.name)" } + Emit-MLText -tag "dcsset:presentation" -text $pres -indent "`t`t" + + X "`t`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">" + + $s = $v.settings + + # Helper: resolve XViewMode/XUserSettingID from settings — emit only if explicitly set + function Get-BlockVM([string]$key) { + $prop = "${key}ViewMode" + if ($s.PSObject.Properties[$prop]) { return "$($s.$prop)" } + return $null + } + function Get-BlockUSID([string]$key) { + $prop = "${key}UserSettingID" + if ($s.PSObject.Properties[$prop]) { return "$($s.$prop)" } + return $null + } + + # userFields — пользовательские вычисляемые поля (Expression / Case) + if ($s.userFields -and $s.userFields.Count -gt 0) { + Emit-UserFields -items $s.userFields -indent "`t`t`t" + } + + # Selection — эмитим даже если items пустые, но есть block-level viewMode/userSettingID. + # Platform может содержать Auto-items на top-level (вместе с явными полями). + $svm = Get-BlockVM 'selection'; $susid = Get-BlockUSID 'selection' + if ($s.selection -or $null -ne $svm -or $null -ne $susid) { + Emit-Selection -items $s.selection -indent "`t`t`t" -blockViewMode $svm -blockUserSettingID $susid + } + + # Filter + $fvm = Get-BlockVM 'filter'; $fusid = Get-BlockUSID 'filter' + if ($s.filter -or $null -ne $fvm -or $null -ne $fusid) { + Emit-Filter -items $s.filter -indent "`t`t`t" -blockViewMode $fvm -blockUserSettingID $fusid + } + + # Order + $ovm = Get-BlockVM 'order'; $ousid = Get-BlockUSID 'order' + if ($s.order -or $null -ne $ovm -or $null -ne $ousid) { + Emit-Order -items $s.order -indent "`t`t`t" -blockViewMode $ovm -blockUserSettingID $ousid + } + + # ConditionalAppearance + $cavm = Get-BlockVM 'conditionalAppearance'; $causid = Get-BlockUSID 'conditionalAppearance' + if ($s.conditionalAppearance -or $null -ne $cavm -or $null -ne $causid) { + Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "`t`t`t" -blockViewMode $cavm -blockUserSettingID $causid + } + + # OutputParameters (platform does NOT emit <viewMode> on this block) + if ($s.outputParameters) { + Emit-OutputParameters -params $s.outputParameters -indent "`t`t`t" + } + + # DataParameters + if ($s.dataParameters -eq 'auto') { + # Auto-generate dataParameters for all non-hidden params. + # Pattern follows 1C Designer / ERP persistence: + # - value set (non-default) → emit value, use=true (implicit) + # - value missing / Custom period → <use>false</use> + <value xsi:nil="true"/> + $autoDP = @() + foreach ($ap in $script:allParams) { + if ($ap.hidden) { continue } + $dpItem = New-Object PSObject + $dpItem | Add-Member -NotePropertyName "parameter" -NotePropertyValue $ap.name + $dpItem | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue "auto" + + $hasMeaningfulValue = $false + + if ($ap.type -eq 'StandardPeriod') { + # Inherit variant; Custom is treated as "empty" + $variant = 'Custom' + $av = $ap.value + if ($null -ne $av) { + if (($av -is [PSCustomObject] -or $av -is [hashtable]) -and $av.variant) { + $variant = "$($av.variant)" + } elseif ("$av") { + $variant = "$av" + } + } + $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue @{ variant = $variant } + if ($variant -ne 'Custom') { $hasMeaningfulValue = $true } + } elseif (-not (Test-EmptyValue $ap.value)) { + $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue $ap.value + $dpItem | Add-Member -NotePropertyName "valueType" -NotePropertyValue "$($ap.type)" + $hasMeaningfulValue = $true + } else { + $dpItem | Add-Member -NotePropertyName "nilValue" -NotePropertyValue $true + } + + if (-not $hasMeaningfulValue) { + $dpItem | Add-Member -NotePropertyName "use" -NotePropertyValue $false + } + + $autoDP += $dpItem + } + if ($autoDP.Count -gt 0) { + Emit-DataParameters -items $autoDP -indent "`t`t`t" + } + } elseif ($s.dataParameters) { + Emit-DataParameters -items $s.dataParameters -indent "`t`t`t" + } + + # Structure (supports string shorthand: "Организация > details") + if ($s.structure) { + $structItems = $s.structure + if ($structItems -is [string]) { + $structItems = Parse-StructureShorthand $structItems + } + foreach ($item in $structItems) { + Emit-StructureItem -item $item -indent "`t`t`t" + } + } + + # <dcsset:itemsViewMode> on <dcsset:settings> — emit only if explicitly set + if ($s.itemsViewMode) { + X "`t`t`t<dcsset:itemsViewMode>$(Esc-Xml "$($s.itemsViewMode)")</dcsset:itemsViewMode>" + } + + # <dcsset:additionalProperties> — key/value свойства варианта + if ($s.additionalProperties) { + X "`t`t`t<dcsset:additionalProperties>" + foreach ($prop in $s.additionalProperties.PSObject.Properties) { + X "`t`t`t`t<v8:Property name=`"$(Esc-Xml $prop.Name)`">" + X "`t`t`t`t`t<v8:Value xsi:type=`"xs:string`">$(Esc-Xml "$($prop.Value)")</v8:Value>" + X "`t`t`t`t</v8:Property>" + } + X "`t`t`t</dcsset:additionalProperties>" + } + + X "`t`t</dcsset:settings>" + X "`t</settingsVariant>" + } +} + +# --- 12. Assemble XML --- + +X "<?xml version=`"1.0`" encoding=`"UTF-8`"?>" +X ("<DataCompositionSchema xmlns=`"http://v8.1c.ru/8.1/data-composition-system/schema`"" + + " xmlns:dcscom=`"http://v8.1c.ru/8.1/data-composition-system/common`"" + + " xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`"" + + " xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`"" + + " xmlns:v8=`"http://v8.1c.ru/8.1/data/core`"" + + " xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`"" + + " xmlns:xs=`"http://www.w3.org/2001/XMLSchema`"" + + " xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`">") + +Emit-DataSources +Emit-DataSets +Emit-DataSetLinks +Emit-CalcFields +Emit-TotalFields +Emit-Parameters +Emit-Templates +Emit-FieldTemplates +Emit-GroupTemplates +Emit-SettingsVariants + +X '</DataCompositionSchema>' + +# --- 13. Write output --- + +Assert-EditAllowed $OutputPath 'editable' + +$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath) +if ($parentDir -and -not (Test-Path $parentDir)) { + New-Item -ItemType Directory -Force $parentDir | Out-Null +} + +$content = $script:xml.ToString() +$utf8Bom = New-Object System.Text.UTF8Encoding $true +[System.IO.File]::WriteAllText($OutputPath, $content, $utf8Bom) + +# --- 14. Statistics --- + +$dsCount = $def.dataSets.Count +$fieldCount = 0 +foreach ($ds in $def.dataSets) { + if ($ds.fields) { $fieldCount += $ds.fields.Count } +} +$calcCount = if ($def.calculatedFields) { $def.calculatedFields.Count } else { 0 } +$totalCount = if ($def.totalFields) { $def.totalFields.Count } else { 0 } +$paramCount = if ($def.parameters) { $def.parameters.Count } else { 0 } +$variantCount = if ($def.settingsVariants) { $def.settingsVariants.Count } else { 1 } +$fileSize = (Get-Item $OutputPath).Length + +Write-Host "OK $OutputPath" +Write-Host " DataSets: $dsCount Fields: $fieldCount Calculated: $calcCount Totals: $totalCount Params: $paramCount Variants: $variantCount" +Write-Host " Size: $fileSize bytes" diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index fc8c4bf9..b39200b8 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -1,373 +1,373 @@ ---- -name: skd-edit -description: Точечное редактирование схемы компоновки данных 1С (СКД). Используй когда нужно модифицировать существующую СКД — добавить поля, итоги, фильтры, параметры, изменить текст запроса -argument-hint: <TemplatePath> -Operation <op> -Value <value> -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /skd-edit — точечное редактирование СКД (Template.xml) - -Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) | -| `Operation` | Операция (см. список ниже) | -| `Value` | Значение операции (shorthand-строка или текст запроса) | -| `DataSet` | (опц.) Имя набора данных (умолч. первый) | -| `Variant` | (опц.) Имя варианта настроек (умолч. первый) | -| `NoSelection` | (опц.) Не добавлять поле в selection варианта | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-edit.ps1" -TemplatePath "<path>" -Operation <op> -Value "<value>" -``` - -## Пакетный режим (batch) - -Несколько значений в одном вызове через разделитель `;;`: - -```powershell --Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" -``` - -Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`. - -## Операции - -### add-field — добавить поле в набор данных - -Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`. - -``` -"Цена: decimal(15,2)" -"Организация [Орг-ция]: CatalogRef.Организации @dimension" -"Служебное: string #noFilter #noOrder" -``` - -Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск. - -Чтобы поле попало в selection не варианта, а конкретной группировки структуры — используй `-NoSelection` и затем `add-selection "Имя @group=ИмяГруппы"`. - -### add-total — добавить итог - -Shorthand: `"<dataPath>: <выражение>"`. Если выражение — известная аггрегатная функция без скобок (`Сумма`, `Количество`, `Минимум`, `Максимум`, `Среднее`), оно автоматически оборачивается в `Func(dataPath)`. Если функция со скобками или произвольное выражение — используется как есть. - -``` -"Цена: Среднее" # → Среднее(Цена) -"Стоимость: Сумма(Кол * Цена)" # → как есть -"Проверка: Проверка" # identity: выражение = Проверка -``` - -### add-calculated-field — добавить вычисляемое поле - -Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter #noOrder #noGroup"`. - -``` -"Маржа = Продажа - Закупка" -"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" -"Служебное: string = \"\" #noFilter #noOrder #noGroup" -``` - -`#noFilter`, `#noOrder`, `#noGroup`, `#noField` → `<useRestriction>` (аналогично add-field). - -Также добавляется в selection варианта. - -### add-parameter — добавить параметр - -``` -"Период [Отчетный период]: StandardPeriod = LastMonth @autoDates" -"Организация: CatalogRef.Организации" -``` - -Shorthand: `"Имя [Заголовок]: тип = значение [availableValue=список] [@флаги]"`. `[Заголовок]` опциональный — добавляет `<title>`. - -Флаги: -- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра. -- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе). -- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода). -- `@valueList` — разрешает передавать в параметр список значений (при значении-списке ниже подразумевается автоматически, отдельно указывать не обязательно). - -Значение-список: несколько значений по умолчанию задаются через запятую в `значение`. Для запятой внутри одного значения — кавычки `'...'`. - -``` -"Виды [Виды субконто]: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" -``` - -``` -"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden" -"Период: StandardPeriod = LastMonth @always" -"ПСчет: ChartOfAccountsRef.Хозрасчетный = ПланСчетов.Хозрасчетный.X @hidden @always" -"Округление: EnumRef.Округления = Окр1 availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс." -``` - -`availableValue=` задаёт начальный список допустимых значений. Формат списка: `v1[: p1], v2[: p2], ...` — элементы через `,`, представление после `:`. Если в значении или представлении встречается `,` или `:` — оборачивай в одинарные кавычки `'...'`: - -``` -"Округление: ... = Окр1 availableValue=Окр1_00: 'руб., коп.', Окр1: руб." -``` - -### modify-parameter — изменить существующий параметр - -Shorthand: `"ИмяПараметра [Заголовок] [ключ=значение]... [@флаги]"`. Находит параметр по имени, обновляет указанные свойства. - -``` -"ПорядокОкругления use=Always" -"ПорядокОкругления [Округление сумм] denyIncompleteValues=true" -"ПериодОтчета [Отчетный период]" # только title -"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс." -"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия" -"Виды value=ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" -"Контрагент @hidden @always" -``` - -`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title. - -`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`. - -`value=` заменяет значение параметра. Несколько значений через запятую → **список значений** (заменяет все прежние); для запятой внутри значения — кавычки `'...'`. - -Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны. - -### rename-parameter — переименовать параметр - -Shorthand: `"OldName => NewName"`. Атомарно обновляет имя параметра, ссылки `&Имя` в выражениях других параметров (только полные совпадения, `&ПериодX` не задевается), и записи в `dataParameters` всех вариантов. Текст запроса не трогает — переименование строго в области параметров. - -``` -"Период => ПериодОтчета" -``` - -### reorder-parameters — переставить параметры в указанном порядке - -Shorthand: `"Имя1, Имя2, Имя3"`. Частичный список — указанные параметры идут первыми в заданном порядке, остальные сохраняют исходный порядок и идут в конце. Параметры из списка, которых нет в схеме — warning, пропуск. - -``` -"ПериодОтчета, НачалоПериода, КонецПериода" -``` - -### add-filter — добавить фильтр в вариант - -Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`. - -``` -"Номенклатура = _ @off @user" -"Дата >= 2024-01-01T00:00:00" -"Статус filled" -``` - -### add-dataParameter — добавить параметр данных в вариант - -Shorthand: `"Имя [= значение] @флаги"`. - -``` -"Период = LastMonth @user" -"Организация @off @user" -``` - -### add-order — добавить сортировку - -Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент. - -``` -"Количество desc" -"Auto" -``` - -### add-selection — добавить элемент выборки - -``` -"Номенклатура" -"Auto" -"Folder(Поступление: ПолеА, ПолеБ, ПолеВ)" -``` - -`Folder(Название: поле1, поле2)` — группа полей (SelectedItemFolder) с заголовком и `placement=Auto`. - -`@group=ИмяГруппировки` — добавить в selection именованной группировки (вместо уровня варианта): - -``` -"Folder(Поступление: ПолеА, ПолеБ) @group=ДанныеОтчета" -``` - -### add-dataSetLink — добавить связь наборов данных - -Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`. - -``` -"Набор1 > Набор2 on Поле1 = Поле2" -"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" -``` - -### add-dataSet — добавить набор данных - -Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`). - -``` -"Доп: ВЫБРАТЬ 1 КАК Тест" -"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура" -"Продажи: @queries/sales.sql" -``` - -`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`). - -### add-variant — добавить вариант настроек - -Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя. - -``` -"Детальный" -"Детальный [Детальный отчёт]" -``` - -Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск. - -### add-conditionalAppearance — добавить условное оформление - -Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение). - -``` -"ЦветТекста = web:Red when Сумма < 0" -"ЦветФона = web:LightGreen when Статус = Одобрен for Статус" -"МинимальнаяШирина = 50 for Организация" -"Формат = ЧДЦ=2 for Цена, Сумма" -``` - -Типы значений appearance (автодетект): `web:*`/`style:*`/`win:*` → Color, `true`/`false` → Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, иначе String. - -Типы значений фильтра (автодетект): `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue, `true`/`false` → Boolean, дата → DateTime, числа → Decimal, иначе String. - -OrGroup: несколько условий через ` or ` в `when` объединяются в FilterItemGroup/OrGroup: - -``` -"Формат = ЧЦ=15; ЧДЦ=0 when ПараметрыДанных.Округление = Перечисление.Округления.Окр1 or ПараметрыДанных.Округление = Перечисление.Округления.Окр1000" -``` - -**Важно**: для параметров данных используйте префикс `ПараметрыДанных.` в поле фильтра. - -### add-drilldown — подключить расшифровку к ресурсам в шаблонах - -Value — имена ресурсов (как в полях/вычисляемых полях СКД) через запятую. - -``` -"ПоступлениеИзПроизводства, ВыбытиеПрочее" -"Сумма_Дт83, Сумма_Дт99, Сумма_68, Сумма_84" -``` - -Подключает DrillDown по `ИмяРесурса` ко всем шаблонам, содержащим указанные ресурсы. Идемпотентно. - -### set-query — заменить текст запроса - -Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD. - -Когда что: **существенная переработка** (добавить поля, соединения, переписать пакет) → выгрузи запрос через `/skd-info <tpl> -Mode query -Name <набор> -Raw -OutFile file.sql`, отредактируй файл, верни `set-query @file`. `-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат точны (включая многопакетные запросы). **Точечная замена** (переименовать идентификатор, заменить подстроку) → выгрузка не нужна, используй `patch-query` ниже. - -### patch-query — точечная замена в тексте запроса - -Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`. - -``` -"СубконтоДт1) В => СубконтоКт1) В" -"КАК ВТ_СтароеИмя => КАК ВТ_НовоеИмя @once" -``` - -`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах. - -Многострочные подстроки поддерживаются. - -### set-outputParameter — установить параметр вывода - -``` -"Заголовок = Мой отчёт" -"ВыводитьЗаголовок = true" -``` - -Если параметр уже существует — заменяет значение. - -### set-structure — установить структуру варианта - -Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим. - -``` -"Организация > Номенклатура > details" -"Валюта, НаименованиеБанка, ИНН" -"details" -"СчетМеждународногоУчета @name=ДанныеОтчета" -``` - -`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`. - -### modify-structure — изменить поля группировки существующей группы - -Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `<groupItems>` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка. - -``` -"Валюта @name=ДанныеОтчета" -"Валюта, НаименованиеБанка @name=ДанныеОтчета" -"details @name=ДанныеОтчета" -``` - -### modify-field — изменить существующее поле - -Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию. - -``` -"Цена [Цена USD]: decimal(10,4) @dimension" -``` - -### set-field-role — установить роль поля - -Shorthand: `"<dataPath> [@флаги] [kv=значение]"`. **Полностью заменяет** роль поля. Если в значении только dataPath без флагов/kv — удаляет роль. - -``` -"Сумма" # снять роль полностью -"СуммаОстаток @balance" # простая балансовая роль -"СуммаНач @balance balanceGroupName=Сумма balanceType=OpeningBalance" # с уточнением -"Контрагент @dimension parentDimension=Группа" -"Период @period" # роль периода -``` - -Флаги: `@balance`, `@dimension`, `@account`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`. -KV: `balanceGroupName`, `balanceType` (OpeningBalance/ClosingBalance), `parentDimension`, `accountTypeExpression`, `orderType` (Asc/Desc), `expression`, `periodNumber`, `periodType`. - -Поддерживает пакетный режим (`;;`). - -### modify-filter — изменить существующий фильтр - -Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `<use>` ниже. - -### modify-dataParameter — изменить параметр данных - -Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги. См. правило для `<use>` ниже. - -#### Правило `<use>` для modify-filter / modify-dataParameter - -В отличие от `add-*`, в `modify-*` поле `<use>` обновляется **только если флаг задан явно**: -- `@off` — установить `<use>false</use>` -- `@on` — убрать существующий `<use>false</use>` (включить параметр) -- ни `@off`, ни `@on` не задано — `<use>` не трогается, существующее значение сохраняется (важно: это значит, что отключённый параметр останется отключённым после модификации других свойств) - -### remove-* и clear-* - -| Операция | Value | Действие | -|----------|-------|----------| -| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта | -| `remove-total` | dataPath | Удаляет итог | -| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection | -| `remove-parameter` | name | Удаляет параметр | -| `remove-filter` | поле | Удаляет первый фильтр с указанным полем | -| `clear-selection` | `*` | Очищает все элементы selection | -| `clear-order` | `*` | Очищает все элементы order | -| `clear-filter` | `*` | Очищает все элементы filter | -| `clear-conditionalAppearance` | `*` | Очищает все правила условного оформления | - -## Верификация - -``` -/skd-validate <TemplatePath> — валидация структуры после редактирования -/skd-info <TemplatePath> — визуальная сводка -``` +--- +name: skd-edit +description: Точечное редактирование схемы компоновки данных 1С (СКД). Используй когда нужно модифицировать существующую СКД — добавить поля, итоги, фильтры, параметры, изменить текст запроса +argument-hint: <TemplatePath> -Operation <op> -Value <value> +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-edit — точечное редактирование СКД (Template.xml) + +Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) | +| `Operation` | Операция (см. список ниже) | +| `Value` | Значение операции (shorthand-строка или текст запроса) | +| `DataSet` | (опц.) Имя набора данных (умолч. первый) | +| `Variant` | (опц.) Имя варианта настроек (умолч. первый) | +| `NoSelection` | (опц.) Не добавлять поле в selection варианта | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-edit.ps1" -TemplatePath "<path>" -Operation <op> -Value "<value>" +``` + +## Пакетный режим (batch) + +Несколько значений в одном вызове через разделитель `;;`: + +```powershell +-Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" +``` + +Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`. + +## Операции + +### add-field — добавить поле в набор данных + +Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`. + +``` +"Цена: decimal(15,2)" +"Организация [Орг-ция]: CatalogRef.Организации @dimension" +"Служебное: string #noFilter #noOrder" +``` + +Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск. + +Чтобы поле попало в selection не варианта, а конкретной группировки структуры — используй `-NoSelection` и затем `add-selection "Имя @group=ИмяГруппы"`. + +### add-total — добавить итог + +Shorthand: `"<dataPath>: <выражение>"`. Если выражение — известная аггрегатная функция без скобок (`Сумма`, `Количество`, `Минимум`, `Максимум`, `Среднее`), оно автоматически оборачивается в `Func(dataPath)`. Если функция со скобками или произвольное выражение — используется как есть. + +``` +"Цена: Среднее" # → Среднее(Цена) +"Стоимость: Сумма(Кол * Цена)" # → как есть +"Проверка: Проверка" # identity: выражение = Проверка +``` + +### add-calculated-field — добавить вычисляемое поле + +Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter #noOrder #noGroup"`. + +``` +"Маржа = Продажа - Закупка" +"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" +"Служебное: string = \"\" #noFilter #noOrder #noGroup" +``` + +`#noFilter`, `#noOrder`, `#noGroup`, `#noField` → `<useRestriction>` (аналогично add-field). + +Также добавляется в selection варианта. + +### add-parameter — добавить параметр + +``` +"Период [Отчетный период]: StandardPeriod = LastMonth @autoDates" +"Организация: CatalogRef.Организации" +``` + +Shorthand: `"Имя [Заголовок]: тип = значение [availableValue=список] [@флаги]"`. `[Заголовок]` опциональный — добавляет `<title>`. + +Флаги: +- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра. +- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе). +- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода). +- `@valueList` — разрешает передавать в параметр список значений (при значении-списке ниже подразумевается автоматически, отдельно указывать не обязательно). + +Значение-список: несколько значений по умолчанию задаются через запятую в `значение`. Для запятой внутри одного значения — кавычки `'...'`. + +``` +"Виды [Виды субконто]: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" +``` + +``` +"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden" +"Период: StandardPeriod = LastMonth @always" +"ПСчет: ChartOfAccountsRef.Хозрасчетный = ПланСчетов.Хозрасчетный.X @hidden @always" +"Округление: EnumRef.Округления = Окр1 availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс." +``` + +`availableValue=` задаёт начальный список допустимых значений. Формат списка: `v1[: p1], v2[: p2], ...` — элементы через `,`, представление после `:`. Если в значении или представлении встречается `,` или `:` — оборачивай в одинарные кавычки `'...'`: + +``` +"Округление: ... = Окр1 availableValue=Окр1_00: 'руб., коп.', Окр1: руб." +``` + +### modify-parameter — изменить существующий параметр + +Shorthand: `"ИмяПараметра [Заголовок] [ключ=значение]... [@флаги]"`. Находит параметр по имени, обновляет указанные свойства. + +``` +"ПорядокОкругления use=Always" +"ПорядокОкругления [Округление сумм] denyIncompleteValues=true" +"ПериодОтчета [Отчетный период]" # только title +"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс." +"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия" +"Виды value=ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры" +"Контрагент @hidden @always" +``` + +`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title. + +`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`. + +`value=` заменяет значение параметра. Несколько значений через запятую → **список значений** (заменяет все прежние); для запятой внутри значения — кавычки `'...'`. + +Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны. + +### rename-parameter — переименовать параметр + +Shorthand: `"OldName => NewName"`. Атомарно обновляет имя параметра, ссылки `&Имя` в выражениях других параметров (только полные совпадения, `&ПериодX` не задевается), и записи в `dataParameters` всех вариантов. Текст запроса не трогает — переименование строго в области параметров. + +``` +"Период => ПериодОтчета" +``` + +### reorder-parameters — переставить параметры в указанном порядке + +Shorthand: `"Имя1, Имя2, Имя3"`. Частичный список — указанные параметры идут первыми в заданном порядке, остальные сохраняют исходный порядок и идут в конце. Параметры из списка, которых нет в схеме — warning, пропуск. + +``` +"ПериодОтчета, НачалоПериода, КонецПериода" +``` + +### add-filter — добавить фильтр в вариант + +Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`. + +``` +"Номенклатура = _ @off @user" +"Дата >= 2024-01-01T00:00:00" +"Статус filled" +``` + +### add-dataParameter — добавить параметр данных в вариант + +Shorthand: `"Имя [= значение] @флаги"`. + +``` +"Период = LastMonth @user" +"Организация @off @user" +``` + +### add-order — добавить сортировку + +Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент. + +``` +"Количество desc" +"Auto" +``` + +### add-selection — добавить элемент выборки + +``` +"Номенклатура" +"Auto" +"Folder(Поступление: ПолеА, ПолеБ, ПолеВ)" +``` + +`Folder(Название: поле1, поле2)` — группа полей (SelectedItemFolder) с заголовком и `placement=Auto`. + +`@group=ИмяГруппировки` — добавить в selection именованной группировки (вместо уровня варианта): + +``` +"Folder(Поступление: ПолеА, ПолеБ) @group=ДанныеОтчета" +``` + +### add-dataSetLink — добавить связь наборов данных + +Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`. + +``` +"Набор1 > Набор2 on Поле1 = Поле2" +"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" +``` + +### add-dataSet — добавить набор данных + +Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`). + +``` +"Доп: ВЫБРАТЬ 1 КАК Тест" +"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура" +"Продажи: @queries/sales.sql" +``` + +`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`). + +### add-variant — добавить вариант настроек + +Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя. + +``` +"Детальный" +"Детальный [Детальный отчёт]" +``` + +Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск. + +### add-conditionalAppearance — добавить условное оформление + +Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение). + +``` +"ЦветТекста = web:Red when Сумма < 0" +"ЦветФона = web:LightGreen when Статус = Одобрен for Статус" +"МинимальнаяШирина = 50 for Организация" +"Формат = ЧДЦ=2 for Цена, Сумма" +``` + +Типы значений appearance (автодетект): `web:*`/`style:*`/`win:*` → Color, `true`/`false` → Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, иначе String. + +Типы значений фильтра (автодетект): `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue, `true`/`false` → Boolean, дата → DateTime, числа → Decimal, иначе String. + +OrGroup: несколько условий через ` or ` в `when` объединяются в FilterItemGroup/OrGroup: + +``` +"Формат = ЧЦ=15; ЧДЦ=0 when ПараметрыДанных.Округление = Перечисление.Округления.Окр1 or ПараметрыДанных.Округление = Перечисление.Округления.Окр1000" +``` + +**Важно**: для параметров данных используйте префикс `ПараметрыДанных.` в поле фильтра. + +### add-drilldown — подключить расшифровку к ресурсам в шаблонах + +Value — имена ресурсов (как в полях/вычисляемых полях СКД) через запятую. + +``` +"ПоступлениеИзПроизводства, ВыбытиеПрочее" +"Сумма_Дт83, Сумма_Дт99, Сумма_68, Сумма_84" +``` + +Подключает DrillDown по `ИмяРесурса` ко всем шаблонам, содержащим указанные ресурсы. Идемпотентно. + +### set-query — заменить текст запроса + +Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD. + +Когда что: **существенная переработка** (добавить поля, соединения, переписать пакет) → выгрузи запрос через `/skd-info <tpl> -Mode query -Name <набор> -Raw -OutFile file.sql`, отредактируй файл, верни `set-query @file`. `-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат точны (включая многопакетные запросы). **Точечная замена** (переименовать идентификатор, заменить подстроку) → выгрузка не нужна, используй `patch-query` ниже. + +### patch-query — точечная замена в тексте запроса + +Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`. + +``` +"СубконтоДт1) В => СубконтоКт1) В" +"КАК ВТ_СтароеИмя => КАК ВТ_НовоеИмя @once" +``` + +`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах. + +Многострочные подстроки поддерживаются. + +### set-outputParameter — установить параметр вывода + +``` +"Заголовок = Мой отчёт" +"ВыводитьЗаголовок = true" +``` + +Если параметр уже существует — заменяет значение. + +### set-structure — установить структуру варианта + +Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим. + +``` +"Организация > Номенклатура > details" +"Валюта, НаименованиеБанка, ИНН" +"details" +"СчетМеждународногоУчета @name=ДанныеОтчета" +``` + +`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`. + +### modify-structure — изменить поля группировки существующей группы + +Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `<groupItems>` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка. + +``` +"Валюта @name=ДанныеОтчета" +"Валюта, НаименованиеБанка @name=ДанныеОтчета" +"details @name=ДанныеОтчета" +``` + +### modify-field — изменить существующее поле + +Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию. + +``` +"Цена [Цена USD]: decimal(10,4) @dimension" +``` + +### set-field-role — установить роль поля + +Shorthand: `"<dataPath> [@флаги] [kv=значение]"`. **Полностью заменяет** роль поля. Если в значении только dataPath без флагов/kv — удаляет роль. + +``` +"Сумма" # снять роль полностью +"СуммаОстаток @balance" # простая балансовая роль +"СуммаНач @balance balanceGroupName=Сумма balanceType=OpeningBalance" # с уточнением +"Контрагент @dimension parentDimension=Группа" +"Период @period" # роль периода +``` + +Флаги: `@balance`, `@dimension`, `@account`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`. +KV: `balanceGroupName`, `balanceType` (OpeningBalance/ClosingBalance), `parentDimension`, `accountTypeExpression`, `orderType` (Asc/Desc), `expression`, `periodNumber`, `periodType`. + +Поддерживает пакетный режим (`;;`). + +### modify-filter — изменить существующий фильтр + +Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `<use>` ниже. + +### modify-dataParameter — изменить параметр данных + +Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги. См. правило для `<use>` ниже. + +#### Правило `<use>` для modify-filter / modify-dataParameter + +В отличие от `add-*`, в `modify-*` поле `<use>` обновляется **только если флаг задан явно**: +- `@off` — установить `<use>false</use>` +- `@on` — убрать существующий `<use>false</use>` (включить параметр) +- ни `@off`, ни `@on` не задано — `<use>` не трогается, существующее значение сохраняется (важно: это значит, что отключённый параметр останется отключённым после модификации других свойств) + +### remove-* и clear-* + +| Операция | Value | Действие | +|----------|-------|----------| +| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта | +| `remove-total` | dataPath | Удаляет итог | +| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection | +| `remove-parameter` | name | Удаляет параметр | +| `remove-filter` | поле | Удаляет первый фильтр с указанным полем | +| `clear-selection` | `*` | Очищает все элементы selection | +| `clear-order` | `*` | Очищает все элементы order | +| `clear-filter` | `*` | Очищает все элементы filter | +| `clear-conditionalAppearance` | `*` | Очищает все правила условного оформления | + +## Верификация + +``` +/skd-validate <TemplatePath> — валидация структуры после редактирования +/skd-info <TemplatePath> — визуальная сводка +``` diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 641ef488..e635c65a 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4049 +1,4049 @@ -# skd-edit v1.27 — Atomic 1C DCS editor -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$TemplatePath, - - [Parameter(Mandatory)] - [ValidateSet( - "add-field","add-total","add-calculated-field","add-parameter","add-filter", - "add-dataParameter","add-order","add-selection","add-dataSetLink", - "add-dataSet","add-variant","add-conditionalAppearance","add-drilldown", - "set-query","patch-query","set-outputParameter","set-structure", - "modify-field","modify-filter","modify-dataParameter","modify-parameter","modify-structure","set-field-role", - "rename-parameter","reorder-parameters", - "clear-selection","clear-order","clear-filter","clear-conditionalAppearance", - "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")] - [string]$Operation, - - [Parameter(Mandatory)] - [string]$Value, - - [string]$DataSet, - [string]$Variant, - [switch]$NoSelection -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# Dirty flag — set to $true by every successful mutation. If still $false at save time, -# the file is left untouched (NO-OP operations like [WARN] not found don't rewrite). -$script:Dirty = $false - -# --- 1. Resolve path --- - -if (-not $TemplatePath.EndsWith(".xml")) { - $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" - if (Test-Path $candidate) { - $TemplatePath = $candidate - } -} - -if (-not (Test-Path $TemplatePath)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -$resolvedPath = (Resolve-Path $TemplatePath).Path - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $resolvedPath 'editable' - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>') -} - -function Resolve-QueryValue { - param([string]$val, [string]$baseDir) - if (-not $val.StartsWith("@")) { return $val } - $filePath = $val.Substring(1) - if ([System.IO.Path]::IsPathRooted($filePath)) { - $candidates = @($filePath) - } else { - $candidates = @( - (Join-Path $baseDir $filePath), - (Join-Path (Get-Location).Path $filePath) - ) - } - foreach ($c in $candidates) { - if (Test-Path $c) { - return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() - } - } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" - exit 1 -} - -$script:queryBaseDir = [System.IO.Path]::GetDirectoryName($resolvedPath) - -# --- 2. Type system (copied from skd-compile) --- - -$script:typeSynonyms = New-Object System.Collections.Hashtable -$script:typeSynonyms["число"] = "decimal" -$script:typeSynonyms["строка"] = "string" -$script:typeSynonyms["булево"] = "boolean" -$script:typeSynonyms["дата"] = "date" -$script:typeSynonyms["датавремя"] = "dateTime" -$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" -$script:typeSynonyms["bool"] = "boolean" -$script:typeSynonyms["str"] = "string" -$script:typeSynonyms["int"] = "decimal" -$script:typeSynonyms["integer"] = "decimal" -$script:typeSynonyms["number"] = "decimal" -$script:typeSynonyms["num"] = "decimal" -$script:typeSynonyms["справочникссылка"] = "CatalogRef" -$script:typeSynonyms["документссылка"] = "DocumentRef" -$script:typeSynonyms["перечислениессылка"] = "EnumRef" -$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" - -$script:outputParamTypes = @{ - "Заголовок" = "mltext" - "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" - "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" - "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" - "МакетОформления" = "xs:string" - "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" - "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" - "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" - "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" -} - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $baseName = $Matches[1].Trim() - $params = $Matches[2] - $resolved = $script:typeSynonyms[$baseName.ToLower()] - if ($resolved) { return "$resolved($params)" } - return $typeStr - } - - if ($typeStr.Contains('.')) { - $dotIdx = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $dotIdx) - $suffix = $typeStr.Substring($dotIdx) - $resolved = $script:typeSynonyms[$prefix.ToLower()] - if ($resolved) { return "$resolved$suffix" } - return $typeStr - } - - $resolved = $script:typeSynonyms[$typeStr.ToLower()] - if ($resolved) { return $resolved } - return $typeStr -} - -# --- 3. Parsers --- - -function Parse-FieldShorthand { - param([string]$s) - - $result = @{ - dataPath = ""; field = ""; title = ""; type = "" - roles = @(); restrict = @() - } - - # Extract [Title] - if ($s -match '\[([^\]]+)\]') { - $result.title = $Matches[1] - $s = $s -replace '\s*\[[^\]]+\]', '' - } - - # Extract @roles - $roleMatches = [regex]::Matches($s, '@(\w+)') - foreach ($m in $roleMatches) { - $result.roles += $m.Groups[1].Value - } - $s = [regex]::Replace($s, '\s*@\w+', '') - - # Extract #restrictions - $restrictMatches = [regex]::Matches($s, '#(\w+)') - foreach ($m in $restrictMatches) { - $result.restrict += $m.Groups[1].Value - } - $s = [regex]::Replace($s, '\s*#\w+', '') - - # Split name: type - $s = $s.Trim() - if ($s.Contains(':')) { - $parts = $s -split ':', 2 - $result.dataPath = $parts[0].Trim() - $result.type = Resolve-TypeStr ($parts[1].Trim()) - } else { - $result.dataPath = $s - } - - $result.field = $result.dataPath - return $result -} - -function Read-FieldProperties($fieldEl) { - $props = @{ - dataPath = ""; field = ""; title = ""; type = "" - roles = @(); restrict = @() - _rawTitle = $null - _unknownChildren = @() - } - - foreach ($ch in $fieldEl.ChildNodes) { - if ($ch.NodeType -ne 'Element') { continue } - switch ($ch.LocalName) { - "dataPath" { $props.dataPath = $ch.InnerText.Trim() } - "field" { $props.field = $ch.InnerText.Trim() } - "title" { - # Preserve full multi-lang title OuterXml — used to keep en/uk/etc. - # siblings when shorthand overrides only the ru content. Strip xmlns - # redeclarations that OuterXml adds for sub-elements. - $raw = $ch.OuterXml - $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') - $props._rawTitle = $raw - # Also extract ru content as plain string (backward compat — used by - # external consumers reading $existing.title). - foreach ($item in $ch.ChildNodes) { - if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') { - $lang = $null; $content = $null - foreach ($gc in $item.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'lang') { $lang = $gc.InnerText.Trim() } - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') { $content = $gc.InnerText.Trim() } - } - if ($lang -eq 'ru' -and $null -ne $content) { $props.title = $content } - } - } - } - "valueType" { - # Preserve the entire <valueType> OuterXml so rebuild can re-emit qualifiers - # (StringQualifiers, NumberQualifiers, DateQualifiers, etc.) that would - # otherwise be lost. Also extract Type string for type-override shorthand. - $raw = $ch.OuterXml - # .NET OuterXml re-declares xmlns on every element where the prefix is in - # scope (because the fragment is treated as standalone). Strip these since - # the parent context at insertion point already provides them. - $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') - $props["_rawValueType"] = $raw - $typeEl = $null - foreach ($gc in $ch.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') { - $typeEl = $gc; break - } - } - if ($typeEl) { - $props["_rawTypeText"] = $typeEl.InnerText.Trim() - } - } - "role" { - foreach ($gc in $ch.ChildNodes) { - if ($gc.NodeType -eq 'Element') { - if ($gc.LocalName -eq 'periodNumber') { - $props.roles += "period" - } elseif ($gc.InnerText.Trim() -eq 'true') { - $props.roles += $gc.LocalName - } - } - } - } - "useRestriction" { - $revMap = @{ "field" = "noField"; "condition" = "noFilter"; "group" = "noGroup"; "order" = "noOrder" } - foreach ($gc in $ch.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.InnerText.Trim() -eq 'true') { - $mapped = $revMap[$gc.LocalName] - if ($mapped) { $props.restrict += $mapped } - } - } - } - default { - # Defense in depth: preserve OuterXml of unknown children so rebuild - # doesn't silently drop them (custom <editFormat>, <appearance>, etc.). - $raw = $ch.OuterXml - $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') - $props._unknownChildren += $raw - } - } - } - return $props -} - -function Parse-TotalShorthand { - param([string]$s) - - # "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity) - $parts = $s -split ':', 2 - $dataPath = $parts[0].Trim() - $funcPart = $parts[1].Trim() - - # Known DCS aggregate functions (ru + en) - $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', - 'Sum','Count','Min','Max','Avg', - 'Minimum','Maximum','Average') - - if ($funcPart -match '^\w+\(') { - # Already has expression form: Func(expr) - return @{ dataPath = $dataPath; expression = $funcPart } - } elseif ($funcPart -in $aggFuncs) { - # Short: Func → Func(DataPath) - return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } - } else { - # Identity or custom expression — use as-is - return @{ dataPath = $dataPath; expression = $funcPart } - } -} - -function Parse-CalcShorthand { - param([string]$s) - - # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". - # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside - # an expression (e.g. index access) isn't interpreted as a title. - # - `#restrict` flags use a known-names pattern and are extracted globally — - # the docs put them after `=`, and the closed flag set avoids matching - # `#word` that happens to appear inside a string literal. - $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' - - $restrict = @() - foreach ($m in [regex]::Matches($s, $restrictPattern)) { - $restrict += $m.Groups[1].Value - } - $s = [regex]::Replace($s, "\s*$restrictPattern", '') - - $eqIdx = $s.IndexOf('=') - if ($eqIdx -gt 0) { - $lhs = $s.Substring(0, $eqIdx) - $rhs = $s.Substring($eqIdx + 1).Trim() - } else { - $lhs = $s - $rhs = $null - } - - $title = "" - if ($lhs -match '\[([^\]]+)\]') { - $title = $Matches[1] - $lhs = $lhs -replace '\s*\[[^\]]+\]', '' - } - $lhs = $lhs.Trim() - - if ($null -ne $rhs) { - if ($lhs.Contains(':')) { - $colonIdx = $lhs.IndexOf(':') - $dataPath = $lhs.Substring(0, $colonIdx).Trim() - $type = Resolve-TypeStr ($lhs.Substring($colonIdx + 1).Trim()) - return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict } - } - return @{ dataPath = $lhs; expression = $rhs; type = ""; title = $title; restrict = $restrict } - } - return @{ dataPath = $lhs; expression = ""; type = ""; title = $title; restrict = $restrict } -} - -function Parse-ParamShorthand { - param([string]$s) - - $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @(); valueListAllowed = $false } - - # Extract availableValue=... (must be before main parse — captures to end of string) - if ($s -match '\s*availableValue=(.+)$') { - $result.availableValues = Parse-AvailableValueList $Matches[1].Trim() - $s = ($s -replace '\s*availableValue=.+$', '').Trim() - } - - if ($s -match '@autoDates') { - $result.autoDates = $true - $s = $s -replace '\s*@autoDates', '' - } - - if ($s -match '@valueList\b') { - $result.valueListAllowed = $true - $s = $s -replace '\s*@valueList\b', '' - } - - if ($s -match '@hidden\b') { - $result.hidden = $true - $s = $s -replace '\s*@hidden\b', '' - } - - if ($s -match '@always\b') { - $result.always = $true - $s = $s -replace '\s*@always\b', '' - } - - # Extract optional [Title] (mirrors Parse-FieldShorthand) - if ($s -match '\[([^\]]*)\]') { - $result.title = $Matches[1].Trim() - $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() - } - - # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty-value sentinel - if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { - $result.name = $Matches[1].Trim() - $result.type = Resolve-TypeStr ($Matches[2].Trim()) - $hasEq = $null -ne $Matches[3] - $rhs = $Matches[4] - if ($hasEq) { - if ($rhs -and $rhs.Trim()) { - $items = Parse-ValueList $rhs.Trim() - if ($items.Count -ge 2) { - # Multi-value default → list; valueListAllowed implied - $result.value = $items - $result.valueListAllowed = $true - } else { - # Scalar (single item, quotes stripped) or empty sentinel - $result.value = if ($items.Count -eq 1) { $items[0] } else { "" } - } - } else { - $result.value = "" - } - } - } else { - $result.name = $s.Trim() - } - - return $result -} - -function Parse-FilterShorthand { - param([string]$s) - - # use is tristate: $null = not specified (modify-* won't touch), - # $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false. - $result = @{ field = ""; op = "Equal"; value = $null; use = $null; userSettingID = $null; viewMode = $null } - - if ($s -match '@user') { - $result.userSettingID = "auto" - $s = $s -replace '\s*@user', '' - } - if ($s -match '@off') { - $result.use = $false - $s = $s -replace '\s*@off', '' - } - if ($s -match '@on\b') { - $result.use = $true - $s = $s -replace '\s*@on\b', '' - } - if ($s -match '@quickAccess') { - $result.viewMode = "QuickAccess" - $s = $s -replace '\s*@quickAccess', '' - } - if ($s -match '@normal') { - $result.viewMode = "Normal" - $s = $s -replace '\s*@normal', '' - } - if ($s -match '@inaccessible') { - $result.viewMode = "Inaccessible" - $s = $s -replace '\s*@inaccessible', '' - } - - $s = $s.Trim() - - $opPatterns = @('<>', '>=', '<=', '=', '>', '<', - 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', - 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', - 'notFilled\b', 'filled\b') - $opJoined = $opPatterns -join '|' - - if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { - $result.field = $Matches[1].Trim() - $opRaw = $Matches[2].Trim() - $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } - - $opMap = @{ - "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" - "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" - "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" - "contains" = "Contains"; "notContains" = "NotContains" - "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" - "filled" = "Filled"; "notFilled" = "NotFilled" - } - $mapped = $opMap[$opRaw] - if ($mapped) { $result.op = $mapped } else { $result.op = $opRaw } - - if ($valPart -and $valPart -ne "_") { - if ($valPart -eq "true" -or $valPart -eq "false") { - $result.value = $valPart - $result["valueType"] = "xs:boolean" - } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { - $result.value = $valPart - $result["valueType"] = "xs:dateTime" - } elseif ($valPart -match '^\d+(\.\d+)?$') { - $result.value = $valPart - $result["valueType"] = "xs:decimal" - } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { - $result.value = $valPart - $result["valueType"] = "dcscor:DesignTimeValue" - } else { - $result.value = $valPart - $result["valueType"] = "xs:string" - } - } - } else { - $result.field = $s - } - - return $result -} - -function Parse-DataParamShorthand { - param([string]$s) - - # use is tristate: $null = not specified (modify-* won't touch), - # $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false. - $result = @{ parameter = ""; value = $null; use = $null; userSettingID = $null; viewMode = $null } - - if ($s -match '@user') { - $result.userSettingID = "auto" - $s = $s -replace '\s*@user', '' - } - if ($s -match '@off') { - $result.use = $false - $s = $s -replace '\s*@off', '' - } - if ($s -match '@on\b') { - $result.use = $true - $s = $s -replace '\s*@on\b', '' - } - if ($s -match '@quickAccess') { - $result.viewMode = "QuickAccess" - $s = $s -replace '\s*@quickAccess', '' - } - if ($s -match '@normal') { - $result.viewMode = "Normal" - $s = $s -replace '\s*@normal', '' - } - - $s = $s.Trim() - - if ($s -match '^([^=]+)=\s*(.*)$') { - $result.parameter = $Matches[1].Trim() - $valStr = $Matches[2].Trim() - - $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") - # Empty / sentinel — record as "" so caller emits xsi:nil - if ($valStr -eq "" -or $valStr -eq "_" -or $valStr.ToLowerInvariant() -eq "null") { - $result.value = "" - } elseif ($periodVariants -contains $valStr) { - $result.value = @{ variant = $valStr } - } else { - $result.value = $valStr - } - } else { - $result.parameter = $s - } - - return $result -} - -function Parse-OrderShorthand { - param([string]$s) - $s = $s.Trim() - if ($s -eq "Auto") { - return @{ field = "Auto"; direction = "" } - } - $parts = $s -split '\s+', 2 - $field = $parts[0] - $dir = "Asc" - if ($parts.Count -gt 1 -and $parts[1] -match '(?i)^desc$') { $dir = "Desc" } - return @{ field = $field; direction = $dir } -} - -function Parse-DataSetLinkShorthand { - param([string]$s) - - $result = @{ source = ""; dest = ""; sourceExpr = ""; destExpr = ""; parameter = "" } - - # Extract optional [param ParamName] - if ($s -match '\[param\s+([^\]]+)\]') { - $result.parameter = $Matches[1].Trim() - $s = $s -replace '\s*\[param\s+[^\]]+\]', '' - } - - # Pattern: "Source > Dest on FieldA = FieldB" - if ($s -match '^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$') { - $result.source = $Matches[1].Trim() - $result.dest = $Matches[2].Trim() - $result.sourceExpr = $Matches[3].Trim() - $result.destExpr = $Matches[4].Trim() - } else { - Write-Error "Invalid dataSetLink shorthand: $s. Expected: 'Source > Dest on FieldA = FieldB [param Name]'" - exit 1 - } - - return $result -} - -function Parse-DataSetShorthand { - param([string]$s) - - $s = $s.Trim() - # "Name: QUERY" — split on first ": " only if prefix is a single word (no spaces) - if ($s -match '^(\S+):\s(.+)$') { - return @{ name = $Matches[1]; query = $Matches[2] } - } - return @{ name = ""; query = $s } -} - -function Parse-VariantShorthand { - param([string]$s) - - $presentation = "" - if ($s -match '\[([^\]]+)\]') { - $presentation = $Matches[1] - $s = $s -replace '\s*\[[^\]]+\]', '' - } - $name = $s.Trim() - if (-not $presentation) { $presentation = $name } - return @{ name = $name; presentation = $presentation } -} - -function Parse-ConditionalAppearanceShorthand { - param([string]$s) - - $result = @{ param = ""; value = ""; filter = $null; fields = @() } - - # Extract " when ..." — condition part - $whenIdx = $s.IndexOf(' when ') - $forIdx = $s.IndexOf(' for ') - - # Determine boundaries - $mainEnd = $s.Length - if ($whenIdx -ge 0 -and $forIdx -ge 0) { - $mainEnd = [Math]::Min($whenIdx, $forIdx) - } elseif ($whenIdx -ge 0) { - $mainEnd = $whenIdx - } elseif ($forIdx -ge 0) { - $mainEnd = $forIdx - } - - # Parse "for" fields - if ($forIdx -ge 0) { - $forEnd = $s.Length - if ($whenIdx -gt $forIdx) { $forEnd = $whenIdx } - $forPart = $s.Substring($forIdx + 5, $forEnd - $forIdx - 5).Trim() - $result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } - - # Parse "when" filter (supports " or " for OrGroup) - if ($whenIdx -ge 0) { - $whenEnd = $s.Length - if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx } - $whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim() - $orParts = $whenPart -split '\s+or\s+' - if ($orParts.Count -gt 1) { - $result.filter = @($orParts | ForEach-Object { Parse-FilterShorthand $_.Trim() }) - } else { - $result.filter = Parse-FilterShorthand $whenPart - } - } - - # Parse main part: "Param = Value" - $mainPart = $s.Substring(0, $mainEnd).Trim() - $eqIdx = $mainPart.IndexOf('=') - if ($eqIdx -gt 0) { - $result.param = $mainPart.Substring(0, $eqIdx).Trim() - $result.value = $mainPart.Substring($eqIdx + 1).Trim() - } else { - $result.param = $mainPart - } - - return $result -} - -function Parse-StructureShorthand { - param([string]$s) - - $segments = $s -split '\s*>\s*' - $result = @() - - $innermost = $null - for ($i = $segments.Count - 1; $i -ge 0; $i--) { - $seg = $segments[$i].Trim() - $group = @{ type = "group" } - - if ($seg -match '@name=(?:"([^"]+)"|''([^'']+)''|(\S+))') { - $rawName = if ($Matches[1]) { $Matches[1] } elseif ($Matches[2]) { $Matches[2] } else { $Matches[3] } - $group["name"] = $rawName.Trim() - $seg = ($seg -replace '\s*@name=(?:"[^"]+"|''[^'']+''|\S+)', '').Trim() - } - - if ($seg -match '^(?i)(details|детали)$') { - $group["groupBy"] = @() - } else { - $fields = @($seg -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - $group["groupBy"] = $fields - } - - if ($null -ne $innermost) { - $group["children"] = @($innermost) - } - $innermost = $group - } - - if ($innermost) { $result += $innermost } - return ,$result -} - -function Parse-OutputParamShorthand { - param([string]$s) - $idx = $s.IndexOf('=') - if ($idx -gt 0) { - return @{ - key = $s.Substring(0, $idx).Trim() - value = $s.Substring($idx + 1).Trim() - } - } - return @{ key = $s.Trim(); value = "" } -} - -function Split-QuotedCsv { - # Splits on top-level commas, respecting 'single' and "double" quoted spans. - # Returns raw (un-stripped, un-trimmed) item spans. Used by both availableValue - # (value:presentation) and value-list (values only) parsing. - param([string]$s) - $items = @() - if ($null -eq $s) { return ,$items } - $buf = New-Object System.Text.StringBuilder - $inQuote = $null - for ($i = 0; $i -lt $s.Length; $i++) { - $ch = $s[$i] - if ($inQuote) { - [void]$buf.Append($ch) - if ($ch -eq $inQuote) { $inQuote = $null } - } elseif ($ch -eq "'" -or $ch -eq '"') { - $inQuote = $ch - [void]$buf.Append($ch) - } elseif ($ch -eq ',') { - $items += $buf.ToString() - [void]$buf.Clear() - } else { - [void]$buf.Append($ch) - } - } - if ($buf.Length -gt 0) { $items += $buf.ToString() } - return ,$items -} - -function Strip-Quotes { - # Strips a single surrounding pair of matching quotes; trims first. - param([string]$t) - $t = $t.Trim() - if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { - return $t.Substring(1, $t.Length - 2) - } - return $t -} - -function Parse-ValueList { - # Returns array of value strings (quotes stripped) split by top-level commas. - # No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00). - param([string]$s) - $result = @() - if ($null -eq $s) { return ,$result } - foreach ($raw in (Split-QuotedCsv $s)) { - $v = Strip-Quotes $raw - if ($v -ne "") { $result += $v } - } - return ,$result -} - -function Parse-AvailableValueList { - # Returns array of @{ value=...; presentation=... } from comma-separated list. - # Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons. - param([string]$s) - - $result = @() - if (-not $s) { return ,$result } - - $items = Split-QuotedCsv $s - $stripQuotes = { param($t) Strip-Quotes $t } - - foreach ($raw in $items) { - $item = $raw.Trim() - if (-not $item) { continue } - - # Find first ':' outside quotes - $colonIdx = -1 - $q = $null - for ($j = 0; $j -lt $item.Length; $j++) { - $c = $item[$j] - if ($q) { - if ($c -eq $q) { $q = $null } - } elseif ($c -eq "'" -or $c -eq '"') { - $q = $c - } elseif ($c -eq ':') { - $colonIdx = $j; break - } - } - - if ($colonIdx -ge 0) { - $valPart = $item.Substring(0, $colonIdx) - $presPart = $item.Substring($colonIdx + 1) - $result += @{ value = (& $stripQuotes $valPart); presentation = (& $stripQuotes $presPart) } - } else { - $result += @{ value = (& $stripQuotes $item); presentation = "" } - } - } - - return ,$result -} - -# --- 4. Build-* functions (XML fragment generators) --- - -function Build-ValueTypeXml { - param($typeStr, [string]$indent) - - if (-not $typeStr) { return "" } - - # Composite: array of types — concatenate per-type fragments - if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { - $parts = @() - foreach ($t in $typeStr) { - $p = Build-ValueTypeXml -typeStr "$t" -indent $indent - if ($p) { $parts += $p } - } - return $parts -join "`n" - } - - $typeStr = Resolve-TypeStr "$typeStr" - $lines = @() - - if ($typeStr -eq "boolean") { - $lines += "$indent<v8:Type>xs:boolean</v8:Type>" - return $lines -join "`n" - } - - # string, string(N), string(N,fix) — fix → AllowedLength=Fixed - if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - $al = if ($Matches[4]) { "Fixed" } else { "Variable" } - $lines += "$indent<v8:Type>xs:string</v8:Type>" - $lines += "$indent<v8:StringQualifiers>" - $lines += "$indent`t<v8:Length>$len</v8:Length>" - $lines += "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" - $lines += "$indent</v8:StringQualifiers>" - return $lines -join "`n" - } - - # decimal forms — bare decimal = money 10,2; decimal(N) = integer N,0 - if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { - if (-not $Matches[1]) { - $digits = "10"; $fraction = "2"; $sign = "Any" - } else { - $digits = $Matches[2] - $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } - $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } - } - $lines += "$indent<v8:Type>xs:decimal</v8:Type>" - $lines += "$indent<v8:NumberQualifiers>" - $lines += "$indent`t<v8:Digits>$digits</v8:Digits>" - $lines += "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" - $lines += "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" - $lines += "$indent</v8:NumberQualifiers>" - return $lines -join "`n" - } - - # date / dateTime / time — all xs:dateTime, differ only in DateFractions - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { - "date" { "Date" } - "dateTime" { "DateTime" } - "time" { "Time" } - } - $lines += "$indent<v8:Type>xs:dateTime</v8:Type>" - $lines += "$indent<v8:DateQualifiers>" - $lines += "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" - $lines += "$indent</v8:DateQualifiers>" - return $lines -join "`n" - } - - if ($typeStr -eq "StandardPeriod") { - $lines += "$indent<v8:Type>v8:StandardPeriod</v8:Type>" - return $lines -join "`n" - } - - if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { - $lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" - return $lines -join "`n" - } - - if ($typeStr.Contains('.')) { - $lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" - return $lines -join "`n" - } - - $lines += "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>" - return $lines -join "`n" -} - -# Sentinel-normalized empty check — null / "" / "_" / "null" (case-insensitive). -function Test-EmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "") { return $true } - if ($s -eq "_") { return $true } - if ($s.ToLowerInvariant() -eq "null") { return $true } - return $false -} - -# Returns XML fragment string for a type-aware empty <value>. -# Empty + valueListAllowed → omit entirely (returns $null). -# tagPrefix used for dcscor: in data parameters. -function Build-EmptyValueXml { - param([string]$type, [string]$indent, [string]$tagPrefix = "", [string]$tagName = "value", [bool]$valueListAllowed = $false) - if ($valueListAllowed) { return $null } - $t = if ($null -eq $type) { "" } else { "$type" } - # Strip well-known XML schema prefixes so callers can pass raw <v8:Type> text - $t = $t -replace '^xs:', '' -replace '^v8:', '' -replace '^d\d+p\d+:', '' - $pf = $tagPrefix - $tn = $tagName - $lines = @() - if ($t -eq "") { - $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" - } elseif ($t -eq "StandardPeriod") { - $lines += "$indent<${pf}${tn} xsi:type=`"v8:StandardPeriod`">" - $lines += "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" - $lines += "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - $lines += "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - $lines += "$indent</${pf}${tn}>" - } elseif ($t -match '^string') { - $lines += "$indent<${pf}${tn} xsi:type=`"xs:string`"/>" - } elseif ($t -match '^(date|time)') { - $lines += "$indent<${pf}${tn} xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}${tn}>" - } elseif ($t -match '^decimal') { - $lines += "$indent<${pf}${tn} xsi:type=`"xs:decimal`">0</${pf}${tn}>" - } elseif ($t -eq "boolean") { - $lines += "$indent<${pf}${tn} xsi:type=`"xs:boolean`">false</${pf}${tn}>" - } else { - # Ref types or unknown — safe nil - $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" - } - return $lines -join "`n" -} - -function Build-MLTextXml { - param([string]$tag, [string]$text, [string]$indent) - $lines = @() - $lines += "$indent<$tag xsi:type=`"v8:LocalStringType`">" - $lines += "$indent`t<v8:item>" - $lines += "$indent`t`t<v8:lang>ru</v8:lang>" - $lines += "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>" - $lines += "$indent`t</v8:item>" - $lines += "$indent</$tag>" - return $lines -join "`n" -} - -# Patches the ru <v8:content> within an existing multi-lang title OuterXml, preserving -# en/uk/etc. siblings. Used when modify-* operates with a title-override shorthand on a -# field/parameter that already has multi-language titles (typical in ERP/БП/ЗУП). -# If no ru item exists, one is prepended before the first existing item. -function Patch-MLTextRu { - param([string]$rawOuterXml, [string]$newRuText, [string]$indent) - $escaped = Esc-Xml $newRuText - $ruItemPat = '(<v8:item>\s*<v8:lang>ru</v8:lang>\s*<v8:content>)[^<]*(</v8:content>\s*</v8:item>)' - if ([regex]::IsMatch($rawOuterXml, $ruItemPat)) { - return [regex]::Replace($rawOuterXml, $ruItemPat, { param($m) $m.Groups[1].Value + $escaped + $m.Groups[2].Value }) - } - # No ru item — prepend one inside the title element, before first <v8:item>. - $prep = "$indent`t<v8:item>`n$indent`t`t<v8:lang>ru</v8:lang>`n$indent`t`t<v8:content>$escaped</v8:content>`n$indent`t</v8:item>" - if ($rawOuterXml -match '<v8:item>') { - $re = New-Object System.Text.RegularExpressions.Regex('(\s*)<v8:item>') - return $re.Replace($rawOuterXml, "`n$prep`$1<v8:item>", 1) - } - # Empty title — inject after opening tag. - $re2 = New-Object System.Text.RegularExpressions.Regex('(<(?:\w+:)?title[^>]*>)') - return $re2.Replace($rawOuterXml, "`$1`n$prep`n$indent", 1) -} - -function Build-RoleXml { - param([string[]]$roles, [string]$indent) - - if (-not $roles -or $roles.Count -eq 0) { return "" } - - $lines = @() - $lines += "$indent<role>" - foreach ($role in $roles) { - if ($role -eq "period") { - $lines += "$indent`t<dcscom:periodNumber>1</dcscom:periodNumber>" - $lines += "$indent`t<dcscom:periodType>Main</dcscom:periodType>" - } else { - $lines += "$indent`t<dcscom:$role>true</dcscom:$role>" - } - } - $lines += "$indent</role>" - return $lines -join "`n" -} - -function Build-RestrictionXml { - param([string[]]$restrict, [string]$indent) - - if (-not $restrict -or $restrict.Count -eq 0) { return "" } - - $restrictMap = @{ - "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" - "noGroup" = "group"; "noOrder" = "order" - } - - $lines = @() - $lines += "$indent<useRestriction>" - foreach ($r in $restrict) { - $xmlName = $restrictMap["$r"] - if ($xmlName) { - $lines += "$indent`t<$xmlName>true</$xmlName>" - } - } - $lines += "$indent</useRestriction>" - return $lines -join "`n" -} - -function Build-FieldFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<field xsi:type=`"DataSetFieldField`">" - $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" - $lines += "$i`t<field>$(Esc-Xml $parsed.field)</field>" - - # Title: prefer raw multi-lang title (preserves en/uk/etc.). When shorthand provides - # a new ru text, patch ru content inside the raw title; otherwise emit raw as-is. - # When no raw title exists, fall back to ru-only build from shorthand. - if ($parsed._rawTitle) { - if ($parsed.title -and $parsed.title -ne $parsed._existingTitleRu) { - $lines += "$i`t" + (Patch-MLTextRu $parsed._rawTitle $parsed.title "$i`t") - } else { - $lines += "$i`t" + $parsed._rawTitle - } - } elseif ($parsed.title) { - $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") - } - - if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { - $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") - } - - $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t" - if ($roleXml) { $lines += $roleXml } - - if ($parsed.rawValueType) { - # Preserve original <valueType> verbatim — keeps qualifiers (StringQualifiers, - # NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand. - $lines += "$i`t" + $parsed.rawValueType - } elseif ($parsed.type) { - $lines += "$i`t<valueType>" - $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") - $lines += "$i`t</valueType>" - } - - # Defense in depth: re-emit OuterXml of unknown children (e.g. <editFormat>, - # <appearance>, custom extensions) that Read-FieldProperties captured. - if ($parsed._unknownChildren) { - foreach ($raw in $parsed._unknownChildren) { - $lines += "$i`t" + $raw - } - } - - $lines += "$i</field>" - return $lines -join "`n" -} - -function Build-TotalFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<totalField>" - $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" - $lines += "$i`t<expression>$(Esc-Xml $parsed.expression)</expression>" - $lines += "$i</totalField>" - return $lines -join "`n" -} - -function Build-CalcFieldFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<calculatedField>" - $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" - $lines += "$i`t<expression>$(Esc-Xml $parsed.expression)</expression>" - - if ($parsed.title) { - $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") - } - - if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { - $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") - } - - if ($parsed.type) { - $lines += "$i`t<valueType>" - $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") - $lines += "$i`t</valueType>" - } - - $lines += "$i</calculatedField>" - return $lines -join "`n" -} - -function Build-ParamValueXml { - # Returns array of XML lines for a <value xsi:type=...>...</value> element (or StandardPeriod block). - # Selects xsi:type by declared type, then falls back to value pattern. - param([string]$type, [string]$value, [string]$indent, [string]$tagName = "value", [string]$tagNs = "") - - $i = $indent - $valStr = "$value" - $open = if ($tagNs) { "$tagNs`:$tagName" } else { $tagName } - $lines = @() - - if ($type -eq "StandardPeriod") { - $lines += "$i<$open xsi:type=`"v8:StandardPeriod`">" - $lines += "$i`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>" - $lines += "$i`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - $lines += "$i`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - $lines += "$i</$open>" - return $lines - } - - $xsi = $null - if ($type -match '^date') { $xsi = "xs:dateTime" } - elseif ($type -eq "boolean") { $xsi = "xs:boolean" } - elseif ($type -match '^decimal') { $xsi = "xs:decimal" } - elseif ($type -match '^string') { $xsi = "xs:string" } - elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { - $xsi = "dcscor:DesignTimeValue" - } - else { - # Type unknown or empty — guess from value - if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $xsi = "xs:dateTime" } - elseif ($valStr -eq "true" -or $valStr -eq "false") { $xsi = "xs:boolean" } - elseif ($valStr -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or - $valStr -match '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - $xsi = "dcscor:DesignTimeValue" - } - else { $xsi = "xs:string" } - } - - $lines += "$i<$open xsi:type=`"$xsi`">$(Esc-Xml $valStr)</$open>" - return $lines -} - -function Build-AvailableValueFragment { - # Returns XML lines (array) for a single <availableValue> block. - param($item, [string]$declaredType, [string]$indent) - - $lines = @() - $lines += "$indent<availableValue>" - if (Test-EmptyValue $item.value) { - $emptyXml = Build-EmptyValueXml -type $declaredType -indent "$indent`t" -tagPrefix "" -tagName "value" -valueListAllowed $false - if ($emptyXml) { $lines += $emptyXml } - } else { - $valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t" - foreach ($vl in $valueLines) { $lines += $vl } - } - if ($item.presentation) { - $lines += "$indent`t<presentation xsi:type=`"v8:LocalStringType`">" - $lines += "$indent`t`t<v8:item>" - $lines += "$indent`t`t`t<v8:lang>ru</v8:lang>" - $lines += "$indent`t`t`t<v8:content>$(Esc-Xml $item.presentation)</v8:content>" - $lines += "$indent`t`t</v8:item>" - $lines += "$indent`t</presentation>" - } - $lines += "$indent</availableValue>" - return $lines -} - -function Build-ParamFragment { - param($parsed, [string]$indent) - - $i = $indent - $fragments = @() - - $lines = @() - $lines += "$i<parameter>" - $lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>" - - if ($parsed.title) { - $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") - } - - if ($parsed.type) { - $lines += "$i`t<valueType>" - $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") - $lines += "$i`t</valueType>" - } - - $vla = [bool]$parsed.valueListAllowed - $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) - if ($valIsArray) { - # Multi-value default (value-list): one <value> per item - foreach ($v in $parsed.value) { - $valueLines = Build-ParamValueXml -type $parsed.type -value $v -indent "$i`t" - foreach ($vl in $valueLines) { $lines += $vl } - } - } elseif ($null -ne $parsed.value) { - if (Test-EmptyValue $parsed.value) { - $emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla - if ($emptyXml) { $lines += $emptyXml } - } else { - $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t" - foreach ($vl in $valueLines) { $lines += $vl } - } - } - - if ($parsed.hidden) { - $lines += "$i`t<useRestriction>true</useRestriction>" - $lines += "$i`t<availableAsField>false</availableAsField>" - } - - if ($vla) { - $lines += "$i`t<valueListAllowed>true</valueListAllowed>" - } - - if ($parsed.availableValues -and $parsed.availableValues.Count -gt 0) { - foreach ($av in $parsed.availableValues) { - $avLines = Build-AvailableValueFragment -item $av -declaredType $parsed.type -indent "$i`t" - foreach ($l in $avLines) { $lines += $l } - } - } - - if ($parsed.always) { - $lines += "$i`t<use>Always</use>" - } - - $lines += "$i</parameter>" - $fragments += ($lines -join "`n") - - if ($parsed.autoDates) { - $paramName = $parsed.name - - # Canonical БСП pattern: title + valueType + value + useRestriction + expression - $bLines = @() - $bLines += "$i<parameter>" - $bLines += "$i`t<name>ДатаНачала</name>" - $bLines += (Build-MLTextXml -tag "title" -text "Начало периода" -indent "$i`t") - $bLines += "$i`t<valueType>" - $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") - $bLines += "$i`t</valueType>" - $bLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>" - $bLines += "$i`t<useRestriction>true</useRestriction>" - $bLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаНачала")</expression>" - $bLines += "$i</parameter>" - $fragments += ($bLines -join "`n") - - $eLines = @() - $eLines += "$i<parameter>" - $eLines += "$i`t<name>ДатаОкончания</name>" - $eLines += (Build-MLTextXml -tag "title" -text "Конец периода" -indent "$i`t") - $eLines += "$i`t<valueType>" - $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") - $eLines += "$i`t</valueType>" - $eLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>" - $eLines += "$i`t<useRestriction>true</useRestriction>" - $eLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаОкончания")</expression>" - $eLines += "$i</parameter>" - $fragments += ($eLines -join "`n") - } - - return ,$fragments -} - -function Build-FilterItemFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" - - if ($parsed.use -eq $false) { - $lines += "$i`t<dcsset:use>false</dcsset:use>" - } - - $lines += "$i`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $parsed.field)</dcsset:left>" - $lines += "$i`t<dcsset:comparisonType>$(Esc-Xml $parsed.op)</dcsset:comparisonType>" - - if ($null -ne $parsed.value) { - $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } - $lines += "$i`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($parsed.value)")</dcsset:right>" - } - - if ($parsed.viewMode) { - $lines += "$i`t<dcsset:viewMode>$(Esc-Xml $parsed.viewMode)</dcsset:viewMode>" - } - - if ($parsed.userSettingID) { - $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } - $lines += "$i`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - - $lines += "$i</dcsset:item>" - return $lines -join "`n" -} - -function Build-SelectionItemFragment { - param([string]$fieldName, [string]$indent) - - $i = $indent - $lines = @() - if ($fieldName -eq "Auto") { - $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" - } elseif ($fieldName -match '^Folder\((.+)\)$') { - $inner = $Matches[1] - $colonIdx = $inner.IndexOf(':') - if ($colonIdx -gt 0) { - $title = $inner.Substring(0, $colonIdx).Trim() - $items = $inner.Substring($colonIdx + 1) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - } else { - $title = "" - $items = $inner -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - } - $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">" - if ($title) { - $lines += "$i`t<dcsset:lwsTitle>" - $lines += "$i`t`t<v8:item>" - $lines += "$i`t`t`t<v8:lang>ru</v8:lang>" - $lines += "$i`t`t`t<v8:content>$(Esc-Xml $title)</v8:content>" - $lines += "$i`t`t</v8:item>" - $lines += "$i`t</dcsset:lwsTitle>" - } - foreach ($item in $items) { - $lines += "$i`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" - $lines += "$i`t`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>" - $lines += "$i`t</dcsset:item>" - } - $lines += "$i`t<dcsset:placement>Auto</dcsset:placement>" - $lines += "$i</dcsset:item>" - } else { - $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" - $lines += "$i`t<dcsset:field>$(Esc-Xml $fieldName)</dcsset:field>" - $lines += "$i</dcsset:item>" - } - return $lines -join "`n" -} - -function Build-DataParamFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - - if ($parsed.use -eq $false) { - $lines += "$i`t<dcscor:use>false</dcscor:use>" - } - - $lines += "$i`t<dcscor:parameter>$(Esc-Xml $parsed.parameter)</dcscor:parameter>" - - if ($null -ne $parsed.value) { - if ($parsed.value -is [hashtable] -and $parsed.value.variant) { - $lines += "$i`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" - $lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>" - $lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - $lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - $lines += "$i`t</dcscor:value>" - } elseif (Test-EmptyValue $parsed.value) { - $lines += "$i`t<dcscor:value xsi:nil=`"true`"/>" - } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { - $lines += "$i`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { - $lines += "$i`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } else { - $lines += "$i`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } - } - - if ($parsed.viewMode) { - $lines += "$i`t<dcsset:viewMode>$(Esc-Xml $parsed.viewMode)</dcsset:viewMode>" - } - - if ($parsed.userSettingID) { - $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } - $lines += "$i`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" - } - - $lines += "$i</dcscor:item>" - return $lines -join "`n" -} - -function Build-OrderItemFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - if ($parsed.field -eq "Auto") { - $lines += "$i<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" - } else { - $lines += "$i<dcsset:item xsi:type=`"dcsset:OrderItemField`">" - $lines += "$i`t<dcsset:field>$(Esc-Xml $parsed.field)</dcsset:field>" - $lines += "$i`t<dcsset:orderType>$($parsed.direction)</dcsset:orderType>" - $lines += "$i</dcsset:item>" - } - return $lines -join "`n" -} - -function Build-DataSetLinkFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dataSetLink>" - $lines += "$i`t<sourceDataSet>$(Esc-Xml $parsed.source)</sourceDataSet>" - $lines += "$i`t<destinationDataSet>$(Esc-Xml $parsed.dest)</destinationDataSet>" - $lines += "$i`t<sourceExpression>$(Esc-Xml $parsed.sourceExpr)</sourceExpression>" - $lines += "$i`t<destinationExpression>$(Esc-Xml $parsed.destExpr)</destinationExpression>" - if ($parsed.parameter) { - $lines += "$i`t<parameter>$(Esc-Xml $parsed.parameter)</parameter>" - } - $lines += "$i</dataSetLink>" - return $lines -join "`n" -} - -function Build-DataSetQueryFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dataSet xsi:type=`"DataSetQuery`">" - $lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>" - $lines += "$i`t<dataSource>$(Esc-Xml $parsed.dataSource)</dataSource>" - $lines += "$i`t<query>$(Esc-Xml $parsed.query)</query>" - $lines += "$i</dataSet>" - return $lines -join "`n" -} - -function Build-VariantFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<settingsVariant>" - $lines += "$i`t<dcsset:name>$(Esc-Xml $parsed.name)</dcsset:name>" - $lines += (Build-MLTextXml -tag "dcsset:presentation" -text $parsed.presentation -indent "$i`t") - $lines += "$i`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">" - $lines += "$i`t`t<dcsset:selection>" - $lines += "$i`t`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" - $lines += "$i`t`t</dcsset:selection>" - $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" - $lines += "$i`t`t`t<dcsset:groupItems/>" - $lines += "$i`t`t`t<dcsset:order>" - $lines += "$i`t`t`t`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" - $lines += "$i`t`t`t</dcsset:order>" - $lines += "$i`t`t`t<dcsset:selection>" - $lines += "$i`t`t`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" - $lines += "$i`t`t`t</dcsset:selection>" - $lines += "$i`t`t</dcsset:item>" - $lines += "$i`t</dcsset:settings>" - $lines += "$i</settingsVariant>" - return $lines -join "`n" -} - -function Emit-FilterComparison { - param($f, [string]$indent) - $lines = @() - $lines += "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" - $lines += "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $f.field)</dcsset:left>" - $lines += "$indent`t<dcsset:comparisonType>$(Esc-Xml $f.op)</dcsset:comparisonType>" - if ($null -ne $f.value) { - $vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" } - $lines += "$indent`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($f.value)")</dcsset:right>" - } - $lines += "$indent</dcsset:item>" - return $lines -} - -function Build-ConditionalAppearanceItemFragment { - param($parsed, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dcsset:item>" - - # selection - if ($parsed.fields -and $parsed.fields.Count -gt 0) { - $lines += "$i`t<dcsset:selection>" - foreach ($fld in $parsed.fields) { - $lines += "$i`t`t<dcsset:item>" - $lines += "$i`t`t`t<dcsset:field>$(Esc-Xml $fld)</dcsset:field>" - $lines += "$i`t`t</dcsset:item>" - } - $lines += "$i`t</dcsset:selection>" - } else { - $lines += "$i`t<dcsset:selection/>" - } - - # filter - if ($parsed.filter) { - $lines += "$i`t<dcsset:filter>" - if ($parsed.filter -is [array]) { - # OrGroup - $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" - $lines += "$i`t`t`t<dcsset:groupType>OrGroup</dcsset:groupType>" - foreach ($f in $parsed.filter) { - $lines += Emit-FilterComparison $f "$i`t`t`t" - } - $lines += "$i`t`t</dcsset:item>" - } else { - $lines += Emit-FilterComparison $parsed.filter "$i`t`t" - } - $lines += "$i`t</dcsset:filter>" - } else { - $lines += "$i`t<dcsset:filter/>" - } - - # appearance - $lines += "$i`t<dcsset:appearance>" - - $val = $parsed.value - $lines += "$i`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - $lines += "$i`t`t`t<dcscor:parameter>$(Esc-Xml $parsed.param)</dcscor:parameter>" - - if ($val -match '^(web|style|win):') { - $lines += "$i`t`t`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $val)</dcscor:value>" - } elseif ($val -eq "true" -or $val -eq "false") { - $lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $val)</dcscor:value>" - } elseif ($parsed.param -eq "Формат" -or $parsed.param -eq "Текст" -or $parsed.param -eq "Заголовок") { - $lines += "$i`t`t`t<dcscor:value xsi:type=`"v8:LocalStringType`">" - $lines += "$i`t`t`t`t<v8:item>" - $lines += "$i`t`t`t`t`t<v8:lang>ru</v8:lang>" - $lines += "$i`t`t`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>" - $lines += "$i`t`t`t`t</v8:item>" - $lines += "$i`t`t`t</dcscor:value>" - } else { - $lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $val)</dcscor:value>" - } - - $lines += "$i`t`t</dcscor:item>" - $lines += "$i`t</dcsset:appearance>" - - $lines += "$i</dcsset:item>" - return $lines -join "`n" -} - -function Build-StructureItemFragment { - param($item, [string]$indent) - - $i = $indent - $lines = @() - $lines += "$i<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" - - # name - if ($item["name"]) { - $lines += "$i`t<dcsset:name>$(Esc-Xml $item["name"])</dcsset:name>" - } - - # groupItems - $groupBy = $item["groupBy"] - if (-not $groupBy -or $groupBy.Count -eq 0) { - $lines += "$i`t<dcsset:groupItems/>" - } else { - $lines += "$i`t<dcsset:groupItems>" - foreach ($field in $groupBy) { - $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" - $lines += "$i`t`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" - $lines += "$i`t`t`t<dcsset:groupType>Items</dcsset:groupType>" - $lines += "$i`t`t`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" - $lines += "$i`t`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" - $lines += "$i`t`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" - $lines += "$i`t`t</dcsset:item>" - } - $lines += "$i`t</dcsset:groupItems>" - } - - # order (Auto) - $lines += "$i`t<dcsset:order>" - $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" - $lines += "$i`t</dcsset:order>" - - # selection (Auto) - $lines += "$i`t<dcsset:selection>" - $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" - $lines += "$i`t</dcsset:selection>" - - # Recursive children - if ($item["children"]) { - foreach ($child in $item["children"]) { - $childXml = Build-StructureItemFragment -item $child -indent "$i`t" - $lines += $childXml - } - } - - $lines += "$i</dcsset:item>" - return $lines -join "`n" -} - -function Build-OutputParamFragment { - param($parsed, [string]$indent) - - $i = $indent - $key = $parsed.key - $val = $parsed.value - $ptype = $script:outputParamTypes[$key] - if (-not $ptype) { $ptype = "xs:string" } - - $lines = @() - $lines += "$i<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - $lines += "$i`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" - - if ($ptype -eq "mltext") { - $lines += "$i`t<dcscor:value xsi:type=`"v8:LocalStringType`">" - $lines += "$i`t`t<v8:item>" - $lines += "$i`t`t`t<v8:lang>ru</v8:lang>" - $lines += "$i`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>" - $lines += "$i`t`t</v8:item>" - $lines += "$i`t</dcscor:value>" - } else { - $lines += "$i`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml $val)</dcscor:value>" - } - - $lines += "$i</dcscor:item>" - return $lines -join "`n" -} - -# --- 5. XML helpers --- - -function Import-Fragment($doc, [string]$xmlString) { - $wrapper = @" -<_W xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:v8="http://v8.1c.ru/8.1/data/core" - xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" - xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" - xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" - xmlns:v8ui="http://v8.1c.ru/8.1/data/ui">$xmlString</_W> -"@ - $frag = New-Object System.Xml.XmlDocument - $frag.PreserveWhitespace = $true - $frag.LoadXml($wrapper) - $nodes = @() - foreach ($child in $frag.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $doc.ImportNode($child, $true) - } - } - return ,$nodes -} - -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - $text = $child.Value - if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($text -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - $depth = 0 - $current = $container - while ($current -and $current -ne $xmlDoc.DocumentElement) { - $depth++ - $current = $current.ParentNode - } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - # LF line endings — 1С DCS files use LF consistently; CRLF causes idempotency - # leaks when modify-* removes one whitespace and inserts a different-style one. - $ws = $xmlDoc.CreateWhitespace("`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $xmlDoc.CreateWhitespace("`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Clear-ContainerChildren($container) { - $toRemove = @() - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $toRemove += $child - } - } - foreach ($el in $toRemove) { - Remove-NodeWithWhitespace $el - } -} - -function Remove-NodeWithWhitespace($node) { - $parent = $node.ParentNode - $prev = $node.PreviousSibling - $next = $node.NextSibling - - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($prev) | Out-Null - } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($next) | Out-Null - } - $parent.RemoveChild($node) | Out-Null -} - -function Find-FirstElement($container, [string[]]$localNames, [string]$nsUri) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element') { - foreach ($name in $localNames) { - if ($child.LocalName -eq $name) { - if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { - return $child - } - } - } - } - } - return $null -} - -function Find-LastElement($container, [string]$localName, [string]$nsUri) { - $last = $null - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { - if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { - $last = $child - } - } - } - return $last -} - -function Find-ElementByChildValue($container, [string]$elemName, [string]$childName, [string]$childValue, [string]$nsUri) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -ne $elemName) { continue } - if ($nsUri -and $child.NamespaceURI -ne $nsUri) { continue } - - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $childName -and $gc.InnerText.Trim() -eq $childValue) { - return $child - } - } - } - return $null -} - -function Set-OrCreateChildElement($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$indent) { - $existing = $null - foreach ($ch in $parent.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { - $existing = $ch - break - } - } - if ($existing) { - $existing.InnerText = $value - } else { - $prefix = $parent.GetPrefixOfNamespace($nsUri) - $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } - $fragXml = "$indent<$qualName>$(Esc-Xml $value)</$qualName>" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $parent $node $null $indent - } - } -} - -function Set-OrCreateChildElementWithAttr($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$xsiType, [string]$indent) { - $existing = $null - foreach ($ch in $parent.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { - $existing = $ch - break - } - } - if ($existing) { - $existing.InnerText = $value - if ($xsiType) { - $existing.SetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance", $xsiType) | Out-Null - } - } else { - $prefix = $parent.GetPrefixOfNamespace($nsUri) - $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } - $typeAttr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } - $fragXml = "$indent<$qualName$typeAttr>$(Esc-Xml $value)</$qualName>" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $parent $node $null $indent - } - } -} - -function Get-AllDataSets { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - $root = $xmlDoc.DocumentElement - $result = @() - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { - $result += $child - } - } - return ,$result -} - -function Normalize-LineEndings([string]$s) { - if ($null -eq $s) { return $s } - return $s.Replace("`r`n", "`n").Replace("`r", "`n") -} - -function Escape-Whitespace([string]$s) { - $sb = New-Object System.Text.StringBuilder - foreach ($c in $s.ToCharArray()) { - $code = [int]$c - if ($c -eq "`n") { [void]$sb.Append('\n') } - elseif ($c -eq "`r") { [void]$sb.Append('\r') } - elseif ($c -eq "`t") { [void]$sb.Append('\t') } - elseif ($code -lt 32 -or $code -eq 0xA0 -or ($code -ge 0x2000 -and $code -le 0x200F) -or $code -eq 0xFEFF) { - [void]$sb.AppendFormat('\u{0:X4}', $code) - } else { - [void]$sb.Append($c) - } - } - return $sb.ToString() -} - -function Collapse-Whitespace([string]$s) { - return ([regex]::Replace($s, "[\s ]+", " ")).Trim() -} - -function Find-LongestPrefixMatch([string]$haystack, [string]$needle) { - # Binary search: largest L such that needle.Substring(0, L) is a substring of haystack. - # Monotonic — if length L matches at position P, then length L-1 (prefix) also matches at P. - if ($needle.Length -eq 0 -or $haystack.Length -eq 0) { - return @{ Length = 0; Offset = -1 } - } - if ($haystack.IndexOf([string]$needle[0]) -lt 0) { - return @{ Length = 0; Offset = -1 } - } - $lo = 1; $hi = $needle.Length - $bestLen = 1; $bestOffset = $haystack.IndexOf([string]$needle[0]) - while ($lo -le $hi) { - $mid = [int](($lo + $hi) / 2) - $idx = $haystack.IndexOf($needle.Substring(0, $mid)) - if ($idx -ge 0) { $bestLen = $mid; $bestOffset = $idx; $lo = $mid + 1 } - else { $hi = $mid - 1 } - } - return @{ Length = $bestLen; Offset = $bestOffset } -} - -function Format-PatchQueryNotFound([string]$oldStr, [string]$queryText, $currentDsNode, [string]$dsName) { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - $lines = @("Substring not found in query of dataset '$dsName'.") - - # Step 1 — cross-dataset probe - foreach ($ds in (Get-AllDataSets)) { - if ($ds -eq $currentDsNode) { continue } - $q = Find-FirstElement $ds @("query") $schNs - if (-not $q) { continue } - $qt = Normalize-LineEndings $q.InnerText - if ($qt.Contains($oldStr)) { - $otherName = Get-DataSetName $ds - $lines += "Found in dataset '$otherName' instead — wrong -DataSet?" - return ($lines -join "`n") - } - } - - # Step 2 — tolerant probe (whitespace + NBSP collapsed) - $normNeedle = Collapse-Whitespace $oldStr - $normHay = Collapse-Whitespace $queryText - $tolerant = ($normNeedle.Length -gt 0 -and $normHay.Contains($normNeedle)) - - # Step 3 — prefix divergence (used by both Step 2 reporting and standalone Step 3) - $prefix = Find-LongestPrefixMatch -haystack $queryText -needle $oldStr - $divergence = $null - if ($prefix.Length -gt 0 -and $prefix.Length -lt $oldStr.Length) { - $queryPos = $prefix.Offset + $prefix.Length - $searchChar = $oldStr[$prefix.Length] - $beforeLen = [Math]::Min(20, $prefix.Length) - $before = $oldStr.Substring($prefix.Length - $beforeLen, $beforeLen) - $divergence = [ordered]@{ - matched = $prefix.Length - total = $oldStr.Length - before = $before - searchChar = $searchChar - queryChar = $(if ($queryPos -lt $queryText.Length) { $queryText[$queryPos] } else { $null }) - } - } - - if ($tolerant) { - $lines += "Not found exactly, but would match with whitespace normalized (tabs/spaces/NBSP)." - if ($divergence) { - $lines += "Diverged at offset $($divergence.matched) of $($divergence.total):" - $lines += " before: '$(Escape-Whitespace $divergence.before)'" - $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" - if ($null -ne $divergence.queryChar) { - $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" - } - } - return ($lines -join "`n") - } - - # Step 3 standalone - if ($prefix.Length -eq 0) { - $lines += "No common prefix with query. Check -DataSet (current: '$dsName')." - return ($lines -join "`n") - } - $lines += "Matched first $($divergence.matched) of $($divergence.total) chars, then diverged:" - $lines += " before: '$(Escape-Whitespace $divergence.before)'" - $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" - if ($null -ne $divergence.queryChar) { - $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" - } else { - $lines += " in query: (end of query)" - } - return ($lines -join "`n") -} - -function Resolve-DataSet { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - $root = $xmlDoc.DocumentElement - - if ($DataSet) { - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { - $nameEl = $null - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { - $nameEl = $gc - break - } - } - if ($nameEl -and $nameEl.InnerText -eq $DataSet) { - return $child - } - } - } - Write-Error "DataSet '$DataSet' not found" - exit 1 - } - - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { - return $child - } - } - Write-Error "No dataSet found in DCS" - exit 1 -} - -function Resolve-VariantSettings { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" - $root = $xmlDoc.DocumentElement - - $sv = $null - if ($Variant) { - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { - $nameEl = $null - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { - $nameEl = $gc - break - } - } - if ($nameEl -and $nameEl.InnerText -eq $Variant) { - $sv = $child - break - } - } - } - if (-not $sv) { - Write-Error "Variant '$Variant' not found" - exit 1 - } - } else { - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { - $sv = $child - break - } - } - if (-not $sv) { - Write-Error "No settingsVariant found in DCS" - exit 1 - } - } - - foreach ($gc in $sv.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) { - return $gc - } - } - - Write-Error "No <dcsset:settings> found in variant" - exit 1 -} - -function Ensure-SettingsChild($settings, [string]$childName, [string[]]$afterSiblings) { - $el = Find-FirstElement $settings @($childName) $setNs - if ($el) { return $el } - - $indent = Get-ChildIndent $settings - $fragXml = "$indent<dcsset:$childName/>" - $nodes = Import-Fragment $xmlDoc $fragXml - - $refNode = $null - foreach ($sibName in $afterSiblings) { - $sib = Find-FirstElement $settings @($sibName) $setNs - if ($sib) { - $refNode = $sib.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - break - } - } - - foreach ($node in $nodes) { - Insert-BeforeElement $settings $node $refNode $indent - } - - return Find-FirstElement $settings @($childName) $setNs -} - -function Get-VariantName { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" - $root = $xmlDoc.DocumentElement - - if ($Variant) { return $Variant } - - foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { - foreach ($gc in $child.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { - return $gc.InnerText - } - } - } - } - return "(unknown)" -} - -function Get-DataSetName($dsNode) { - $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" - foreach ($gc in $dsNode.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { - return $gc.InnerText - } - } - return "(unknown)" -} - -function Get-ContainerChildIndent($container) { - $hasElements = $false - foreach ($ch in $container.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } - } - if ($hasElements) { - return Get-ChildIndent $container - } else { - $parentIndent = Get-ChildIndent $container.ParentNode - return $parentIndent + "`t" - } -} - -# --- 6. Load XML --- - -# Capture raw original BEFORE DOM parse — needed at save time to: -# (a) restore exact root <DataCompositionSchema xmlns=...> opening tag (DOM serializer -# collapses multi-line xmlns into a single line); -# (b) detect NO-OP via byte-equality as an extra safety net. -$script:RawOriginal = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) -$rootOpenMatch = [regex]::Match($script:RawOriginal, '<DataCompositionSchema\b[^>]*>') -if ($rootOpenMatch.Success) { $script:RawRootOpening = $rootOpenMatch.Value } else { $script:RawRootOpening = $null } - -# Detect line ending convention so save can normalize back to whatever the source used. -# 1С Designer writes CRLF on Windows; LF-edited files should stay LF. -$script:LineEnding = if ($script:RawOriginal.Contains("`r`n")) { "`r`n" } else { "`n" } - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $true -$xmlDoc.Load($resolvedPath) - -$schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" -$setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" -$corNs = "http://v8.1c.ru/8.1/data-composition-system/core" - -# --- 7. Batch value splitting --- - -if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "modify-structure" -or $Operation -eq "add-dataSet") { - $values = @($Value) -} elseif ($Operation -eq "patch-query") { - $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -} elseif ($Operation -eq "add-drilldown") { - if ($Value.Contains(';;')) { - $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } else { - $values = @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - } -} else { - $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -} - -# --- 8. Main logic --- - -switch ($Operation) { - "add-field" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - foreach ($val in $values) { - $parsed = Parse-FieldShorthand $val - $childIndent = Get-ChildIndent $dsNode - - # Duplicate check - $existing = Find-ElementByChildValue $dsNode "field" "dataPath" $parsed.dataPath $schNs - if ($existing) { - Write-Host "[WARN] Field `"$($parsed.dataPath)`" already exists in dataset `"$dsName`" — skipped" - continue - } - - $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - $refNode = Find-FirstElement $dsNode @("dataSource") $schNs - foreach ($node in $nodes) { - Insert-BeforeElement $dsNode $node $refNode $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" - - if (-not $NoSelection) { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Ensure-SettingsChild $settings "selection" @() - $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs - if ($existingSel) { - Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" - } else { - $selIndent = Get-ContainerChildIndent $selection - $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent - $selNodes = Import-Fragment $xmlDoc $selXml - foreach ($node in $selNodes) { - Insert-BeforeElement $selection $node $null $selIndent - } - $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" - } - } - } - } - - "add-total" { - foreach ($val in $values) { - $parsed = Parse-TotalShorthand $val - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - - # Duplicate check - $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs - if ($existing) { - Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped" - continue - } - - $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - $root = $xmlDoc.DocumentElement - $lastTotal = Find-LastElement $root "totalField" $schNs - if ($lastTotal) { - $refNode = $lastTotal.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs - } - - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" - } - } - - "add-calculated-field" { - foreach ($val in $values) { - $parsed = Parse-CalcShorthand $val - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - - # Duplicate check - $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs - if ($existing) { - Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped" - continue - } - - $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - $root = $xmlDoc.DocumentElement - $lastCalc = Find-LastElement $root "calculatedField" $schNs - if ($lastCalc) { - $refNode = $lastCalc.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs - } - - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" - - if (-not $NoSelection) { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Ensure-SettingsChild $settings "selection" @() - $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs - if ($existingSel) { - Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" - } else { - $selIndent = Get-ContainerChildIndent $selection - $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent - $selNodes = Import-Fragment $xmlDoc $selXml - foreach ($node in $selNodes) { - Insert-BeforeElement $selection $node $null $selIndent - } - $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" - } - } - } - } - - "add-parameter" { - foreach ($val in $values) { - $parsed = Parse-ParamShorthand $val - $childIndent = Get-ChildIndent $xmlDoc.DocumentElement - - # Duplicate check - $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs - if ($existing) { - Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped" - continue - } - - $fragments = Build-ParamFragment -parsed $parsed -indent $childIndent - - $root = $xmlDoc.DocumentElement - $lastParam = Find-LastElement $root "parameter" $schNs - if ($lastParam) { - $refNode = $lastParam.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = Find-FirstElement $root @("template","groupTemplate","settingsVariant") $schNs - } - - foreach ($fragXml in $fragments) { - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - } - - $script:Dirty = $true; Write-Host "[OK] Parameter `"$($parsed.name)`" added" - if ($parsed.autoDates) { - $script:Dirty = $true; Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" - } - } - } - - "modify-parameter" { - foreach ($val in $values) { - # Parse: "ParamName [Title] key=value key=value" - # Extract optional [Title] first (mirrors Parse-FieldShorthand) - $titleVal = $null - if ($val -match '\[([^\]]*)\]') { - $titleVal = $Matches[1].Trim() - $val = ($val -replace '\s*\[[^\]]*\]\s*', ' ').Trim() - } - - $parts = $val -split '\s+', 2 - $paramName = $parts[0].Trim() - $rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" } - - # Extract @hidden / @always flags - $flagHidden = $false - $flagAlways = $false - if ($rest -match '@hidden\b') { $flagHidden = $true; $rest = ($rest -replace '\s*@hidden\b', '').Trim() } - if ($rest -match '@always\b') { $flagAlways = $true; $rest = ($rest -replace '\s*@always\b', '').Trim() } - - # Find parameter element - $paramEl = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $paramName $schNs - if (-not $paramEl) { - Write-Host "[WARN] Parameter `"$paramName`" not found — skipped" - continue - } - - $childIndent = Get-ChildIndent $paramEl - - # Set/replace title (must come right after <name>, before <valueType>) - if ($null -ne $titleVal) { - $existingTitle = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'title') { - $existingTitle = $ch; break - } - } - # If the existing title has multiple <v8:item> (multi-language: ru + en + …), - # patch only the ru <v8:content> via raw-string surgery to preserve other langs. - # Otherwise rebuild as ru-only fragment. - $titleFrag = $null - if ($existingTitle) { - $rawTitle = $existingTitle.OuterXml - $rawTitle = [regex]::Replace($rawTitle, ' xmlns(?::\w+)?="[^"]*"', '') - # Count <v8:item> occurrences — if >1, treat as multi-lang. - $itemCount = ([regex]::Matches($rawTitle, '<v8:item>')).Count - if ($itemCount -gt 1) { - $titleFrag = $childIndent + (Patch-MLTextRu $rawTitle $titleVal $childIndent) - } - Remove-NodeWithWhitespace $existingTitle - } - if (-not $titleFrag) { - $titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent - } - # Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...) - $titleRef = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -ne 'name') { - $titleRef = $ch; break - } - } - $titleNodes = Import-Fragment $xmlDoc $titleFrag - foreach ($node in $titleNodes) { - Insert-BeforeElement $paramEl $node $titleRef $childIndent - } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`"" - } - - # Separate availableValue=... from simple kv pairs - $simpleRest = $rest - $avPart = $null - $avIdx = $rest.IndexOf('availableValue=') - if ($avIdx -ge 0) { - $simpleRest = $rest.Substring(0, $avIdx).Trim() - $avPart = $rest.Substring($avIdx) - } - - # Separate a multi-value value=... (list) — kv-regex below grabs only a single - # \S+ token, so a comma-separated list (with spaces) wouldn't be captured. - # availableValue already peeled, so 'value=' here is the real value key. - $valueListItems = $null - $vlIdx = $simpleRest.IndexOf('value=') - if ($vlIdx -ge 0) { - $vlRhs = $simpleRest.Substring($vlIdx + 'value='.Length) - $cand = Parse-ValueList $vlRhs - if ($cand.Count -ge 2) { - $valueListItems = $cand - $simpleRest = $simpleRest.Substring(0, $vlIdx).Trim() - } - } - - # Process simple key=value pairs (use, denyIncompleteValues, value, etc.) - if ($simpleRest) { - $kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)') - foreach ($kv in $kvPairs) { - $key = $kv.Groups[1].Value - $value = $kv.Groups[2].Value - - # Namespace-aware lookup (children live in $schNs) - $existing = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $key -and $ch.NamespaceURI -eq $schNs) { - $existing = $ch; break - } - } - - if ($key -eq "value") { - # Special-case: rebuild <value> with correct xsi:type from <valueType> - $declaredType = "" - $vtEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } - } - if ($vtEl) { - foreach ($tnode in $vtEl.ChildNodes) { - if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { - $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' - break - } - } - } - # Detect valueListAllowed flag on the parameter — empty value should be omitted - $vlaSet = $false - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { - if ($ch.InnerText.Trim() -eq 'true') { $vlaSet = $true } - break - } - } - if (Test-EmptyValue $value) { - $fragXml = Build-EmptyValueXml -type $declaredType -indent $childIndent -tagPrefix "" -tagName "value" -valueListAllowed $vlaSet - } else { - $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent - $fragXml = $valueLines -join "`n" - } - - # Collect ALL existing <value> (a param may carry a value-list) — scalar - # value= collapses them to one, so remove every <value>, not just the first. - $allValueEls = @() - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $schNs) { $allValueEls += $ch } - } - $wasExisting = ($allValueEls.Count -gt 0) - if ($wasExisting) { - # Capture position after the last existing value, then remove all - $refNode = $allValueEls[$allValueEls.Count - 1].NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - foreach ($ve in $allValueEls) { Remove-NodeWithWhitespace $ve } - } else { - # Insert before useRestriction/availableValue/denyIncompleteValues/use - $refNode = $null - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { - $refNode = $child; break - } - } - } - if ($fragXml) { - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $paramEl $node $refNode $childIndent - } - } - $verb = if ($wasExisting) { "updated" } else { "added" } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value $verb to $value" - } elseif ($existing) { - $existing.InnerText = $value - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key updated to $value" - } else { - # Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use - $refNode = $null - if ($key -eq "denyIncompleteValues") { - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'use') { - $refNode = $child; break - } - } - } - $fragXml = "$childIndent<$key>$(Esc-Xml $value)</$key>" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $paramEl $node $refNode $childIndent - } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key=$value added" - } - } - } - - # Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true - if ($valueListItems) { - # Declared type from <valueType> - $declaredType = "" - $vtEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } - } - if ($vtEl) { - foreach ($tnode in $vtEl.ChildNodes) { - if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { - $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' - break - } - } - } - # Remove ALL existing <value>; capture insertion ref after the last one - $valueEls = @() - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'value' -and $child.NamespaceURI -eq $schNs) { $valueEls += $child } - } - $refNode = $null - if ($valueEls.Count -gt 0) { - $refNode = $valueEls[$valueEls.Count - 1].NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } - foreach ($ve in $valueEls) { Remove-NodeWithWhitespace $ve } - } else { - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } - } - } - foreach ($v in $valueListItems) { - $fragXml = (Build-ParamValueXml -type $declaredType -value $v -indent $childIndent) -join "`n" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } - } - # Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use) - $vlaEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { $vlaEl = $ch; break } - } - if ($vlaEl) { - if ($vlaEl.InnerText.Trim() -ne 'true') { $vlaEl.InnerText = 'true' } - } else { - $refVla = $null - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refVla = $child; break } - } - $nodes = Import-Fragment $xmlDoc "$childIndent<valueListAllowed>true</valueListAllowed>" - foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refVla $childIndent } - } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value set to list of $($valueListItems.Count) item(s)" - } - - # Process availableValue — replace whole list with new items - if ($avPart) { - $avRest = ($avPart -replace '^availableValue=', '').Trim() - $avItems = Parse-AvailableValueList $avRest - - # Detect value type: prefer declared <valueType> of the parameter, else guess from value - $declaredType = "" - $vtEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } - } - if ($vtEl) { - foreach ($tnode in $vtEl.ChildNodes) { - if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { - $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' - break - } - } - } - - # Remove all existing <availableValue> elements - $toRemove = @() - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableValue' -and $ch.NamespaceURI -eq $schNs) { - $toRemove += $ch - } - } - foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } - - # Insert each new <availableValue> before (denyIncompleteValues, use) - $refNode = $null - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and ($child.LocalName -eq 'denyIncompleteValues' -or $child.LocalName -eq 'use')) { - $refNode = $child; break - } - } - foreach ($av in $avItems) { - $avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent - $fragXml = $avLines -join "`n" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $paramEl $node $refNode $childIndent - } - } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)" - } - - # Process @hidden / @always flags (idempotent) - if ($flagHidden) { - # useRestriction → true (insert after <value>, before <expression>/<availableAsField>/...) - $urEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'useRestriction' -and $ch.NamespaceURI -eq $schNs) { $urEl = $ch; break } - } - if ($urEl) { - if ($urEl.InnerText.Trim() -ne 'true') { $urEl.InnerText = 'true' } - } else { - $refNode = $null - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('expression','availableAsField','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } - } - $nodes = Import-Fragment $xmlDoc "$childIndent<useRestriction>true</useRestriction>" - foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } - } - - # availableAsField → false (insert after <expression>, before <availableValue>/<denyIncompleteValues>/<use>) - $afEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableAsField' -and $ch.NamespaceURI -eq $schNs) { $afEl = $ch; break } - } - if ($afEl) { - if ($afEl.InnerText.Trim() -ne 'false') { $afEl.InnerText = 'false' } - } else { - $refNode = $null - foreach ($child in $paramEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refNode = $child; break } - } - $nodes = Import-Fragment $xmlDoc "$childIndent<availableAsField>false</availableAsField>" - foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } - } - - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @hidden applied" - } - - if ($flagAlways) { - $useEl = $null - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $schNs) { $useEl = $ch; break } - } - if ($useEl) { - if ($useEl.InnerText.Trim() -ne 'Always') { $useEl.InnerText = 'Always' } - } else { - $nodes = Import-Fragment $xmlDoc "$childIndent<use>Always</use>" - foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $null $childIndent } - } - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @always applied" - } - } - } - - "rename-parameter" { - foreach ($val in $values) { - # Shorthand: "OldName => NewName" - if ($val -notmatch '^\s*(.+?)\s*=>\s*(.+?)\s*$') { - Write-Host "[WARN] rename-parameter expects 'OldName => NewName', got: $val" - continue - } - $oldName = $Matches[1].Trim() - $newName = $Matches[2].Trim() - - if ($oldName -eq $newName) { - Write-Host "[WARN] rename-parameter: old and new names are equal — skipped" - continue - } - - # 1. Rename <parameter><name>OldName</name> - $root = $xmlDoc.DocumentElement - $paramEl = Find-ElementByChildValue $root "parameter" "name" $oldName $schNs - if (-not $paramEl) { - Write-Host "[WARN] Parameter `"$oldName`" not found — skipped" - continue - } - foreach ($ch in $paramEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $schNs) { - $ch.InnerText = $newName - break - } - } - - # 2. Update <expression> in other <parameter> elements. - # Regex matches "&OldName" only when followed by a non-identifier char (or end), - # so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета". - $escOld = [regex]::Escape($oldName) - $exprRegex = "&$escOld(?=[^\w\u0400-\u04FF]|$)" - $exprUpdated = 0 - foreach ($ch in $root.ChildNodes) { - if ($ch.NodeType -ne 'Element' -or $ch.LocalName -ne 'parameter' -or $ch.NamespaceURI -ne $schNs) { continue } - foreach ($gc in $ch.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'expression' -and $gc.NamespaceURI -eq $schNs) { - $oldExpr = $gc.InnerText - $newExpr = [regex]::Replace($oldExpr, $exprRegex, "&$newName") - if ($newExpr -ne $oldExpr) { - $gc.InnerText = $newExpr - $exprUpdated++ - } - } - } - } - - # 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants. - # Note: <settingsVariant> is in schNs, but <settings> and <dataParameters> are in setNs. - # IMPORTANT: don't use $variant — it collides with script parameter [string]$Variant - # (PowerShell vars are case-insensitive, and the [string] type would coerce XmlNode to ""). - $dpUpdated = 0 - foreach ($variantNode in $root.ChildNodes) { - if ($variantNode.NodeType -ne 'Element' -or $variantNode.LocalName -ne 'settingsVariant' -or $variantNode.NamespaceURI -ne $schNs) { continue } - $settings = Find-FirstElement $variantNode @("settings") $setNs - if (-not $settings) { continue } - $dpEl = Find-FirstElement $settings @("dataParameters") $setNs - if (-not $dpEl) { continue } - foreach ($item in $dpEl.ChildNodes) { - if ($item.NodeType -ne 'Element' -or $item.LocalName -ne 'item') { continue } - foreach ($gc in $item.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'parameter' -and $gc.NamespaceURI -eq $corNs) { - if ($gc.InnerText.Trim() -eq $oldName) { - $gc.InnerText = $newName - $dpUpdated++ - } - } - } - } - } - - $script:Dirty = $true; Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)" - } - } - - "reorder-parameters" { - foreach ($val in $values) { - # Shorthand: "Name1, Name2, Name3" — partial list, listed names go first in order, rest preserve original order - $order = @($val -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - if ($order.Count -eq 0) { - Write-Host "[WARN] reorder-parameters: empty list — skipped" - continue - } - - $root = $xmlDoc.DocumentElement - - # Collect all <parameter> in document order with their child indent - $allParams = @() - foreach ($ch in $root.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'parameter' -and $ch.NamespaceURI -eq $schNs) { - $allParams += $ch - } - } - if ($allParams.Count -eq 0) { - Write-Host "[WARN] reorder-parameters: no parameters in schema" - continue - } - - $childIndent = Get-ChildIndent $root - - # Build name -> element map - $byName = @{} - foreach ($pe in $allParams) { - foreach ($gc in $pe.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { - $byName[$gc.InnerText.Trim()] = $pe - break - } - } - } - - # Build new order - $newOrder = @() - $used = @{} - foreach ($name in $order) { - if ($byName.ContainsKey($name)) { - $newOrder += $byName[$name] - $used[$name] = $true - } else { - Write-Host "[WARN] reorder-parameters: parameter `"$name`" not found — skipped" - } - } - foreach ($pe in $allParams) { - $peName = $null - foreach ($gc in $pe.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { - $peName = $gc.InnerText.Trim(); break - } - } - if ($peName -and -not $used.ContainsKey($peName)) { - $newOrder += $pe - } - } - - # Find anchor: element right after the last parameter in original order - $lastParam = $allParams[-1] - $anchor = $lastParam.NextSibling - - # Remove all parameters with surrounding whitespace - foreach ($pe in $allParams) { - Remove-NodeWithWhitespace $pe - } - - # Re-insert in new order before anchor - foreach ($pe in $newOrder) { - Insert-BeforeElement $root $pe $anchor $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)" - } - } - - "add-filter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-FilterShorthand $val - - $filterEl = Ensure-SettingsChild $settings "filter" @("selection") - $filterIndent = Get-ContainerChildIndent $filterEl - - $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $filterEl $node $null $filterIndent - } - - $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`"" - } - } - - "add-dataParameter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-DataParamShorthand $val - - $dpEl = Ensure-SettingsChild $settings "dataParameters" @("outputParameters","conditionalAppearance","order","filter","selection") - $dpIndent = Get-ContainerChildIndent $dpEl - - $fragXml = Build-DataParamFragment -parsed $parsed -indent $dpIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $dpEl $node $null $dpIndent - } - - $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`"" - } - } - - "add-order" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-OrderShorthand $val - - $orderEl = Ensure-SettingsChild $settings "order" @("filter","selection") - $orderIndent = Get-ContainerChildIndent $orderEl - - # Duplicate check - if ($parsed.field -eq "Auto") { - $isDup = $false - foreach ($ch in $orderEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { - $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($typeAttr -and $typeAttr.Contains("OrderItemAuto")) { $isDup = $true; break } - } - } - if ($isDup) { - Write-Host "[WARN] OrderItemAuto already exists in variant `"$varName`" — skipped" - continue - } - } else { - $existingOrd = Find-ElementByChildValue $orderEl "item" "field" $parsed.field $setNs - if ($existingOrd) { - Write-Host "[WARN] Order `"$($parsed.field)`" already exists in variant `"$varName`" — skipped" - continue - } - } - - $fragXml = Build-OrderItemFragment -parsed $parsed -indent $orderIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $orderEl $node $null $orderIndent - } - - $desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" } - $script:Dirty = $true; Write-Host "[OK] Order `"$desc`" added to variant `"$varName`"" - } - } - - "add-selection" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $fieldName = $val.Trim() - $groupName = $null - - # Extract @group=Name - if ($fieldName -match '\s*@group=(\S+)') { - $groupName = $Matches[1] - $fieldName = ($fieldName -replace '\s*@group=\S+', '').Trim() - } - - if ($groupName) { - # Find named StructureItemGroup - $dcssetNs = "http://v8.1c.ru/8.1/data-composition-system/settings" - $xsiNs = "http://www.w3.org/2001/XMLSchema-instance" - $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $nsMgr.AddNamespace("dcsset", $dcssetNs) - $nsMgr.AddNamespace("xsi", $xsiNs) - $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$groupName']", $nsMgr) - if (-not $groupEl) { - Write-Host "[WARN] StructureItemGroup `"$groupName`" not found — adding to variant level" - $targetEl = $settings - } else { - $targetEl = $groupEl - } - } else { - $targetEl = $settings - } - - $selection = Ensure-SettingsChild $targetEl "selection" @() - - # Dedup: skip if SelectedItemAuto already exists - if ($fieldName -eq "Auto") { - $isDup = $false - foreach ($ch in $selection.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { - $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($typeAttr -and $typeAttr.Contains("SelectedItemAuto")) { $isDup = $true; break } - } - } - if ($isDup) { - $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } - Write-Host "[WARN] SelectedItemAuto already exists in $target — skipped" - continue - } - } - - $selIndent = Get-ContainerChildIndent $selection - - $selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent - $selNodes = Import-Fragment $xmlDoc $selXml - foreach ($node in $selNodes) { - Insert-BeforeElement $selection $node $null $selIndent - } - - $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } - $script:Dirty = $true; Write-Host "[OK] Selection `"$fieldName`" added to $target" - } - } - - "set-query" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - $queryEl = Find-FirstElement $dsNode @("query") $schNs - if (-not $queryEl) { - Write-Error "No <query> element found in dataset '$dsName'" - exit 1 - } - - # InnerText setter handles XML escaping automatically - $queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir - - $script:Dirty = $true; Write-Host "[OK] Query replaced in dataset `"$dsName`"" - } - - "patch-query" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - $queryEl = Find-FirstElement $dsNode @("query") $schNs - if (-not $queryEl) { - Write-Error "No <query> element found in dataset '$dsName'" - exit 1 - } - - foreach ($val in $values) { - $once = $false - if ($val -match '@once\b') { - $once = $true - $val = ($val -replace '\s*@once\b', '').Trim() - } - - $sepIdx = $val.IndexOf(" => ") - if ($sepIdx -lt 0) { - Write-Error "patch-query value must contain ' => ' separator: old => new" - exit 1 - } - $oldStr = Normalize-LineEndings $val.Substring(0, $sepIdx) - $newStr = Normalize-LineEndings $val.Substring($sepIdx + 4) - $queryText = Normalize-LineEndings $queryEl.InnerText - - $count = ([regex]::Matches($queryText, [regex]::Escape($oldStr))).Count - if ($count -eq 0) { - $diag = Format-PatchQueryNotFound $oldStr $queryText $dsNode $dsName - Write-Error $diag - exit 1 - } - if ($once -and $count -ne 1) { - Write-Error "@once: expected 1 occurrence of '$oldStr' in dataset '$dsName', found $count" - exit 1 - } - - $queryEl.InnerText = $queryText.Replace($oldStr, $newStr) - $suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" } - $script:Dirty = $true; Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix" - } - } - - "set-outputParameter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-OutputParamShorthand $val - - $outputEl = Ensure-SettingsChild $settings "outputParameters" @("conditionalAppearance","order","filter","selection") - $outputIndent = Get-ContainerChildIndent $outputEl - - # Remove existing parameter with same key if present - $existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs - if ($existingParam) { - Remove-NodeWithWhitespace $existingParam - $script:Dirty = $true; Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`"" - } else { - $script:Dirty = $true; Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`"" - } - - $fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $outputEl $node $null $outputIndent - } - } - } - - "set-structure" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - # Remove all existing structure items (dcsset:item elements) - $toRemove = @() - foreach ($ch in $settings.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item' -and $ch.NamespaceURI -eq $setNs) { - $toRemove += $ch - } - } - foreach ($el in $toRemove) { - Remove-NodeWithWhitespace $el - } - - # Parse structure shorthand - $structItems = Parse-StructureShorthand $Value - $settingsIndent = Get-ChildIndent $settings - - # Find insertion point — before outputParameters/dataParameters/conditionalAppearance/order/filter/selection or at end - $refNode = Find-FirstElement $settings @("outputParameters","dataParameters","conditionalAppearance","order","filter","selection","item") $setNs - if (-not $refNode) { $refNode = $null } - - foreach ($structItem in $structItems) { - $fragXml = Build-StructureItemFragment -item $structItem -indent $settingsIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $settings $node $refNode $settingsIndent - } - } - - $script:Dirty = $true; Write-Host "[OK] Structure set in variant `"$varName`": $Value" - } - - "modify-structure" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - $structItems = Parse-StructureShorthand $Value - - # Flatten parsed tree into (name, groupBy) targets - $targets = @() - $stack = New-Object System.Collections.Stack - foreach ($it in $structItems) { $stack.Push($it) } - while ($stack.Count -gt 0) { - $it = $stack.Pop() - if ($it["name"]) { - $targets += @{ name = $it["name"]; groupBy = $it["groupBy"] } - } - if ($it["children"]) { - foreach ($ch in $it["children"]) { $stack.Push($ch) } - } - } - - if ($targets.Count -eq 0) { - Write-Error "modify-structure requires @name= for at least one group: $Value" - exit 1 - } - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $nsMgr.AddNamespace("dcsset", $setNs) - $nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - - foreach ($t in $targets) { - $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$($t.name)']", $nsMgr) - if (-not $groupEl) { - Write-Host "[WARN] Group with @name=`"$($t.name)`" not found — skipped" - continue - } - - $giEl = $null - foreach ($ch in $groupEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { - $giEl = $ch; break - } - } - $groupIndent = Get-ChildIndent $groupEl - if (-not $giEl) { - # Create <groupItems> after <name>, before <order>/<selection>/... - $nameEl = $null - $refAfterName = $null - foreach ($ch in $groupEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $setNs) { - $nameEl = $ch - } elseif ($ch.NodeType -eq 'Element' -and $nameEl -and -not $refAfterName) { - $refAfterName = $ch; break - } - } - $giFrag = "$groupIndent<dcsset:groupItems></dcsset:groupItems>" - $nodes = Import-Fragment $xmlDoc $giFrag - foreach ($node in $nodes) { - Insert-BeforeElement $groupEl $node $refAfterName $groupIndent - } - # Re-find - foreach ($ch in $groupEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { - $giEl = $ch; break - } - } - } - - $toRemove = @() - foreach ($ch in $giEl.ChildNodes) { - if ($ch.NodeType -eq 'Element') { $toRemove += $ch } - } - foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } - - $itemIndent = "$groupIndent`t" - - foreach ($field in $t.groupBy) { - $lines = @() - $lines += "$itemIndent<dcsset:item xsi:type=`"dcsset:GroupItemField`">" - $lines += "$itemIndent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" - $lines += "$itemIndent`t<dcsset:groupType>Items</dcsset:groupType>" - $lines += "$itemIndent`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" - $lines += "$itemIndent`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" - $lines += "$itemIndent`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" - $lines += "$itemIndent</dcsset:item>" - $fragXml = $lines -join "`n" - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $giEl $node $null $itemIndent - } - } - - $desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' } - $script:Dirty = $true; Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc" - } - } - - "add-dataSetLink" { - foreach ($val in $values) { - $parsed = Parse-DataSetLinkShorthand $val - $root = $xmlDoc.DocumentElement - $childIndent = Get-ChildIndent $root - - $fragXml = Build-DataSetLinkFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - # Insert after last dataSetLink, or before calculatedField/totalField/parameter/... - $lastLink = Find-LastElement $root "dataSetLink" $schNs - if ($lastLink) { - $refNode = $lastLink.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = Find-FirstElement $root @("calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs - } - - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - - $desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)" - if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" } - $script:Dirty = $true; Write-Host "[OK] DataSetLink `"$desc`" added" - } - } - - "add-dataSet" { - $root = $xmlDoc.DocumentElement - $childIndent = Get-ChildIndent $root - - $parsed = Parse-DataSetShorthand $Value - $parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir - - # Auto-name if empty - if (-not $parsed.name) { - $count = 0 - foreach ($ch in $root.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'dataSet' -and $ch.NamespaceURI -eq $schNs) { $count++ } - } - $parsed.name = "НаборДанных$($count + 1)" - } - - # Duplicate check - $existing = Find-ElementByChildValue $root "dataSet" "name" $parsed.name $schNs - if ($existing) { - Write-Host "[WARN] DataSet `"$($parsed.name)`" already exists — skipped" - } else { - # Get dataSource name from first existing <dataSource> - $dsSourceEl = Find-FirstElement $root @("dataSource") $schNs - $dsSourceName = "ИсточникДанных1" - if ($dsSourceEl) { - $nameEl = Find-FirstElement $dsSourceEl @("name") $schNs - if ($nameEl) { $dsSourceName = $nameEl.InnerText.Trim() } - } - $parsed["dataSource"] = $dsSourceName - - $fragXml = Build-DataSetQueryFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - # Insert after last <dataSet>, or after <dataSource> if none - $lastDS = Find-LastElement $root "dataSet" $schNs - if ($lastDS) { - $refNode = $lastDS.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = Find-FirstElement $root @("dataSetLink","calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs - } - - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)" - } - } - - "add-variant" { - $root = $xmlDoc.DocumentElement - $childIndent = Get-ChildIndent $root - - foreach ($val in $values) { - $parsed = Parse-VariantShorthand $val - - # Duplicate check — search for settingsVariant with matching dcsset:name - $isDup = $false - foreach ($ch in $root.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'settingsVariant' -and $ch.NamespaceURI -eq $schNs) { - foreach ($gc in $ch.ChildNodes) { - if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs -and $gc.InnerText -eq $parsed.name) { - $isDup = $true; break - } - } - if ($isDup) { break } - } - } - if ($isDup) { - Write-Host "[WARN] Variant `"$($parsed.name)`" already exists — skipped" - continue - } - - $fragXml = Build-VariantFragment -parsed $parsed -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - # Insert after last <settingsVariant> - $lastSV = Find-LastElement $root "settingsVariant" $schNs - if ($lastSV) { - $refNode = $lastSV.NextSibling - while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { - $refNode = $refNode.NextSibling - } - } else { - $refNode = $null - } - - foreach ($node in $nodes) { - Insert-BeforeElement $root $node $refNode $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added" - } - } - - "add-conditionalAppearance" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-ConditionalAppearanceShorthand $val - - $caEl = Ensure-SettingsChild $settings "conditionalAppearance" @("outputParameters","order","filter","selection") - $caIndent = Get-ContainerChildIndent $caEl - - $fragXml = Build-ConditionalAppearanceItemFragment -parsed $parsed -indent $caIndent - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $caEl $node $null $caIndent - } - - $desc = "$($parsed.param) = $($parsed.value)" - if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" } - if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" } - $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`"" - } - } - - "clear-selection" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Find-FirstElement $settings @("selection") $setNs - if ($selection) { - Clear-ContainerChildren $selection - $script:Dirty = $true; Write-Host "[OK] Selection cleared in variant `"$varName`"" - } else { - Write-Host "[INFO] No selection section in variant `"$varName`"" - } - } - - "clear-order" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $orderEl = Find-FirstElement $settings @("order") $setNs - if ($orderEl) { - Clear-ContainerChildren $orderEl - $script:Dirty = $true; Write-Host "[OK] Order cleared in variant `"$varName`"" - } else { - Write-Host "[INFO] No order section in variant `"$varName`"" - } - } - - "clear-filter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $filterEl = Find-FirstElement $settings @("filter") $setNs - if ($filterEl) { - Clear-ContainerChildren $filterEl - $script:Dirty = $true; Write-Host "[OK] Filter cleared in variant `"$varName`"" - } else { - Write-Host "[INFO] No filter section in variant `"$varName`"" - } - } - - "clear-conditionalAppearance" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $caEl = Find-FirstElement $settings @("conditionalAppearance") $setNs - if ($caEl) { - Clear-ContainerChildren $caEl - $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`"" - } else { - Write-Host "[INFO] No conditionalAppearance section in variant `"$varName`"" - } - } - - "modify-filter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-FilterShorthand $val - - $filterEl = Find-FirstElement $settings @("filter") $setNs - if (-not $filterEl) { - Write-Host "[WARN] No filter section in variant `"$varName`"" - continue - } - - $filterItem = Find-ElementByChildValue $filterEl "item" "left" $parsed.field $setNs - if (-not $filterItem) { - Write-Host "[WARN] Filter for `"$($parsed.field)`" not found in variant `"$varName`"" - continue - } - - $itemIndent = Get-ChildIndent $filterItem - - # Update comparisonType - Set-OrCreateChildElement $filterItem "comparisonType" $setNs $parsed.op $itemIndent - - # Update right value - if ($null -ne $parsed.value) { - $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } - Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent - } - - # Update use (only when explicitly set via @off / @on) - if ($parsed.use -eq $false) { - Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent - } elseif ($parsed.use -eq $true) { - # @on: remove existing use=false if any - $useEl = $null - foreach ($ch in $filterItem.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) { - $useEl = $ch; break - } - } - if ($useEl -and $useEl.InnerText -eq 'false') { - Remove-NodeWithWhitespace $useEl - } - } - - # Update viewMode - if ($parsed.viewMode) { - Set-OrCreateChildElement $filterItem "viewMode" $setNs $parsed.viewMode $itemIndent - } - - # Update userSettingID - if ($parsed.userSettingID) { - $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } - Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent - } - - $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`"" - } - } - - "modify-dataParameter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $parsed = Parse-DataParamShorthand $val - - $dpEl = Find-FirstElement $settings @("dataParameters") $setNs - if (-not $dpEl) { - Write-Host "[WARN] No dataParameters section in variant `"$varName`"" - continue - } - - $dpItem = Find-ElementByChildValue $dpEl "item" "parameter" $parsed.parameter $corNs - if (-not $dpItem) { - Write-Host "[WARN] DataParameter `"$($parsed.parameter)`" not found in variant `"$varName`"" - continue - } - - $itemIndent = Get-ChildIndent $dpItem - - # Update value - if ($null -ne $parsed.value) { - # Remove existing value element first - $existingVal = $null - foreach ($ch in $dpItem.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $corNs) { - $existingVal = $ch; break - } - } - if ($existingVal) { - Remove-NodeWithWhitespace $existingVal - } - - # Build new value fragment - $valLines = @() - if ($parsed.value -is [hashtable] -and $parsed.value.variant) { - $valLines += "$itemIndent<dcscor:value xsi:type=`"v8:StandardPeriod`">" - $valLines += "$itemIndent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>" - $valLines += "$itemIndent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - $valLines += "$itemIndent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - $valLines += "$itemIndent</dcscor:value>" - } elseif (Test-EmptyValue $parsed.value) { - $valLines += "$itemIndent<dcscor:value xsi:nil=`"true`"/>" - } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { - $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { - $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } else { - $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" - } - $valXml = $valLines -join "`n" - $valNodes = Import-Fragment $xmlDoc $valXml - foreach ($node in $valNodes) { - Insert-BeforeElement $dpItem $node $null $itemIndent - } - } - - # Update use (only when explicitly set via @off / @on) - if ($parsed.use -eq $false) { - Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent - } elseif ($parsed.use -eq $true) { - # @on: remove existing use=false if any - $useEl = $null - foreach ($ch in $dpItem.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) { - $useEl = $ch; break - } - } - if ($useEl -and $useEl.InnerText -eq 'false') { - Remove-NodeWithWhitespace $useEl - } - } - - # Update viewMode - if ($parsed.viewMode) { - Set-OrCreateChildElement $dpItem "viewMode" $setNs $parsed.viewMode $itemIndent - } - - # Update userSettingID - if ($parsed.userSettingID) { - $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } - Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent - } - - $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`"" - } - } - - "modify-field" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - foreach ($val in $values) { - $parsed = Parse-FieldShorthand $val - $fieldName = $parsed.dataPath - - # Find existing field - $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs - if (-not $fieldEl) { - Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" - continue - } - - # Read existing properties - $existing = Read-FieldProperties $fieldEl - - # Merge: parsed overrides existing for non-empty values - $merged = @{ - dataPath = $existing.dataPath - field = $existing.field - title = if ($parsed.title) { $parsed.title } else { $existing.title } - type = if ($parsed.type) { $parsed.type } else { $existing.type } - roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles } - restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict } - # Preserve raw <valueType> only when user did NOT override type via shorthand — - # otherwise the override path rebuilds valueType from $parsed.type. - rawValueType = if ($parsed.type) { $null } else { $existing._rawValueType } - # Preserve raw multi-lang title; pass existing ru content for change detection. - _rawTitle = $existing._rawTitle - _existingTitleRu = $existing.title - # Pass-through unknown children (e.g. <editFormat>, <appearance>, custom extensions). - _unknownChildren = $existing._unknownChildren - } - - # Remember position (NextSibling after whitespace) - $nextSib = $fieldEl.NextSibling - while ($nextSib -and ($nextSib.NodeType -eq 'Whitespace' -or $nextSib.NodeType -eq 'SignificantWhitespace')) { - $nextSib = $nextSib.NextSibling - } - - # Remove old field - $childIndent = Get-ChildIndent $dsNode - Remove-NodeWithWhitespace $fieldEl - - # Build new field fragment with merged data - $fragXml = Build-FieldFragment -parsed $merged -indent $childIndent - $nodes = Import-Fragment $xmlDoc $fragXml - - # Insert at saved position - foreach ($node in $nodes) { - Insert-BeforeElement $dsNode $node $nextSib $childIndent - } - - $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`"" - } - } - - "set-field-role" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - foreach ($val in $values) { - # Parse shorthand: "dataPath [@flag ...] [kv=value ...]" - $s = $val.Trim() - - # Extract @flags - $flags = @() - $flagMatches = [regex]::Matches($s, '@(\w+)') - foreach ($m in $flagMatches) { $flags += $m.Groups[1].Value } - $s = [regex]::Replace($s, '\s*@\w+', '').Trim() - - # Extract kv=value (value is non-whitespace) - $kv = [ordered]@{} - $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') - foreach ($m in $kvMatches) { $kv[$m.Groups[1].Value] = $m.Groups[2].Value } - $s = [regex]::Replace($s, '\s*\w+=\S+', '').Trim() - - $dataPath = $s - if (-not $dataPath) { - Write-Host "[WARN] set-field-role: empty dataPath in `"$val`"" - continue - } - - $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $dataPath $schNs - if (-not $fieldEl) { - Write-Host "[WARN] Field `"$dataPath`" not found in dataset `"$dsName`"" - continue - } - - $fieldIndent = Get-ChildIndent $fieldEl - - # Remove existing <role> — but first capture OuterXml of any sub-children that - # Build-RoleXml won't re-emit (e.g. <dcscom:addition>, <dcscom:groupFields>, - # custom extension elements). Preserved across rebuild. - $oldRole = $null - foreach ($ch in $fieldEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'role' -and $ch.NamespaceURI -eq $schNs) { $oldRole = $ch; break } - } - $knownRoleChildren = @('periodNumber','periodType','dimension','ignoreNullsInGroups','balance','account','accountTypeExpression','additionType','addition') - $preservedRoleChildren = @() - if ($oldRole) { - foreach ($gc in $oldRole.ChildNodes) { - if ($gc.NodeType -ne 'Element') { continue } - if ($knownRoleChildren -contains $gc.LocalName) { continue } - # kv keys override the same-named sub-element on rebuild — don't preserve - # what the user explicitly set. - if ($kv.Contains($gc.LocalName)) { continue } - $raw = $gc.OuterXml - $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') - $preservedRoleChildren += $raw - } - Remove-NodeWithWhitespace $oldRole - } - - # Empty spec — remove only - if ($flags.Count -eq 0 -and $kv.Count -eq 0) { - $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role cleared" - continue - } - - # Build new <role> - $lines = @() - $lines += "$fieldIndent<role>" - foreach ($flag in $flags) { - if ($flag -eq 'period') { - $lines += "$fieldIndent`t<dcscom:periodNumber>1</dcscom:periodNumber>" - $lines += "$fieldIndent`t<dcscom:periodType>Main</dcscom:periodType>" - } else { - $lines += "$fieldIndent`t<dcscom:$flag>true</dcscom:$flag>" - } - } - foreach ($k in $kv.Keys) { - $lines += "$fieldIndent`t<dcscom:$k>$(Esc-Xml $kv[$k])</dcscom:$k>" - } - foreach ($raw in $preservedRoleChildren) { - $lines += "$fieldIndent`t" + $raw - } - $lines += "$fieldIndent</role>" - $fragXml = $lines -join "`n" - - # Insert before <valueType>, else before <inputParameters>, else at end - $refNode = $null - foreach ($ch in $fieldEl.ChildNodes) { - if ($ch.NodeType -eq 'Element' -and $ch.LocalName -in @('valueType','inputParameters') -and $ch.NamespaceURI -eq $schNs) { $refNode = $ch; break } - } - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $fieldEl $node $refNode $fieldIndent - } - - $desc = @() - if ($flags.Count -gt 0) { $desc += ($flags | ForEach-Object { "@$_" }) -join ' ' } - if ($kv.Count -gt 0) { $desc += ($kv.Keys | ForEach-Object { "$_=$($kv[$_])" }) -join ' ' } - $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')" - } - } - - "remove-field" { - $dsNode = Resolve-DataSet - $dsName = Get-DataSetName $dsNode - - foreach ($val in $values) { - $fieldName = $val.Trim() - - $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs - if (-not $fieldEl) { - Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" - continue - } - - Remove-NodeWithWhitespace $fieldEl - $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`"" - - # Also remove from selection in variant - try { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Find-FirstElement $settings @("selection") $setNs - if ($selection) { - $selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs - if ($selItem) { - Remove-NodeWithWhitespace $selItem - $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`"" - } - } - } catch { - # No variant — that's fine - } - } - } - - "remove-total" { - foreach ($val in $values) { - $dataPath = $val.Trim() - $root = $xmlDoc.DocumentElement - - $totalEl = Find-ElementByChildValue $root "totalField" "dataPath" $dataPath $schNs - if (-not $totalEl) { - Write-Host "[WARN] TotalField `"$dataPath`" not found" - continue - } - - Remove-NodeWithWhitespace $totalEl - $script:Dirty = $true; Write-Host "[OK] TotalField `"$dataPath`" removed" - } - } - - "remove-calculated-field" { - foreach ($val in $values) { - $dataPath = $val.Trim() - $root = $xmlDoc.DocumentElement - - $calcEl = Find-ElementByChildValue $root "calculatedField" "dataPath" $dataPath $schNs - if (-not $calcEl) { - Write-Host "[WARN] CalculatedField `"$dataPath`" not found" - continue - } - - Remove-NodeWithWhitespace $calcEl - $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$dataPath`" removed" - - # Also remove from selection - try { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - $selection = Find-FirstElement $settings @("selection") $setNs - if ($selection) { - $selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs - if ($selItem) { - Remove-NodeWithWhitespace $selItem - $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`"" - } - } - } catch { } - } - } - - "remove-parameter" { - foreach ($val in $values) { - $paramName = $val.Trim() - $root = $xmlDoc.DocumentElement - - $paramEl = Find-ElementByChildValue $root "parameter" "name" $paramName $schNs - if (-not $paramEl) { - Write-Host "[WARN] Parameter `"$paramName`" not found" - continue - } - - Remove-NodeWithWhitespace $paramEl - $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`" removed" - } - } - - "remove-filter" { - $settings = Resolve-VariantSettings - $varName = Get-VariantName - - foreach ($val in $values) { - $fieldName = $val.Trim() - - $filterEl = Find-FirstElement $settings @("filter") $setNs - if (-not $filterEl) { - Write-Host "[WARN] No filter section in variant `"$varName`"" - continue - } - - $filterItem = Find-ElementByChildValue $filterEl "item" "left" $fieldName $setNs - if (-not $filterItem) { - Write-Host "[WARN] Filter for `"$fieldName`" not found in variant `"$varName`"" - continue - } - - Remove-NodeWithWhitespace $filterItem - $script:Dirty = $true; Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`"" - } - } - - "add-drilldown" { - # String-based manipulation — templates use dcsat namespace with inline xmlns - $rawText = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) - $nl = "`r`n" - $dcsatNsDecl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"' - - # Find all outer <template> blocks by nesting-aware scan - $tplStarts = [System.Collections.ArrayList]::new() - $nameRegex = [regex]'<template>\s*<name>([^<]+)</name>' - foreach ($m in $nameRegex.Matches($rawText)) { - [void]$tplStarts.Add(@{ pos = $m.Index; name = $m.Groups[1].Value }) - } - - # For each start, find closing </template> at nesting depth 0 - $tplBlocks = [System.Collections.ArrayList]::new() - foreach ($ts in $tplStarts) { - $depth = 1 - $scanPos = $ts.pos + 10 # skip past opening <template> - while ($depth -gt 0 -and $scanPos -lt $rawText.Length) { - $nextOpen = $rawText.IndexOf("<template", $scanPos) - $nextClose = $rawText.IndexOf("</template>", $scanPos) - if ($nextClose -lt 0) { break } - if ($nextOpen -ge 0 -and $nextOpen -lt $nextClose) { - $depth++ - $scanPos = $nextOpen + 10 - } else { - $depth-- - if ($depth -eq 0) { - $endPos = $nextClose + "</template>".Length - [void]$tplBlocks.Add(@{ name = $ts.name; start = $ts.pos; text = $rawText.Substring($ts.pos, $endPos - $ts.pos) }) - } - $scanPos = $nextClose + 11 - } - } - } - - if ($tplBlocks.Count -eq 0) { - Write-Host "[WARN] No named templates found in schema" - } - - # Collect all insertions as (position, text) — apply in reverse order - $insertions = [System.Collections.ArrayList]::new() - - foreach ($tplBlock in $tplBlocks) { - $tplName = $tplBlock.name - $tplText = $tplBlock.text - $tplStart = $tplBlock.start - - # Build map: expression → paramName from ExpressionAreaTemplateParameter - $exprMap = @{} - $exprRegex = [regex]'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*<dcsat:name>([^<]+)</dcsat:name>\s*<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>' - foreach ($em in $exprRegex.Matches($tplText)) { - $pName = $em.Groups[1].Value - $pExpr = $em.Groups[2].Value - $exprMap[$pExpr] = $pName - } - - foreach ($resource in $values) { - $drillName = "Расшифровка_$resource" - - # Idempotency: check if already exists - if ($tplText.Contains($drillName)) { - Write-Host "[INFO] $drillName already exists in $tplName — skipped" - continue - } - - # Find ExpressionAreaTemplateParameter by expression - $paramName = $null - if ($exprMap.ContainsKey($resource)) { - $paramName = $exprMap[$resource] - } else { - Write-Host "[WARN] Expression `"$resource`" not found in template $tplName — skipped" - continue - } - - $cellCount = 0 - - # Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template - $lastParamEndTag = "</parameter>" - $lastParamPos = $tplText.LastIndexOf($lastParamEndTag) - if ($lastParamPos -ge 0) { - $insertPos = $tplStart + $lastParamPos + $lastParamEndTag.Length - # Detect indent from context - $prevNewline = $tplText.LastIndexOf("`n", $lastParamPos) - $indent = "`t`t" - if ($prevNewline -ge 0) { - $lineStart = $prevNewline + 1 - $indentMatch = [regex]::Match($tplText.Substring($lineStart), '^(\s*)') - if ($indentMatch.Success) { $indent = $indentMatch.Groups[1].Value } - } - $detailsXml = "$nl$indent<parameter $dcsatNsDecl xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" + - "$nl$indent`t<dcsat:name>$drillName</dcsat:name>" + - "$nl$indent`t<dcsat:fieldExpression>" + - "$nl$indent`t`t<dcsat:field>ИмяРесурса</dcsat:field>" + - "$nl$indent`t`t<dcsat:expression>`"$resource`"</dcsat:expression>" + - "$nl$indent`t</dcsat:fieldExpression>" + - "$nl$indent`t<dcsat:mainAction>DrillDown</dcsat:mainAction>" + - "$nl$indent</parameter>" - [void]$insertions.Add(@{ pos = $insertPos; text = $detailsXml }) - } - - # Step 2: Insert appearance binding in cells referencing this parameter - $cellTag = '<dcsat:value xsi:type="dcscor:Parameter">' + $paramName + '</dcsat:value>' - $searchStart = 0 - while (($cellIdx = $tplText.IndexOf($cellTag, $searchStart)) -ge 0) { - $cellEnd = $tplText.IndexOf("</dcsat:tableCell>", $cellIdx) - if ($cellEnd -lt 0) { break } - $appEnd = $tplText.LastIndexOf("</dcsat:appearance>", $cellEnd) - if ($appEnd -lt $cellIdx) { $searchStart = $cellEnd + 1; continue } - - # Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance> - $appPrevNl = $tplText.LastIndexOf("`n", $appEnd) - $appIndent = "`t`t`t`t`t`t" - if ($appPrevNl -ge 0) { - $appLineStart = $appPrevNl + 1 - $appIndentMatch = [regex]::Match($tplText.Substring($appLineStart), '^(\s*)') - if ($appIndentMatch.Success) { $appIndent = $appIndentMatch.Groups[1].Value } - } - $itemIndent = $appIndent + "`t" - $appearanceXml = "$itemIndent<dcscor:item>$nl" + - "$itemIndent`t<dcscor:parameter>Расшифровка</dcscor:parameter>$nl" + - "$itemIndent`t<dcscor:value xsi:type=`"dcscor:Parameter`">$drillName</dcscor:value>$nl" + - "$itemIndent</dcscor:item>$nl" - # Insert after \n (before indent of closing tag), not before the tag itself - $insertAt = if ($appPrevNl -ge 0) { $tplStart + $appPrevNl + 1 } else { $tplStart + $appEnd } - [void]$insertions.Add(@{ pos = $insertAt; text = $appearanceXml }) - $cellCount++ - $searchStart = $cellEnd + 1 - } - - $script:Dirty = $true; Write-Host "[OK] $drillName → $tplName (param + $cellCount cell(s))" - } - } - - # Apply insertions in reverse order to preserve offsets. - # For same position: reverse insertion order so first resource ends up first in file. - $idx = 0; foreach ($ins in $insertions) { $ins.seq = $idx; $idx++ } - $sorted = $insertions | Sort-Object { $_.pos }, { $_.seq } -Descending - foreach ($ins in $sorted) { - $rawText = $rawText.Insert($ins.pos, $ins.text) - } - - # Write directly — skip DOM save - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($resolvedPath, $rawText, $enc) - $script:Dirty = $true; Write-Host "[OK] Saved $resolvedPath" - exit 0 - } -} - -# --- 9. Save --- - -if (-not $script:Dirty) { - Write-Host "[INFO] No changes -- file untouched" - exit 0 -} - -$content = $xmlDoc.OuterXml -$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8' - -# Format-preserve post-processing: -# (1) restore the original raw <DataCompositionSchema ...> opening tag — DOM collapses -# multi-line xmlns declarations into one line. -if ($script:RawRootOpening) { - $content = [regex]::Replace($content, '<DataCompositionSchema\b[^>]*>', { param($m) $script:RawRootOpening }) -} - -# (2) normalize self-closing tags: `.NET XmlDocument` adds a space before `/>` -# (`<foo bar="x" />`) but 1C-Designer writes `<foo bar="x"/>`. Strip the space. -$content = [regex]::Replace($content, '(?<=\S) />', '/>') - -# (3) normalize line endings to match source — operations may mix LF (from new -# fragments) with whatever the source used (CRLF on Windows, LF on Linux/git). -if ($script:LineEnding -eq "`r`n") { - $content = $content -replace '(?<!\r)\n', "`r`n" -} else { - $content = $content -replace "`r`n", "`n" -} - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedPath, $content, $enc) - -Write-Host "[OK] Saved $resolvedPath" +# skd-edit v1.27 — Atomic 1C DCS editor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$TemplatePath, + + [Parameter(Mandatory)] + [ValidateSet( + "add-field","add-total","add-calculated-field","add-parameter","add-filter", + "add-dataParameter","add-order","add-selection","add-dataSetLink", + "add-dataSet","add-variant","add-conditionalAppearance","add-drilldown", + "set-query","patch-query","set-outputParameter","set-structure", + "modify-field","modify-filter","modify-dataParameter","modify-parameter","modify-structure","set-field-role", + "rename-parameter","reorder-parameters", + "clear-selection","clear-order","clear-filter","clear-conditionalAppearance", + "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")] + [string]$Operation, + + [Parameter(Mandatory)] + [string]$Value, + + [string]$DataSet, + [string]$Variant, + [switch]$NoSelection +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# Dirty flag — set to $true by every successful mutation. If still $false at save time, +# the file is left untouched (NO-OP operations like [WARN] not found don't rewrite). +$script:Dirty = $false + +# --- 1. Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $resolvedPath 'editable' + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>') +} + +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) + } + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + +$script:queryBaseDir = [System.IO.Path]::GetDirectoryName($resolvedPath) + +# --- 2. Type system (copied from skd-compile) --- + +$script:typeSynonyms = New-Object System.Collections.Hashtable +$script:typeSynonyms["число"] = "decimal" +$script:typeSynonyms["строка"] = "string" +$script:typeSynonyms["булево"] = "boolean" +$script:typeSynonyms["дата"] = "date" +$script:typeSynonyms["датавремя"] = "dateTime" +$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" +$script:typeSynonyms["bool"] = "boolean" +$script:typeSynonyms["str"] = "string" +$script:typeSynonyms["int"] = "decimal" +$script:typeSynonyms["integer"] = "decimal" +$script:typeSynonyms["number"] = "decimal" +$script:typeSynonyms["num"] = "decimal" +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" + +$script:outputParamTypes = @{ + "Заголовок" = "mltext" + "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" + "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" + "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" + "МакетОформления" = "xs:string" + "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" + "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" + "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + return $typeStr +} + +# --- 3. Parsers --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + } + + # Extract [Title] + if ($s -match '\[([^\]]+)\]') { + $result.title = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' + } + + # Extract @roles + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + # Extract #restrictions + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + # Split name: type + $s = $s.Trim() + if ($s.Contains(':')) { + $parts = $s -split ':', 2 + $result.dataPath = $parts[0].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) + } else { + $result.dataPath = $s + } + + $result.field = $result.dataPath + return $result +} + +function Read-FieldProperties($fieldEl) { + $props = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + _rawTitle = $null + _unknownChildren = @() + } + + foreach ($ch in $fieldEl.ChildNodes) { + if ($ch.NodeType -ne 'Element') { continue } + switch ($ch.LocalName) { + "dataPath" { $props.dataPath = $ch.InnerText.Trim() } + "field" { $props.field = $ch.InnerText.Trim() } + "title" { + # Preserve full multi-lang title OuterXml — used to keep en/uk/etc. + # siblings when shorthand overrides only the ru content. Strip xmlns + # redeclarations that OuterXml adds for sub-elements. + $raw = $ch.OuterXml + $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') + $props._rawTitle = $raw + # Also extract ru content as plain string (backward compat — used by + # external consumers reading $existing.title). + foreach ($item in $ch.ChildNodes) { + if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') { + $lang = $null; $content = $null + foreach ($gc in $item.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'lang') { $lang = $gc.InnerText.Trim() } + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') { $content = $gc.InnerText.Trim() } + } + if ($lang -eq 'ru' -and $null -ne $content) { $props.title = $content } + } + } + } + "valueType" { + # Preserve the entire <valueType> OuterXml so rebuild can re-emit qualifiers + # (StringQualifiers, NumberQualifiers, DateQualifiers, etc.) that would + # otherwise be lost. Also extract Type string for type-override shorthand. + $raw = $ch.OuterXml + # .NET OuterXml re-declares xmlns on every element where the prefix is in + # scope (because the fragment is treated as standalone). Strip these since + # the parent context at insertion point already provides them. + $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') + $props["_rawValueType"] = $raw + $typeEl = $null + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') { + $typeEl = $gc; break + } + } + if ($typeEl) { + $props["_rawTypeText"] = $typeEl.InnerText.Trim() + } + } + "role" { + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element') { + if ($gc.LocalName -eq 'periodNumber') { + $props.roles += "period" + } elseif ($gc.InnerText.Trim() -eq 'true') { + $props.roles += $gc.LocalName + } + } + } + } + "useRestriction" { + $revMap = @{ "field" = "noField"; "condition" = "noFilter"; "group" = "noGroup"; "order" = "noOrder" } + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.InnerText.Trim() -eq 'true') { + $mapped = $revMap[$gc.LocalName] + if ($mapped) { $props.restrict += $mapped } + } + } + } + default { + # Defense in depth: preserve OuterXml of unknown children so rebuild + # doesn't silently drop them (custom <editFormat>, <appearance>, etc.). + $raw = $ch.OuterXml + $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') + $props._unknownChildren += $raw + } + } + } + return $props +} + +function Parse-TotalShorthand { + param([string]$s) + + # "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity) + $parts = $s -split ':', 2 + $dataPath = $parts[0].Trim() + $funcPart = $parts[1].Trim() + + # Known DCS aggregate functions (ru + en) + $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', + 'Sum','Count','Min','Max','Avg', + 'Minimum','Maximum','Average') + + if ($funcPart -match '^\w+\(') { + # Already has expression form: Func(expr) + return @{ dataPath = $dataPath; expression = $funcPart } + } elseif ($funcPart -in $aggFuncs) { + # Short: Func → Func(DataPath) + return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } + } else { + # Identity or custom expression — use as-is + return @{ dataPath = $dataPath; expression = $funcPart } + } +} + +function Parse-CalcShorthand { + param([string]$s) + + # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". + # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside + # an expression (e.g. index access) isn't interpreted as a title. + # - `#restrict` flags use a known-names pattern and are extracted globally — + # the docs put them after `=`, and the closed flag set avoids matching + # `#word` that happens to appear inside a string literal. + $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' + + $restrict = @() + foreach ($m in [regex]::Matches($s, $restrictPattern)) { + $restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, "\s*$restrictPattern", '') + + $eqIdx = $s.IndexOf('=') + if ($eqIdx -gt 0) { + $lhs = $s.Substring(0, $eqIdx) + $rhs = $s.Substring($eqIdx + 1).Trim() + } else { + $lhs = $s + $rhs = $null + } + + $title = "" + if ($lhs -match '\[([^\]]+)\]') { + $title = $Matches[1] + $lhs = $lhs -replace '\s*\[[^\]]+\]', '' + } + $lhs = $lhs.Trim() + + if ($null -ne $rhs) { + if ($lhs.Contains(':')) { + $colonIdx = $lhs.IndexOf(':') + $dataPath = $lhs.Substring(0, $colonIdx).Trim() + $type = Resolve-TypeStr ($lhs.Substring($colonIdx + 1).Trim()) + return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict } + } + return @{ dataPath = $lhs; expression = $rhs; type = ""; title = $title; restrict = $restrict } + } + return @{ dataPath = $lhs; expression = ""; type = ""; title = $title; restrict = $restrict } +} + +function Parse-ParamShorthand { + param([string]$s) + + $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @(); valueListAllowed = $false } + + # Extract availableValue=... (must be before main parse — captures to end of string) + if ($s -match '\s*availableValue=(.+)$') { + $result.availableValues = Parse-AvailableValueList $Matches[1].Trim() + $s = ($s -replace '\s*availableValue=.+$', '').Trim() + } + + if ($s -match '@autoDates') { + $result.autoDates = $true + $s = $s -replace '\s*@autoDates', '' + } + + if ($s -match '@valueList\b') { + $result.valueListAllowed = $true + $s = $s -replace '\s*@valueList\b', '' + } + + if ($s -match '@hidden\b') { + $result.hidden = $true + $s = $s -replace '\s*@hidden\b', '' + } + + if ($s -match '@always\b') { + $result.always = $true + $s = $s -replace '\s*@always\b', '' + } + + # Extract optional [Title] (mirrors Parse-FieldShorthand) + if ($s -match '\[([^\]]*)\]') { + $result.title = $Matches[1].Trim() + $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() + } + + # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty-value sentinel + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { + $result.name = $Matches[1].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) + $hasEq = $null -ne $Matches[3] + $rhs = $Matches[4] + if ($hasEq) { + if ($rhs -and $rhs.Trim()) { + $items = Parse-ValueList $rhs.Trim() + if ($items.Count -ge 2) { + # Multi-value default → list; valueListAllowed implied + $result.value = $items + $result.valueListAllowed = $true + } else { + # Scalar (single item, quotes stripped) or empty sentinel + $result.value = if ($items.Count -eq 1) { $items[0] } else { "" } + } + } else { + $result.value = "" + } + } + } else { + $result.name = $s.Trim() + } + + return $result +} + +function Parse-FilterShorthand { + param([string]$s) + + # use is tristate: $null = not specified (modify-* won't touch), + # $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false. + $result = @{ field = ""; op = "Equal"; value = $null; use = $null; userSettingID = $null; viewMode = $null } + + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@on\b') { + $result.use = $true + $s = $s -replace '\s*@on\b', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + if ($s -match '@inaccessible') { + $result.viewMode = "Inaccessible" + $s = $s -replace '\s*@inaccessible', '' + } + + $s = $s.Trim() + + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $opRaw = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + + $opMap = @{ + "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" + } + $mapped = $opMap[$opRaw] + if ($mapped) { $result.op = $mapped } else { $result.op = $opRaw } + + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { + $result.value = $valPart + $result["valueType"] = "xs:boolean" + } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valPart + $result["valueType"] = "xs:dateTime" + } elseif ($valPart -match '^\d+(\.\d+)?$') { + $result.value = $valPart + $result["valueType"] = "xs:decimal" + } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { + $result.value = $valPart + $result["valueType"] = "dcscor:DesignTimeValue" + } else { + $result.value = $valPart + $result["valueType"] = "xs:string" + } + } + } else { + $result.field = $s + } + + return $result +} + +function Parse-DataParamShorthand { + param([string]$s) + + # use is tristate: $null = not specified (modify-* won't touch), + # $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false. + $result = @{ parameter = ""; value = $null; use = $null; userSettingID = $null; viewMode = $null } + + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@on\b') { + $result.use = $true + $s = $s -replace '\s*@on\b', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + + $s = $s.Trim() + + if ($s -match '^([^=]+)=\s*(.*)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + # Empty / sentinel — record as "" so caller emits xsi:nil + if ($valStr -eq "" -or $valStr -eq "_" -or $valStr.ToLowerInvariant() -eq "null") { + $result.value = "" + } elseif ($periodVariants -contains $valStr) { + $result.value = @{ variant = $valStr } + } else { + $result.value = $valStr + } + } else { + $result.parameter = $s + } + + return $result +} + +function Parse-OrderShorthand { + param([string]$s) + $s = $s.Trim() + if ($s -eq "Auto") { + return @{ field = "Auto"; direction = "" } + } + $parts = $s -split '\s+', 2 + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '(?i)^desc$') { $dir = "Desc" } + return @{ field = $field; direction = $dir } +} + +function Parse-DataSetLinkShorthand { + param([string]$s) + + $result = @{ source = ""; dest = ""; sourceExpr = ""; destExpr = ""; parameter = "" } + + # Extract optional [param ParamName] + if ($s -match '\[param\s+([^\]]+)\]') { + $result.parameter = $Matches[1].Trim() + $s = $s -replace '\s*\[param\s+[^\]]+\]', '' + } + + # Pattern: "Source > Dest on FieldA = FieldB" + if ($s -match '^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$') { + $result.source = $Matches[1].Trim() + $result.dest = $Matches[2].Trim() + $result.sourceExpr = $Matches[3].Trim() + $result.destExpr = $Matches[4].Trim() + } else { + Write-Error "Invalid dataSetLink shorthand: $s. Expected: 'Source > Dest on FieldA = FieldB [param Name]'" + exit 1 + } + + return $result +} + +function Parse-DataSetShorthand { + param([string]$s) + + $s = $s.Trim() + # "Name: QUERY" — split on first ": " only if prefix is a single word (no spaces) + if ($s -match '^(\S+):\s(.+)$') { + return @{ name = $Matches[1]; query = $Matches[2] } + } + return @{ name = ""; query = $s } +} + +function Parse-VariantShorthand { + param([string]$s) + + $presentation = "" + if ($s -match '\[([^\]]+)\]') { + $presentation = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' + } + $name = $s.Trim() + if (-not $presentation) { $presentation = $name } + return @{ name = $name; presentation = $presentation } +} + +function Parse-ConditionalAppearanceShorthand { + param([string]$s) + + $result = @{ param = ""; value = ""; filter = $null; fields = @() } + + # Extract " when ..." — condition part + $whenIdx = $s.IndexOf(' when ') + $forIdx = $s.IndexOf(' for ') + + # Determine boundaries + $mainEnd = $s.Length + if ($whenIdx -ge 0 -and $forIdx -ge 0) { + $mainEnd = [Math]::Min($whenIdx, $forIdx) + } elseif ($whenIdx -ge 0) { + $mainEnd = $whenIdx + } elseif ($forIdx -ge 0) { + $mainEnd = $forIdx + } + + # Parse "for" fields + if ($forIdx -ge 0) { + $forEnd = $s.Length + if ($whenIdx -gt $forIdx) { $forEnd = $whenIdx } + $forPart = $s.Substring($forIdx + 5, $forEnd - $forIdx - 5).Trim() + $result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + + # Parse "when" filter (supports " or " for OrGroup) + if ($whenIdx -ge 0) { + $whenEnd = $s.Length + if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx } + $whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim() + $orParts = $whenPart -split '\s+or\s+' + if ($orParts.Count -gt 1) { + $result.filter = @($orParts | ForEach-Object { Parse-FilterShorthand $_.Trim() }) + } else { + $result.filter = Parse-FilterShorthand $whenPart + } + } + + # Parse main part: "Param = Value" + $mainPart = $s.Substring(0, $mainEnd).Trim() + $eqIdx = $mainPart.IndexOf('=') + if ($eqIdx -gt 0) { + $result.param = $mainPart.Substring(0, $eqIdx).Trim() + $result.value = $mainPart.Substring($eqIdx + 1).Trim() + } else { + $result.param = $mainPart + } + + return $result +} + +function Parse-StructureShorthand { + param([string]$s) + + $segments = $s -split '\s*>\s*' + $result = @() + + $innermost = $null + for ($i = $segments.Count - 1; $i -ge 0; $i--) { + $seg = $segments[$i].Trim() + $group = @{ type = "group" } + + if ($seg -match '@name=(?:"([^"]+)"|''([^'']+)''|(\S+))') { + $rawName = if ($Matches[1]) { $Matches[1] } elseif ($Matches[2]) { $Matches[2] } else { $Matches[3] } + $group["name"] = $rawName.Trim() + $seg = ($seg -replace '\s*@name=(?:"[^"]+"|''[^'']+''|\S+)', '').Trim() + } + + if ($seg -match '^(?i)(details|детали)$') { + $group["groupBy"] = @() + } else { + $fields = @($seg -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $group["groupBy"] = $fields + } + + if ($null -ne $innermost) { + $group["children"] = @($innermost) + } + $innermost = $group + } + + if ($innermost) { $result += $innermost } + return ,$result +} + +function Parse-OutputParamShorthand { + param([string]$s) + $idx = $s.IndexOf('=') + if ($idx -gt 0) { + return @{ + key = $s.Substring(0, $idx).Trim() + value = $s.Substring($idx + 1).Trim() + } + } + return @{ key = $s.Trim(); value = "" } +} + +function Split-QuotedCsv { + # Splits on top-level commas, respecting 'single' and "double" quoted spans. + # Returns raw (un-stripped, un-trimmed) item spans. Used by both availableValue + # (value:presentation) and value-list (values only) parsing. + param([string]$s) + $items = @() + if ($null -eq $s) { return ,$items } + $buf = New-Object System.Text.StringBuilder + $inQuote = $null + for ($i = 0; $i -lt $s.Length; $i++) { + $ch = $s[$i] + if ($inQuote) { + [void]$buf.Append($ch) + if ($ch -eq $inQuote) { $inQuote = $null } + } elseif ($ch -eq "'" -or $ch -eq '"') { + $inQuote = $ch + [void]$buf.Append($ch) + } elseif ($ch -eq ',') { + $items += $buf.ToString() + [void]$buf.Clear() + } else { + [void]$buf.Append($ch) + } + } + if ($buf.Length -gt 0) { $items += $buf.ToString() } + return ,$items +} + +function Strip-Quotes { + # Strips a single surrounding pair of matching quotes; trims first. + param([string]$t) + $t = $t.Trim() + if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { + return $t.Substring(1, $t.Length - 2) + } + return $t +} + +function Parse-ValueList { + # Returns array of value strings (quotes stripped) split by top-level commas. + # No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00). + param([string]$s) + $result = @() + if ($null -eq $s) { return ,$result } + foreach ($raw in (Split-QuotedCsv $s)) { + $v = Strip-Quotes $raw + if ($v -ne "") { $result += $v } + } + return ,$result +} + +function Parse-AvailableValueList { + # Returns array of @{ value=...; presentation=... } from comma-separated list. + # Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons. + param([string]$s) + + $result = @() + if (-not $s) { return ,$result } + + $items = Split-QuotedCsv $s + $stripQuotes = { param($t) Strip-Quotes $t } + + foreach ($raw in $items) { + $item = $raw.Trim() + if (-not $item) { continue } + + # Find first ':' outside quotes + $colonIdx = -1 + $q = $null + for ($j = 0; $j -lt $item.Length; $j++) { + $c = $item[$j] + if ($q) { + if ($c -eq $q) { $q = $null } + } elseif ($c -eq "'" -or $c -eq '"') { + $q = $c + } elseif ($c -eq ':') { + $colonIdx = $j; break + } + } + + if ($colonIdx -ge 0) { + $valPart = $item.Substring(0, $colonIdx) + $presPart = $item.Substring($colonIdx + 1) + $result += @{ value = (& $stripQuotes $valPart); presentation = (& $stripQuotes $presPart) } + } else { + $result += @{ value = (& $stripQuotes $item); presentation = "" } + } + } + + return ,$result +} + +# --- 4. Build-* functions (XML fragment generators) --- + +function Build-ValueTypeXml { + param($typeStr, [string]$indent) + + if (-not $typeStr) { return "" } + + # Composite: array of types — concatenate per-type fragments + if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { + $parts = @() + foreach ($t in $typeStr) { + $p = Build-ValueTypeXml -typeStr "$t" -indent $indent + if ($p) { $parts += $p } + } + return $parts -join "`n" + } + + $typeStr = Resolve-TypeStr "$typeStr" + $lines = @() + + if ($typeStr -eq "boolean") { + $lines += "$indent<v8:Type>xs:boolean</v8:Type>" + return $lines -join "`n" + } + + # string, string(N), string(N,fix) — fix → AllowedLength=Fixed + if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $al = if ($Matches[4]) { "Fixed" } else { "Variable" } + $lines += "$indent<v8:Type>xs:string</v8:Type>" + $lines += "$indent<v8:StringQualifiers>" + $lines += "$indent`t<v8:Length>$len</v8:Length>" + $lines += "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" + $lines += "$indent</v8:StringQualifiers>" + return $lines -join "`n" + } + + # decimal forms — bare decimal = money 10,2; decimal(N) = integer N,0 + if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { + if (-not $Matches[1]) { + $digits = "10"; $fraction = "2"; $sign = "Any" + } else { + $digits = $Matches[2] + $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } + $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } + } + $lines += "$indent<v8:Type>xs:decimal</v8:Type>" + $lines += "$indent<v8:NumberQualifiers>" + $lines += "$indent`t<v8:Digits>$digits</v8:Digits>" + $lines += "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" + $lines += "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" + $lines += "$indent</v8:NumberQualifiers>" + return $lines -join "`n" + } + + # date / dateTime / time — all xs:dateTime, differ only in DateFractions + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + $lines += "$indent<v8:Type>xs:dateTime</v8:Type>" + $lines += "$indent<v8:DateQualifiers>" + $lines += "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" + $lines += "$indent</v8:DateQualifiers>" + return $lines -join "`n" + } + + if ($typeStr -eq "StandardPeriod") { + $lines += "$indent<v8:Type>v8:StandardPeriod</v8:Type>" + return $lines -join "`n" + } + + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + $lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" + return $lines -join "`n" + } + + if ($typeStr.Contains('.')) { + $lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>" + return $lines -join "`n" + } + + $lines += "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>" + return $lines -join "`n" +} + +# Sentinel-normalized empty check — null / "" / "_" / "null" (case-insensitive). +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} + +# Returns XML fragment string for a type-aware empty <value>. +# Empty + valueListAllowed → omit entirely (returns $null). +# tagPrefix used for dcscor: in data parameters. +function Build-EmptyValueXml { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [string]$tagName = "value", [bool]$valueListAllowed = $false) + if ($valueListAllowed) { return $null } + $t = if ($null -eq $type) { "" } else { "$type" } + # Strip well-known XML schema prefixes so callers can pass raw <v8:Type> text + $t = $t -replace '^xs:', '' -replace '^v8:', '' -replace '^d\d+p\d+:', '' + $pf = $tagPrefix + $tn = $tagName + $lines = @() + if ($t -eq "") { + $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" + } elseif ($t -eq "StandardPeriod") { + $lines += "$indent<${pf}${tn} xsi:type=`"v8:StandardPeriod`">" + $lines += "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" + $lines += "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + $lines += "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + $lines += "$indent</${pf}${tn}>" + } elseif ($t -match '^string') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:string`"/>" + } elseif ($t -match '^(date|time)') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}${tn}>" + } elseif ($t -match '^decimal') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:decimal`">0</${pf}${tn}>" + } elseif ($t -eq "boolean") { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:boolean`">false</${pf}${tn}>" + } else { + # Ref types or unknown — safe nil + $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" + } + return $lines -join "`n" +} + +function Build-MLTextXml { + param([string]$tag, [string]$text, [string]$indent) + $lines = @() + $lines += "$indent<$tag xsi:type=`"v8:LocalStringType`">" + $lines += "$indent`t<v8:item>" + $lines += "$indent`t`t<v8:lang>ru</v8:lang>" + $lines += "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>" + $lines += "$indent`t</v8:item>" + $lines += "$indent</$tag>" + return $lines -join "`n" +} + +# Patches the ru <v8:content> within an existing multi-lang title OuterXml, preserving +# en/uk/etc. siblings. Used when modify-* operates with a title-override shorthand on a +# field/parameter that already has multi-language titles (typical in ERP/БП/ЗУП). +# If no ru item exists, one is prepended before the first existing item. +function Patch-MLTextRu { + param([string]$rawOuterXml, [string]$newRuText, [string]$indent) + $escaped = Esc-Xml $newRuText + $ruItemPat = '(<v8:item>\s*<v8:lang>ru</v8:lang>\s*<v8:content>)[^<]*(</v8:content>\s*</v8:item>)' + if ([regex]::IsMatch($rawOuterXml, $ruItemPat)) { + return [regex]::Replace($rawOuterXml, $ruItemPat, { param($m) $m.Groups[1].Value + $escaped + $m.Groups[2].Value }) + } + # No ru item — prepend one inside the title element, before first <v8:item>. + $prep = "$indent`t<v8:item>`n$indent`t`t<v8:lang>ru</v8:lang>`n$indent`t`t<v8:content>$escaped</v8:content>`n$indent`t</v8:item>" + if ($rawOuterXml -match '<v8:item>') { + $re = New-Object System.Text.RegularExpressions.Regex('(\s*)<v8:item>') + return $re.Replace($rawOuterXml, "`n$prep`$1<v8:item>", 1) + } + # Empty title — inject after opening tag. + $re2 = New-Object System.Text.RegularExpressions.Regex('(<(?:\w+:)?title[^>]*>)') + return $re2.Replace($rawOuterXml, "`$1`n$prep`n$indent", 1) +} + +function Build-RoleXml { + param([string[]]$roles, [string]$indent) + + if (-not $roles -or $roles.Count -eq 0) { return "" } + + $lines = @() + $lines += "$indent<role>" + foreach ($role in $roles) { + if ($role -eq "period") { + $lines += "$indent`t<dcscom:periodNumber>1</dcscom:periodNumber>" + $lines += "$indent`t<dcscom:periodType>Main</dcscom:periodType>" + } else { + $lines += "$indent`t<dcscom:$role>true</dcscom:$role>" + } + } + $lines += "$indent</role>" + return $lines -join "`n" +} + +function Build-RestrictionXml { + param([string[]]$restrict, [string]$indent) + + if (-not $restrict -or $restrict.Count -eq 0) { return "" } + + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + + $lines = @() + $lines += "$indent<useRestriction>" + foreach ($r in $restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + $lines += "$indent`t<$xmlName>true</$xmlName>" + } + } + $lines += "$indent</useRestriction>" + return $lines -join "`n" +} + +function Build-FieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<field xsi:type=`"DataSetFieldField`">" + $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" + $lines += "$i`t<field>$(Esc-Xml $parsed.field)</field>" + + # Title: prefer raw multi-lang title (preserves en/uk/etc.). When shorthand provides + # a new ru text, patch ru content inside the raw title; otherwise emit raw as-is. + # When no raw title exists, fall back to ru-only build from shorthand. + if ($parsed._rawTitle) { + if ($parsed.title -and $parsed.title -ne $parsed._existingTitleRu) { + $lines += "$i`t" + (Patch-MLTextRu $parsed._rawTitle $parsed.title "$i`t") + } else { + $lines += "$i`t" + $parsed._rawTitle + } + } elseif ($parsed.title) { + $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") + } + + if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { + $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") + } + + $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t" + if ($roleXml) { $lines += $roleXml } + + if ($parsed.rawValueType) { + # Preserve original <valueType> verbatim — keeps qualifiers (StringQualifiers, + # NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand. + $lines += "$i`t" + $parsed.rawValueType + } elseif ($parsed.type) { + $lines += "$i`t<valueType>" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t</valueType>" + } + + # Defense in depth: re-emit OuterXml of unknown children (e.g. <editFormat>, + # <appearance>, custom extensions) that Read-FieldProperties captured. + if ($parsed._unknownChildren) { + foreach ($raw in $parsed._unknownChildren) { + $lines += "$i`t" + $raw + } + } + + $lines += "$i</field>" + return $lines -join "`n" +} + +function Build-TotalFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<totalField>" + $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" + $lines += "$i`t<expression>$(Esc-Xml $parsed.expression)</expression>" + $lines += "$i</totalField>" + return $lines -join "`n" +} + +function Build-CalcFieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<calculatedField>" + $lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>" + $lines += "$i`t<expression>$(Esc-Xml $parsed.expression)</expression>" + + if ($parsed.title) { + $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") + } + + if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { + $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") + } + + if ($parsed.type) { + $lines += "$i`t<valueType>" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t</valueType>" + } + + $lines += "$i</calculatedField>" + return $lines -join "`n" +} + +function Build-ParamValueXml { + # Returns array of XML lines for a <value xsi:type=...>...</value> element (or StandardPeriod block). + # Selects xsi:type by declared type, then falls back to value pattern. + param([string]$type, [string]$value, [string]$indent, [string]$tagName = "value", [string]$tagNs = "") + + $i = $indent + $valStr = "$value" + $open = if ($tagNs) { "$tagNs`:$tagName" } else { $tagName } + $lines = @() + + if ($type -eq "StandardPeriod") { + $lines += "$i<$open xsi:type=`"v8:StandardPeriod`">" + $lines += "$i`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>" + $lines += "$i`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + $lines += "$i`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + $lines += "$i</$open>" + return $lines + } + + $xsi = $null + if ($type -match '^date') { $xsi = "xs:dateTime" } + elseif ($type -eq "boolean") { $xsi = "xs:boolean" } + elseif ($type -match '^decimal') { $xsi = "xs:decimal" } + elseif ($type -match '^string') { $xsi = "xs:string" } + elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { + $xsi = "dcscor:DesignTimeValue" + } + else { + # Type unknown or empty — guess from value + if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $xsi = "xs:dateTime" } + elseif ($valStr -eq "true" -or $valStr -eq "false") { $xsi = "xs:boolean" } + elseif ($valStr -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or + $valStr -match '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + $xsi = "dcscor:DesignTimeValue" + } + else { $xsi = "xs:string" } + } + + $lines += "$i<$open xsi:type=`"$xsi`">$(Esc-Xml $valStr)</$open>" + return $lines +} + +function Build-AvailableValueFragment { + # Returns XML lines (array) for a single <availableValue> block. + param($item, [string]$declaredType, [string]$indent) + + $lines = @() + $lines += "$indent<availableValue>" + if (Test-EmptyValue $item.value) { + $emptyXml = Build-EmptyValueXml -type $declaredType -indent "$indent`t" -tagPrefix "" -tagName "value" -valueListAllowed $false + if ($emptyXml) { $lines += $emptyXml } + } else { + $valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t" + foreach ($vl in $valueLines) { $lines += $vl } + } + if ($item.presentation) { + $lines += "$indent`t<presentation xsi:type=`"v8:LocalStringType`">" + $lines += "$indent`t`t<v8:item>" + $lines += "$indent`t`t`t<v8:lang>ru</v8:lang>" + $lines += "$indent`t`t`t<v8:content>$(Esc-Xml $item.presentation)</v8:content>" + $lines += "$indent`t`t</v8:item>" + $lines += "$indent`t</presentation>" + } + $lines += "$indent</availableValue>" + return $lines +} + +function Build-ParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $fragments = @() + + $lines = @() + $lines += "$i<parameter>" + $lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>" + + if ($parsed.title) { + $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") + } + + if ($parsed.type) { + $lines += "$i`t<valueType>" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t</valueType>" + } + + $vla = [bool]$parsed.valueListAllowed + $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) + if ($valIsArray) { + # Multi-value default (value-list): one <value> per item + foreach ($v in $parsed.value) { + $valueLines = Build-ParamValueXml -type $parsed.type -value $v -indent "$i`t" + foreach ($vl in $valueLines) { $lines += $vl } + } + } elseif ($null -ne $parsed.value) { + if (Test-EmptyValue $parsed.value) { + $emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla + if ($emptyXml) { $lines += $emptyXml } + } else { + $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t" + foreach ($vl in $valueLines) { $lines += $vl } + } + } + + if ($parsed.hidden) { + $lines += "$i`t<useRestriction>true</useRestriction>" + $lines += "$i`t<availableAsField>false</availableAsField>" + } + + if ($vla) { + $lines += "$i`t<valueListAllowed>true</valueListAllowed>" + } + + if ($parsed.availableValues -and $parsed.availableValues.Count -gt 0) { + foreach ($av in $parsed.availableValues) { + $avLines = Build-AvailableValueFragment -item $av -declaredType $parsed.type -indent "$i`t" + foreach ($l in $avLines) { $lines += $l } + } + } + + if ($parsed.always) { + $lines += "$i`t<use>Always</use>" + } + + $lines += "$i</parameter>" + $fragments += ($lines -join "`n") + + if ($parsed.autoDates) { + $paramName = $parsed.name + + # Canonical БСП pattern: title + valueType + value + useRestriction + expression + $bLines = @() + $bLines += "$i<parameter>" + $bLines += "$i`t<name>ДатаНачала</name>" + $bLines += (Build-MLTextXml -tag "title" -text "Начало периода" -indent "$i`t") + $bLines += "$i`t<valueType>" + $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $bLines += "$i`t</valueType>" + $bLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>" + $bLines += "$i`t<useRestriction>true</useRestriction>" + $bLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаНачала")</expression>" + $bLines += "$i</parameter>" + $fragments += ($bLines -join "`n") + + $eLines = @() + $eLines += "$i<parameter>" + $eLines += "$i`t<name>ДатаОкончания</name>" + $eLines += (Build-MLTextXml -tag "title" -text "Конец периода" -indent "$i`t") + $eLines += "$i`t<valueType>" + $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $eLines += "$i`t</valueType>" + $eLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>" + $eLines += "$i`t<useRestriction>true</useRestriction>" + $eLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаОкончания")</expression>" + $eLines += "$i</parameter>" + $fragments += ($eLines -join "`n") + } + + return ,$fragments +} + +function Build-FilterItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" + + if ($parsed.use -eq $false) { + $lines += "$i`t<dcsset:use>false</dcsset:use>" + } + + $lines += "$i`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $parsed.field)</dcsset:left>" + $lines += "$i`t<dcsset:comparisonType>$(Esc-Xml $parsed.op)</dcsset:comparisonType>" + + if ($null -ne $parsed.value) { + $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } + $lines += "$i`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($parsed.value)")</dcsset:right>" + } + + if ($parsed.viewMode) { + $lines += "$i`t<dcsset:viewMode>$(Esc-Xml $parsed.viewMode)</dcsset:viewMode>" + } + + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + $lines += "$i`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + + $lines += "$i</dcsset:item>" + return $lines -join "`n" +} + +function Build-SelectionItemFragment { + param([string]$fieldName, [string]$indent) + + $i = $indent + $lines = @() + if ($fieldName -eq "Auto") { + $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" + } elseif ($fieldName -match '^Folder\((.+)\)$') { + $inner = $Matches[1] + $colonIdx = $inner.IndexOf(':') + if ($colonIdx -gt 0) { + $title = $inner.Substring(0, $colonIdx).Trim() + $items = $inner.Substring($colonIdx + 1) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + } else { + $title = "" + $items = $inner -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + } + $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">" + if ($title) { + $lines += "$i`t<dcsset:lwsTitle>" + $lines += "$i`t`t<v8:item>" + $lines += "$i`t`t`t<v8:lang>ru</v8:lang>" + $lines += "$i`t`t`t<v8:content>$(Esc-Xml $title)</v8:content>" + $lines += "$i`t`t</v8:item>" + $lines += "$i`t</dcsset:lwsTitle>" + } + foreach ($item in $items) { + $lines += "$i`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" + $lines += "$i`t`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>" + $lines += "$i`t</dcsset:item>" + } + $lines += "$i`t<dcsset:placement>Auto</dcsset:placement>" + $lines += "$i</dcsset:item>" + } else { + $lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemField`">" + $lines += "$i`t<dcsset:field>$(Esc-Xml $fieldName)</dcsset:field>" + $lines += "$i</dcsset:item>" + } + return $lines -join "`n" +} + +function Build-DataParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + + if ($parsed.use -eq $false) { + $lines += "$i`t<dcscor:use>false</dcscor:use>" + } + + $lines += "$i`t<dcscor:parameter>$(Esc-Xml $parsed.parameter)</dcscor:parameter>" + + if ($null -ne $parsed.value) { + if ($parsed.value -is [hashtable] -and $parsed.value.variant) { + $lines += "$i`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" + $lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>" + $lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + $lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + $lines += "$i`t</dcscor:value>" + } elseif (Test-EmptyValue $parsed.value) { + $lines += "$i`t<dcscor:value xsi:nil=`"true`"/>" + } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { + $lines += "$i`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { + $lines += "$i`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } else { + $lines += "$i`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } + } + + if ($parsed.viewMode) { + $lines += "$i`t<dcsset:viewMode>$(Esc-Xml $parsed.viewMode)</dcsset:viewMode>" + } + + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + $lines += "$i`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + + $lines += "$i</dcscor:item>" + return $lines -join "`n" +} + +function Build-OrderItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + if ($parsed.field -eq "Auto") { + $lines += "$i<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" + } else { + $lines += "$i<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + $lines += "$i`t<dcsset:field>$(Esc-Xml $parsed.field)</dcsset:field>" + $lines += "$i`t<dcsset:orderType>$($parsed.direction)</dcsset:orderType>" + $lines += "$i</dcsset:item>" + } + return $lines -join "`n" +} + +function Build-DataSetLinkFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dataSetLink>" + $lines += "$i`t<sourceDataSet>$(Esc-Xml $parsed.source)</sourceDataSet>" + $lines += "$i`t<destinationDataSet>$(Esc-Xml $parsed.dest)</destinationDataSet>" + $lines += "$i`t<sourceExpression>$(Esc-Xml $parsed.sourceExpr)</sourceExpression>" + $lines += "$i`t<destinationExpression>$(Esc-Xml $parsed.destExpr)</destinationExpression>" + if ($parsed.parameter) { + $lines += "$i`t<parameter>$(Esc-Xml $parsed.parameter)</parameter>" + } + $lines += "$i</dataSetLink>" + return $lines -join "`n" +} + +function Build-DataSetQueryFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dataSet xsi:type=`"DataSetQuery`">" + $lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>" + $lines += "$i`t<dataSource>$(Esc-Xml $parsed.dataSource)</dataSource>" + $lines += "$i`t<query>$(Esc-Xml $parsed.query)</query>" + $lines += "$i</dataSet>" + return $lines -join "`n" +} + +function Build-VariantFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<settingsVariant>" + $lines += "$i`t<dcsset:name>$(Esc-Xml $parsed.name)</dcsset:name>" + $lines += (Build-MLTextXml -tag "dcsset:presentation" -text $parsed.presentation -indent "$i`t") + $lines += "$i`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">" + $lines += "$i`t`t<dcsset:selection>" + $lines += "$i`t`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" + $lines += "$i`t`t</dcsset:selection>" + $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" + $lines += "$i`t`t`t<dcsset:groupItems/>" + $lines += "$i`t`t`t<dcsset:order>" + $lines += "$i`t`t`t`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" + $lines += "$i`t`t`t</dcsset:order>" + $lines += "$i`t`t`t<dcsset:selection>" + $lines += "$i`t`t`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" + $lines += "$i`t`t`t</dcsset:selection>" + $lines += "$i`t`t</dcsset:item>" + $lines += "$i`t</dcsset:settings>" + $lines += "$i</settingsVariant>" + return $lines -join "`n" +} + +function Emit-FilterComparison { + param($f, [string]$indent) + $lines = @() + $lines += "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" + $lines += "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $f.field)</dcsset:left>" + $lines += "$indent`t<dcsset:comparisonType>$(Esc-Xml $f.op)</dcsset:comparisonType>" + if ($null -ne $f.value) { + $vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" } + $lines += "$indent`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($f.value)")</dcsset:right>" + } + $lines += "$indent</dcsset:item>" + return $lines +} + +function Build-ConditionalAppearanceItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dcsset:item>" + + # selection + if ($parsed.fields -and $parsed.fields.Count -gt 0) { + $lines += "$i`t<dcsset:selection>" + foreach ($fld in $parsed.fields) { + $lines += "$i`t`t<dcsset:item>" + $lines += "$i`t`t`t<dcsset:field>$(Esc-Xml $fld)</dcsset:field>" + $lines += "$i`t`t</dcsset:item>" + } + $lines += "$i`t</dcsset:selection>" + } else { + $lines += "$i`t<dcsset:selection/>" + } + + # filter + if ($parsed.filter) { + $lines += "$i`t<dcsset:filter>" + if ($parsed.filter -is [array]) { + # OrGroup + $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" + $lines += "$i`t`t`t<dcsset:groupType>OrGroup</dcsset:groupType>" + foreach ($f in $parsed.filter) { + $lines += Emit-FilterComparison $f "$i`t`t`t" + } + $lines += "$i`t`t</dcsset:item>" + } else { + $lines += Emit-FilterComparison $parsed.filter "$i`t`t" + } + $lines += "$i`t</dcsset:filter>" + } else { + $lines += "$i`t<dcsset:filter/>" + } + + # appearance + $lines += "$i`t<dcsset:appearance>" + + $val = $parsed.value + $lines += "$i`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + $lines += "$i`t`t`t<dcscor:parameter>$(Esc-Xml $parsed.param)</dcscor:parameter>" + + if ($val -match '^(web|style|win):') { + $lines += "$i`t`t`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $val)</dcscor:value>" + } elseif ($val -eq "true" -or $val -eq "false") { + $lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $val)</dcscor:value>" + } elseif ($parsed.param -eq "Формат" -or $parsed.param -eq "Текст" -or $parsed.param -eq "Заголовок") { + $lines += "$i`t`t`t<dcscor:value xsi:type=`"v8:LocalStringType`">" + $lines += "$i`t`t`t`t<v8:item>" + $lines += "$i`t`t`t`t`t<v8:lang>ru</v8:lang>" + $lines += "$i`t`t`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>" + $lines += "$i`t`t`t`t</v8:item>" + $lines += "$i`t`t`t</dcscor:value>" + } else { + $lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $val)</dcscor:value>" + } + + $lines += "$i`t`t</dcscor:item>" + $lines += "$i`t</dcsset:appearance>" + + $lines += "$i</dcsset:item>" + return $lines -join "`n" +} + +function Build-StructureItemFragment { + param($item, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">" + + # name + if ($item["name"]) { + $lines += "$i`t<dcsset:name>$(Esc-Xml $item["name"])</dcsset:name>" + } + + # groupItems + $groupBy = $item["groupBy"] + if (-not $groupBy -or $groupBy.Count -eq 0) { + $lines += "$i`t<dcsset:groupItems/>" + } else { + $lines += "$i`t<dcsset:groupItems>" + foreach ($field in $groupBy) { + $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">" + $lines += "$i`t`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + $lines += "$i`t`t`t<dcsset:groupType>Items</dcsset:groupType>" + $lines += "$i`t`t`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" + $lines += "$i`t`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" + $lines += "$i`t`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" + $lines += "$i`t`t</dcsset:item>" + } + $lines += "$i`t</dcsset:groupItems>" + } + + # order (Auto) + $lines += "$i`t<dcsset:order>" + $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" + $lines += "$i`t</dcsset:order>" + + # selection (Auto) + $lines += "$i`t<dcsset:selection>" + $lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>" + $lines += "$i`t</dcsset:selection>" + + # Recursive children + if ($item["children"]) { + foreach ($child in $item["children"]) { + $childXml = Build-StructureItemFragment -item $child -indent "$i`t" + $lines += $childXml + } + } + + $lines += "$i</dcsset:item>" + return $lines -join "`n" +} + +function Build-OutputParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $key = $parsed.key + $val = $parsed.value + $ptype = $script:outputParamTypes[$key] + if (-not $ptype) { $ptype = "xs:string" } + + $lines = @() + $lines += "$i<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + $lines += "$i`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + + if ($ptype -eq "mltext") { + $lines += "$i`t<dcscor:value xsi:type=`"v8:LocalStringType`">" + $lines += "$i`t`t<v8:item>" + $lines += "$i`t`t`t<v8:lang>ru</v8:lang>" + $lines += "$i`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>" + $lines += "$i`t`t</v8:item>" + $lines += "$i`t</dcscor:value>" + } else { + $lines += "$i`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml $val)</dcscor:value>" + } + + $lines += "$i</dcscor:item>" + return $lines -join "`n" +} + +# --- 5. XML helpers --- + +function Import-Fragment($doc, [string]$xmlString) { + $wrapper = @" +<_W xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:v8="http://v8.1c.ru/8.1/data/core" + xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" + xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" + xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" + xmlns:v8ui="http://v8.1c.ru/8.1/data/ui">$xmlString</_W> +"@ + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $doc.ImportNode($child, $true) + } + } + return ,$nodes +} + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + $text = $child.Value + if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($text -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0 + $current = $container + while ($current -and $current -ne $xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + # LF line endings — 1С DCS files use LF consistently; CRLF causes idempotency + # leaks when modify-* removes one whitespace and inserts a different-style one. + $ws = $xmlDoc.CreateWhitespace("`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $xmlDoc.CreateWhitespace("`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Clear-ContainerChildren($container) { + $toRemove = @() + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $toRemove += $child + } + } + foreach ($el in $toRemove) { + Remove-NodeWithWhitespace $el + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Find-FirstElement($container, [string[]]$localNames, [string]$nsUri) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element') { + foreach ($name in $localNames) { + if ($child.LocalName -eq $name) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + return $child + } + } + } + } + } + return $null +} + +function Find-LastElement($container, [string]$localName, [string]$nsUri) { + $last = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + $last = $child + } + } + } + return $last +} + +function Find-ElementByChildValue($container, [string]$elemName, [string]$childName, [string]$childValue, [string]$nsUri) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -ne $elemName) { continue } + if ($nsUri -and $child.NamespaceURI -ne $nsUri) { continue } + + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $childName -and $gc.InnerText.Trim() -eq $childValue) { + return $child + } + } + } + return $null +} + +function Set-OrCreateChildElement($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$indent) { + $existing = $null + foreach ($ch in $parent.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { + $existing = $ch + break + } + } + if ($existing) { + $existing.InnerText = $value + } else { + $prefix = $parent.GetPrefixOfNamespace($nsUri) + $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } + $fragXml = "$indent<$qualName>$(Esc-Xml $value)</$qualName>" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $parent $node $null $indent + } + } +} + +function Set-OrCreateChildElementWithAttr($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$xsiType, [string]$indent) { + $existing = $null + foreach ($ch in $parent.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { + $existing = $ch + break + } + } + if ($existing) { + $existing.InnerText = $value + if ($xsiType) { + $existing.SetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance", $xsiType) | Out-Null + } + } else { + $prefix = $parent.GetPrefixOfNamespace($nsUri) + $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } + $typeAttr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } + $fragXml = "$indent<$qualName$typeAttr>$(Esc-Xml $value)</$qualName>" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $parent $node $null $indent + } + } +} + +function Get-AllDataSets { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $root = $xmlDoc.DocumentElement + $result = @() + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + $result += $child + } + } + return ,$result +} + +function Normalize-LineEndings([string]$s) { + if ($null -eq $s) { return $s } + return $s.Replace("`r`n", "`n").Replace("`r", "`n") +} + +function Escape-Whitespace([string]$s) { + $sb = New-Object System.Text.StringBuilder + foreach ($c in $s.ToCharArray()) { + $code = [int]$c + if ($c -eq "`n") { [void]$sb.Append('\n') } + elseif ($c -eq "`r") { [void]$sb.Append('\r') } + elseif ($c -eq "`t") { [void]$sb.Append('\t') } + elseif ($code -lt 32 -or $code -eq 0xA0 -or ($code -ge 0x2000 -and $code -le 0x200F) -or $code -eq 0xFEFF) { + [void]$sb.AppendFormat('\u{0:X4}', $code) + } else { + [void]$sb.Append($c) + } + } + return $sb.ToString() +} + +function Collapse-Whitespace([string]$s) { + return ([regex]::Replace($s, "[\s ]+", " ")).Trim() +} + +function Find-LongestPrefixMatch([string]$haystack, [string]$needle) { + # Binary search: largest L such that needle.Substring(0, L) is a substring of haystack. + # Monotonic — if length L matches at position P, then length L-1 (prefix) also matches at P. + if ($needle.Length -eq 0 -or $haystack.Length -eq 0) { + return @{ Length = 0; Offset = -1 } + } + if ($haystack.IndexOf([string]$needle[0]) -lt 0) { + return @{ Length = 0; Offset = -1 } + } + $lo = 1; $hi = $needle.Length + $bestLen = 1; $bestOffset = $haystack.IndexOf([string]$needle[0]) + while ($lo -le $hi) { + $mid = [int](($lo + $hi) / 2) + $idx = $haystack.IndexOf($needle.Substring(0, $mid)) + if ($idx -ge 0) { $bestLen = $mid; $bestOffset = $idx; $lo = $mid + 1 } + else { $hi = $mid - 1 } + } + return @{ Length = $bestLen; Offset = $bestOffset } +} + +function Format-PatchQueryNotFound([string]$oldStr, [string]$queryText, $currentDsNode, [string]$dsName) { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $lines = @("Substring not found in query of dataset '$dsName'.") + + # Step 1 — cross-dataset probe + foreach ($ds in (Get-AllDataSets)) { + if ($ds -eq $currentDsNode) { continue } + $q = Find-FirstElement $ds @("query") $schNs + if (-not $q) { continue } + $qt = Normalize-LineEndings $q.InnerText + if ($qt.Contains($oldStr)) { + $otherName = Get-DataSetName $ds + $lines += "Found in dataset '$otherName' instead — wrong -DataSet?" + return ($lines -join "`n") + } + } + + # Step 2 — tolerant probe (whitespace + NBSP collapsed) + $normNeedle = Collapse-Whitespace $oldStr + $normHay = Collapse-Whitespace $queryText + $tolerant = ($normNeedle.Length -gt 0 -and $normHay.Contains($normNeedle)) + + # Step 3 — prefix divergence (used by both Step 2 reporting and standalone Step 3) + $prefix = Find-LongestPrefixMatch -haystack $queryText -needle $oldStr + $divergence = $null + if ($prefix.Length -gt 0 -and $prefix.Length -lt $oldStr.Length) { + $queryPos = $prefix.Offset + $prefix.Length + $searchChar = $oldStr[$prefix.Length] + $beforeLen = [Math]::Min(20, $prefix.Length) + $before = $oldStr.Substring($prefix.Length - $beforeLen, $beforeLen) + $divergence = [ordered]@{ + matched = $prefix.Length + total = $oldStr.Length + before = $before + searchChar = $searchChar + queryChar = $(if ($queryPos -lt $queryText.Length) { $queryText[$queryPos] } else { $null }) + } + } + + if ($tolerant) { + $lines += "Not found exactly, but would match with whitespace normalized (tabs/spaces/NBSP)." + if ($divergence) { + $lines += "Diverged at offset $($divergence.matched) of $($divergence.total):" + $lines += " before: '$(Escape-Whitespace $divergence.before)'" + $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" + if ($null -ne $divergence.queryChar) { + $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" + } + } + return ($lines -join "`n") + } + + # Step 3 standalone + if ($prefix.Length -eq 0) { + $lines += "No common prefix with query. Check -DataSet (current: '$dsName')." + return ($lines -join "`n") + } + $lines += "Matched first $($divergence.matched) of $($divergence.total) chars, then diverged:" + $lines += " before: '$(Escape-Whitespace $divergence.before)'" + $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" + if ($null -ne $divergence.queryChar) { + $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" + } else { + $lines += " in query: (end of query)" + } + return ($lines -join "`n") +} + +function Resolve-DataSet { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $root = $xmlDoc.DocumentElement + + if ($DataSet) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $DataSet) { + return $child + } + } + } + Write-Error "DataSet '$DataSet' not found" + exit 1 + } + + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + return $child + } + } + Write-Error "No dataSet found in DCS" + exit 1 +} + +function Resolve-VariantSettings { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + $sv = $null + if ($Variant) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $Variant) { + $sv = $child + break + } + } + } + if (-not $sv) { + Write-Error "Variant '$Variant' not found" + exit 1 + } + } else { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $sv = $child + break + } + } + if (-not $sv) { + Write-Error "No settingsVariant found in DCS" + exit 1 + } + } + + foreach ($gc in $sv.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) { + return $gc + } + } + + Write-Error "No <dcsset:settings> found in variant" + exit 1 +} + +function Ensure-SettingsChild($settings, [string]$childName, [string[]]$afterSiblings) { + $el = Find-FirstElement $settings @($childName) $setNs + if ($el) { return $el } + + $indent = Get-ChildIndent $settings + $fragXml = "$indent<dcsset:$childName/>" + $nodes = Import-Fragment $xmlDoc $fragXml + + $refNode = $null + foreach ($sibName in $afterSiblings) { + $sib = Find-FirstElement $settings @($sibName) $setNs + if ($sib) { + $refNode = $sib.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + break + } + } + + foreach ($node in $nodes) { + Insert-BeforeElement $settings $node $refNode $indent + } + + return Find-FirstElement $settings @($childName) $setNs +} + +function Get-VariantName { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + if ($Variant) { return $Variant } + + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + return $gc.InnerText + } + } + } + } + return "(unknown)" +} + +function Get-DataSetName($dsNode) { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + foreach ($gc in $dsNode.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + return $gc.InnerText + } + } + return "(unknown)" +} + +function Get-ContainerChildIndent($container) { + $hasElements = $false + foreach ($ch in $container.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } + } + if ($hasElements) { + return Get-ChildIndent $container + } else { + $parentIndent = Get-ChildIndent $container.ParentNode + return $parentIndent + "`t" + } +} + +# --- 6. Load XML --- + +# Capture raw original BEFORE DOM parse — needed at save time to: +# (a) restore exact root <DataCompositionSchema xmlns=...> opening tag (DOM serializer +# collapses multi-line xmlns into a single line); +# (b) detect NO-OP via byte-equality as an extra safety net. +$script:RawOriginal = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) +$rootOpenMatch = [regex]::Match($script:RawOriginal, '<DataCompositionSchema\b[^>]*>') +if ($rootOpenMatch.Success) { $script:RawRootOpening = $rootOpenMatch.Value } else { $script:RawRootOpening = $null } + +# Detect line ending convention so save can normalize back to whatever the source used. +# 1С Designer writes CRLF on Windows; LF-edited files should stay LF. +$script:LineEnding = if ($script:RawOriginal.Contains("`r`n")) { "`r`n" } else { "`n" } + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($resolvedPath) + +$schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +$setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" +$corNs = "http://v8.1c.ru/8.1/data-composition-system/core" + +# --- 7. Batch value splitting --- + +if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "modify-structure" -or $Operation -eq "add-dataSet") { + $values = @($Value) +} elseif ($Operation -eq "patch-query") { + $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +} elseif ($Operation -eq "add-drilldown") { + if ($Value.Contains(';;')) { + $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } else { + $values = @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } +} else { + $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +} + +# --- 8. Main logic --- + +switch ($Operation) { + "add-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $parsed = Parse-FieldShorthand $val + $childIndent = Get-ChildIndent $dsNode + + # Duplicate check + $existing = Find-ElementByChildValue $dsNode "field" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] Field `"$($parsed.dataPath)`" already exists in dataset `"$dsName`" — skipped" + continue + } + + $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $refNode = Find-FirstElement $dsNode @("dataSource") $schNs + foreach ($node in $nodes) { + Insert-BeforeElement $dsNode $node $refNode $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" + + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Ensure-SettingsChild $settings "selection" @() + $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs + if ($existingSel) { + Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" + } else { + $selIndent = Get-ContainerChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + } + + "add-total" { + foreach ($val in $values) { + $parsed = Parse-TotalShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped" + continue + } + + $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $root = $xmlDoc.DocumentElement + $lastTotal = Find-LastElement $root "totalField" $schNs + if ($lastTotal) { + $refNode = $lastTotal.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" + } + } + + "add-calculated-field" { + foreach ($val in $values) { + $parsed = Parse-CalcShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped" + continue + } + + $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $root = $xmlDoc.DocumentElement + $lastCalc = Find-LastElement $root "calculatedField" $schNs + if ($lastCalc) { + $refNode = $lastCalc.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" + + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Ensure-SettingsChild $settings "selection" @() + $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs + if ($existingSel) { + Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" + } else { + $selIndent = Get-ContainerChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + } + + "add-parameter" { + foreach ($val in $values) { + $parsed = Parse-ParamShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs + if ($existing) { + Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped" + continue + } + + $fragments = Build-ParamFragment -parsed $parsed -indent $childIndent + + $root = $xmlDoc.DocumentElement + $lastParam = Find-LastElement $root "parameter" $schNs + if ($lastParam) { + $refNode = $lastParam.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("template","groupTemplate","settingsVariant") $schNs + } + + foreach ($fragXml in $fragments) { + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + } + + $script:Dirty = $true; Write-Host "[OK] Parameter `"$($parsed.name)`" added" + if ($parsed.autoDates) { + $script:Dirty = $true; Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" + } + } + } + + "modify-parameter" { + foreach ($val in $values) { + # Parse: "ParamName [Title] key=value key=value" + # Extract optional [Title] first (mirrors Parse-FieldShorthand) + $titleVal = $null + if ($val -match '\[([^\]]*)\]') { + $titleVal = $Matches[1].Trim() + $val = ($val -replace '\s*\[[^\]]*\]\s*', ' ').Trim() + } + + $parts = $val -split '\s+', 2 + $paramName = $parts[0].Trim() + $rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" } + + # Extract @hidden / @always flags + $flagHidden = $false + $flagAlways = $false + if ($rest -match '@hidden\b') { $flagHidden = $true; $rest = ($rest -replace '\s*@hidden\b', '').Trim() } + if ($rest -match '@always\b') { $flagAlways = $true; $rest = ($rest -replace '\s*@always\b', '').Trim() } + + # Find parameter element + $paramEl = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $paramName $schNs + if (-not $paramEl) { + Write-Host "[WARN] Parameter `"$paramName`" not found — skipped" + continue + } + + $childIndent = Get-ChildIndent $paramEl + + # Set/replace title (must come right after <name>, before <valueType>) + if ($null -ne $titleVal) { + $existingTitle = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'title') { + $existingTitle = $ch; break + } + } + # If the existing title has multiple <v8:item> (multi-language: ru + en + …), + # patch only the ru <v8:content> via raw-string surgery to preserve other langs. + # Otherwise rebuild as ru-only fragment. + $titleFrag = $null + if ($existingTitle) { + $rawTitle = $existingTitle.OuterXml + $rawTitle = [regex]::Replace($rawTitle, ' xmlns(?::\w+)?="[^"]*"', '') + # Count <v8:item> occurrences — if >1, treat as multi-lang. + $itemCount = ([regex]::Matches($rawTitle, '<v8:item>')).Count + if ($itemCount -gt 1) { + $titleFrag = $childIndent + (Patch-MLTextRu $rawTitle $titleVal $childIndent) + } + Remove-NodeWithWhitespace $existingTitle + } + if (-not $titleFrag) { + $titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent + } + # Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...) + $titleRef = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -ne 'name') { + $titleRef = $ch; break + } + } + $titleNodes = Import-Fragment $xmlDoc $titleFrag + foreach ($node in $titleNodes) { + Insert-BeforeElement $paramEl $node $titleRef $childIndent + } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`"" + } + + # Separate availableValue=... from simple kv pairs + $simpleRest = $rest + $avPart = $null + $avIdx = $rest.IndexOf('availableValue=') + if ($avIdx -ge 0) { + $simpleRest = $rest.Substring(0, $avIdx).Trim() + $avPart = $rest.Substring($avIdx) + } + + # Separate a multi-value value=... (list) — kv-regex below grabs only a single + # \S+ token, so a comma-separated list (with spaces) wouldn't be captured. + # availableValue already peeled, so 'value=' here is the real value key. + $valueListItems = $null + $vlIdx = $simpleRest.IndexOf('value=') + if ($vlIdx -ge 0) { + $vlRhs = $simpleRest.Substring($vlIdx + 'value='.Length) + $cand = Parse-ValueList $vlRhs + if ($cand.Count -ge 2) { + $valueListItems = $cand + $simpleRest = $simpleRest.Substring(0, $vlIdx).Trim() + } + } + + # Process simple key=value pairs (use, denyIncompleteValues, value, etc.) + if ($simpleRest) { + $kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)') + foreach ($kv in $kvPairs) { + $key = $kv.Groups[1].Value + $value = $kv.Groups[2].Value + + # Namespace-aware lookup (children live in $schNs) + $existing = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $key -and $ch.NamespaceURI -eq $schNs) { + $existing = $ch; break + } + } + + if ($key -eq "value") { + # Special-case: rebuild <value> with correct xsi:type from <valueType> + $declaredType = "" + $vtEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } + } + if ($vtEl) { + foreach ($tnode in $vtEl.ChildNodes) { + if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { + $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' + break + } + } + } + # Detect valueListAllowed flag on the parameter — empty value should be omitted + $vlaSet = $false + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { + if ($ch.InnerText.Trim() -eq 'true') { $vlaSet = $true } + break + } + } + if (Test-EmptyValue $value) { + $fragXml = Build-EmptyValueXml -type $declaredType -indent $childIndent -tagPrefix "" -tagName "value" -valueListAllowed $vlaSet + } else { + $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent + $fragXml = $valueLines -join "`n" + } + + # Collect ALL existing <value> (a param may carry a value-list) — scalar + # value= collapses them to one, so remove every <value>, not just the first. + $allValueEls = @() + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $schNs) { $allValueEls += $ch } + } + $wasExisting = ($allValueEls.Count -gt 0) + if ($wasExisting) { + # Capture position after the last existing value, then remove all + $refNode = $allValueEls[$allValueEls.Count - 1].NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + foreach ($ve in $allValueEls) { Remove-NodeWithWhitespace $ve } + } else { + # Insert before useRestriction/availableValue/denyIncompleteValues/use + $refNode = $null + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { + $refNode = $child; break + } + } + } + if ($fragXml) { + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $paramEl $node $refNode $childIndent + } + } + $verb = if ($wasExisting) { "updated" } else { "added" } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value $verb to $value" + } elseif ($existing) { + $existing.InnerText = $value + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key updated to $value" + } else { + # Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use + $refNode = $null + if ($key -eq "denyIncompleteValues") { + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'use') { + $refNode = $child; break + } + } + } + $fragXml = "$childIndent<$key>$(Esc-Xml $value)</$key>" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $paramEl $node $refNode $childIndent + } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key=$value added" + } + } + } + + # Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true + if ($valueListItems) { + # Declared type from <valueType> + $declaredType = "" + $vtEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } + } + if ($vtEl) { + foreach ($tnode in $vtEl.ChildNodes) { + if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { + $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' + break + } + } + } + # Remove ALL existing <value>; capture insertion ref after the last one + $valueEls = @() + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'value' -and $child.NamespaceURI -eq $schNs) { $valueEls += $child } + } + $refNode = $null + if ($valueEls.Count -gt 0) { + $refNode = $valueEls[$valueEls.Count - 1].NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } + foreach ($ve in $valueEls) { Remove-NodeWithWhitespace $ve } + } else { + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } + } + } + foreach ($v in $valueListItems) { + $fragXml = (Build-ParamValueXml -type $declaredType -value $v -indent $childIndent) -join "`n" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } + } + # Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use) + $vlaEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { $vlaEl = $ch; break } + } + if ($vlaEl) { + if ($vlaEl.InnerText.Trim() -ne 'true') { $vlaEl.InnerText = 'true' } + } else { + $refVla = $null + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refVla = $child; break } + } + $nodes = Import-Fragment $xmlDoc "$childIndent<valueListAllowed>true</valueListAllowed>" + foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refVla $childIndent } + } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value set to list of $($valueListItems.Count) item(s)" + } + + # Process availableValue — replace whole list with new items + if ($avPart) { + $avRest = ($avPart -replace '^availableValue=', '').Trim() + $avItems = Parse-AvailableValueList $avRest + + # Detect value type: prefer declared <valueType> of the parameter, else guess from value + $declaredType = "" + $vtEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } + } + if ($vtEl) { + foreach ($tnode in $vtEl.ChildNodes) { + if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { + $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' + break + } + } + } + + # Remove all existing <availableValue> elements + $toRemove = @() + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableValue' -and $ch.NamespaceURI -eq $schNs) { + $toRemove += $ch + } + } + foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } + + # Insert each new <availableValue> before (denyIncompleteValues, use) + $refNode = $null + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and ($child.LocalName -eq 'denyIncompleteValues' -or $child.LocalName -eq 'use')) { + $refNode = $child; break + } + } + foreach ($av in $avItems) { + $avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent + $fragXml = $avLines -join "`n" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $paramEl $node $refNode $childIndent + } + } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)" + } + + # Process @hidden / @always flags (idempotent) + if ($flagHidden) { + # useRestriction → true (insert after <value>, before <expression>/<availableAsField>/...) + $urEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'useRestriction' -and $ch.NamespaceURI -eq $schNs) { $urEl = $ch; break } + } + if ($urEl) { + if ($urEl.InnerText.Trim() -ne 'true') { $urEl.InnerText = 'true' } + } else { + $refNode = $null + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('expression','availableAsField','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } + } + $nodes = Import-Fragment $xmlDoc "$childIndent<useRestriction>true</useRestriction>" + foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } + } + + # availableAsField → false (insert after <expression>, before <availableValue>/<denyIncompleteValues>/<use>) + $afEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableAsField' -and $ch.NamespaceURI -eq $schNs) { $afEl = $ch; break } + } + if ($afEl) { + if ($afEl.InnerText.Trim() -ne 'false') { $afEl.InnerText = 'false' } + } else { + $refNode = $null + foreach ($child in $paramEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refNode = $child; break } + } + $nodes = Import-Fragment $xmlDoc "$childIndent<availableAsField>false</availableAsField>" + foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } + } + + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @hidden applied" + } + + if ($flagAlways) { + $useEl = $null + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $schNs) { $useEl = $ch; break } + } + if ($useEl) { + if ($useEl.InnerText.Trim() -ne 'Always') { $useEl.InnerText = 'Always' } + } else { + $nodes = Import-Fragment $xmlDoc "$childIndent<use>Always</use>" + foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $null $childIndent } + } + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @always applied" + } + } + } + + "rename-parameter" { + foreach ($val in $values) { + # Shorthand: "OldName => NewName" + if ($val -notmatch '^\s*(.+?)\s*=>\s*(.+?)\s*$') { + Write-Host "[WARN] rename-parameter expects 'OldName => NewName', got: $val" + continue + } + $oldName = $Matches[1].Trim() + $newName = $Matches[2].Trim() + + if ($oldName -eq $newName) { + Write-Host "[WARN] rename-parameter: old and new names are equal — skipped" + continue + } + + # 1. Rename <parameter><name>OldName</name> + $root = $xmlDoc.DocumentElement + $paramEl = Find-ElementByChildValue $root "parameter" "name" $oldName $schNs + if (-not $paramEl) { + Write-Host "[WARN] Parameter `"$oldName`" not found — skipped" + continue + } + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $schNs) { + $ch.InnerText = $newName + break + } + } + + # 2. Update <expression> in other <parameter> elements. + # Regex matches "&OldName" only when followed by a non-identifier char (or end), + # so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета". + $escOld = [regex]::Escape($oldName) + $exprRegex = "&$escOld(?=[^\w\u0400-\u04FF]|$)" + $exprUpdated = 0 + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -ne 'Element' -or $ch.LocalName -ne 'parameter' -or $ch.NamespaceURI -ne $schNs) { continue } + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'expression' -and $gc.NamespaceURI -eq $schNs) { + $oldExpr = $gc.InnerText + $newExpr = [regex]::Replace($oldExpr, $exprRegex, "&$newName") + if ($newExpr -ne $oldExpr) { + $gc.InnerText = $newExpr + $exprUpdated++ + } + } + } + } + + # 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants. + # Note: <settingsVariant> is in schNs, but <settings> and <dataParameters> are in setNs. + # IMPORTANT: don't use $variant — it collides with script parameter [string]$Variant + # (PowerShell vars are case-insensitive, and the [string] type would coerce XmlNode to ""). + $dpUpdated = 0 + foreach ($variantNode in $root.ChildNodes) { + if ($variantNode.NodeType -ne 'Element' -or $variantNode.LocalName -ne 'settingsVariant' -or $variantNode.NamespaceURI -ne $schNs) { continue } + $settings = Find-FirstElement $variantNode @("settings") $setNs + if (-not $settings) { continue } + $dpEl = Find-FirstElement $settings @("dataParameters") $setNs + if (-not $dpEl) { continue } + foreach ($item in $dpEl.ChildNodes) { + if ($item.NodeType -ne 'Element' -or $item.LocalName -ne 'item') { continue } + foreach ($gc in $item.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'parameter' -and $gc.NamespaceURI -eq $corNs) { + if ($gc.InnerText.Trim() -eq $oldName) { + $gc.InnerText = $newName + $dpUpdated++ + } + } + } + } + } + + $script:Dirty = $true; Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)" + } + } + + "reorder-parameters" { + foreach ($val in $values) { + # Shorthand: "Name1, Name2, Name3" — partial list, listed names go first in order, rest preserve original order + $order = @($val -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + if ($order.Count -eq 0) { + Write-Host "[WARN] reorder-parameters: empty list — skipped" + continue + } + + $root = $xmlDoc.DocumentElement + + # Collect all <parameter> in document order with their child indent + $allParams = @() + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'parameter' -and $ch.NamespaceURI -eq $schNs) { + $allParams += $ch + } + } + if ($allParams.Count -eq 0) { + Write-Host "[WARN] reorder-parameters: no parameters in schema" + continue + } + + $childIndent = Get-ChildIndent $root + + # Build name -> element map + $byName = @{} + foreach ($pe in $allParams) { + foreach ($gc in $pe.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + $byName[$gc.InnerText.Trim()] = $pe + break + } + } + } + + # Build new order + $newOrder = @() + $used = @{} + foreach ($name in $order) { + if ($byName.ContainsKey($name)) { + $newOrder += $byName[$name] + $used[$name] = $true + } else { + Write-Host "[WARN] reorder-parameters: parameter `"$name`" not found — skipped" + } + } + foreach ($pe in $allParams) { + $peName = $null + foreach ($gc in $pe.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + $peName = $gc.InnerText.Trim(); break + } + } + if ($peName -and -not $used.ContainsKey($peName)) { + $newOrder += $pe + } + } + + # Find anchor: element right after the last parameter in original order + $lastParam = $allParams[-1] + $anchor = $lastParam.NextSibling + + # Remove all parameters with surrounding whitespace + foreach ($pe in $allParams) { + Remove-NodeWithWhitespace $pe + } + + # Re-insert in new order before anchor + foreach ($pe in $newOrder) { + Insert-BeforeElement $root $pe $anchor $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)" + } + } + + "add-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-FilterShorthand $val + + $filterEl = Ensure-SettingsChild $settings "filter" @("selection") + $filterIndent = Get-ContainerChildIndent $filterEl + + $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $filterEl $node $null $filterIndent + } + + $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`"" + } + } + + "add-dataParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-DataParamShorthand $val + + $dpEl = Ensure-SettingsChild $settings "dataParameters" @("outputParameters","conditionalAppearance","order","filter","selection") + $dpIndent = Get-ContainerChildIndent $dpEl + + $fragXml = Build-DataParamFragment -parsed $parsed -indent $dpIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $dpEl $node $null $dpIndent + } + + $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`"" + } + } + + "add-order" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-OrderShorthand $val + + $orderEl = Ensure-SettingsChild $settings "order" @("filter","selection") + $orderIndent = Get-ContainerChildIndent $orderEl + + # Duplicate check + if ($parsed.field -eq "Auto") { + $isDup = $false + foreach ($ch in $orderEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { + $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($typeAttr -and $typeAttr.Contains("OrderItemAuto")) { $isDup = $true; break } + } + } + if ($isDup) { + Write-Host "[WARN] OrderItemAuto already exists in variant `"$varName`" — skipped" + continue + } + } else { + $existingOrd = Find-ElementByChildValue $orderEl "item" "field" $parsed.field $setNs + if ($existingOrd) { + Write-Host "[WARN] Order `"$($parsed.field)`" already exists in variant `"$varName`" — skipped" + continue + } + } + + $fragXml = Build-OrderItemFragment -parsed $parsed -indent $orderIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $orderEl $node $null $orderIndent + } + + $desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" } + $script:Dirty = $true; Write-Host "[OK] Order `"$desc`" added to variant `"$varName`"" + } + } + + "add-selection" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $fieldName = $val.Trim() + $groupName = $null + + # Extract @group=Name + if ($fieldName -match '\s*@group=(\S+)') { + $groupName = $Matches[1] + $fieldName = ($fieldName -replace '\s*@group=\S+', '').Trim() + } + + if ($groupName) { + # Find named StructureItemGroup + $dcssetNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $xsiNs = "http://www.w3.org/2001/XMLSchema-instance" + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $nsMgr.AddNamespace("dcsset", $dcssetNs) + $nsMgr.AddNamespace("xsi", $xsiNs) + $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$groupName']", $nsMgr) + if (-not $groupEl) { + Write-Host "[WARN] StructureItemGroup `"$groupName`" not found — adding to variant level" + $targetEl = $settings + } else { + $targetEl = $groupEl + } + } else { + $targetEl = $settings + } + + $selection = Ensure-SettingsChild $targetEl "selection" @() + + # Dedup: skip if SelectedItemAuto already exists + if ($fieldName -eq "Auto") { + $isDup = $false + foreach ($ch in $selection.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { + $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($typeAttr -and $typeAttr.Contains("SelectedItemAuto")) { $isDup = $true; break } + } + } + if ($isDup) { + $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } + Write-Host "[WARN] SelectedItemAuto already exists in $target — skipped" + continue + } + } + + $selIndent = Get-ContainerChildIndent $selection + + $selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + + $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } + $script:Dirty = $true; Write-Host "[OK] Selection `"$fieldName`" added to $target" + } + } + + "set-query" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + $queryEl = Find-FirstElement $dsNode @("query") $schNs + if (-not $queryEl) { + Write-Error "No <query> element found in dataset '$dsName'" + exit 1 + } + + # InnerText setter handles XML escaping automatically + $queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir + + $script:Dirty = $true; Write-Host "[OK] Query replaced in dataset `"$dsName`"" + } + + "patch-query" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + $queryEl = Find-FirstElement $dsNode @("query") $schNs + if (-not $queryEl) { + Write-Error "No <query> element found in dataset '$dsName'" + exit 1 + } + + foreach ($val in $values) { + $once = $false + if ($val -match '@once\b') { + $once = $true + $val = ($val -replace '\s*@once\b', '').Trim() + } + + $sepIdx = $val.IndexOf(" => ") + if ($sepIdx -lt 0) { + Write-Error "patch-query value must contain ' => ' separator: old => new" + exit 1 + } + $oldStr = Normalize-LineEndings $val.Substring(0, $sepIdx) + $newStr = Normalize-LineEndings $val.Substring($sepIdx + 4) + $queryText = Normalize-LineEndings $queryEl.InnerText + + $count = ([regex]::Matches($queryText, [regex]::Escape($oldStr))).Count + if ($count -eq 0) { + $diag = Format-PatchQueryNotFound $oldStr $queryText $dsNode $dsName + Write-Error $diag + exit 1 + } + if ($once -and $count -ne 1) { + Write-Error "@once: expected 1 occurrence of '$oldStr' in dataset '$dsName', found $count" + exit 1 + } + + $queryEl.InnerText = $queryText.Replace($oldStr, $newStr) + $suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" } + $script:Dirty = $true; Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix" + } + } + + "set-outputParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-OutputParamShorthand $val + + $outputEl = Ensure-SettingsChild $settings "outputParameters" @("conditionalAppearance","order","filter","selection") + $outputIndent = Get-ContainerChildIndent $outputEl + + # Remove existing parameter with same key if present + $existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs + if ($existingParam) { + Remove-NodeWithWhitespace $existingParam + $script:Dirty = $true; Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`"" + } else { + $script:Dirty = $true; Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`"" + } + + $fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $outputEl $node $null $outputIndent + } + } + } + + "set-structure" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + # Remove all existing structure items (dcsset:item elements) + $toRemove = @() + foreach ($ch in $settings.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item' -and $ch.NamespaceURI -eq $setNs) { + $toRemove += $ch + } + } + foreach ($el in $toRemove) { + Remove-NodeWithWhitespace $el + } + + # Parse structure shorthand + $structItems = Parse-StructureShorthand $Value + $settingsIndent = Get-ChildIndent $settings + + # Find insertion point — before outputParameters/dataParameters/conditionalAppearance/order/filter/selection or at end + $refNode = Find-FirstElement $settings @("outputParameters","dataParameters","conditionalAppearance","order","filter","selection","item") $setNs + if (-not $refNode) { $refNode = $null } + + foreach ($structItem in $structItems) { + $fragXml = Build-StructureItemFragment -item $structItem -indent $settingsIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $settings $node $refNode $settingsIndent + } + } + + $script:Dirty = $true; Write-Host "[OK] Structure set in variant `"$varName`": $Value" + } + + "modify-structure" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + $structItems = Parse-StructureShorthand $Value + + # Flatten parsed tree into (name, groupBy) targets + $targets = @() + $stack = New-Object System.Collections.Stack + foreach ($it in $structItems) { $stack.Push($it) } + while ($stack.Count -gt 0) { + $it = $stack.Pop() + if ($it["name"]) { + $targets += @{ name = $it["name"]; groupBy = $it["groupBy"] } + } + if ($it["children"]) { + foreach ($ch in $it["children"]) { $stack.Push($ch) } + } + } + + if ($targets.Count -eq 0) { + Write-Error "modify-structure requires @name= for at least one group: $Value" + exit 1 + } + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $nsMgr.AddNamespace("dcsset", $setNs) + $nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + + foreach ($t in $targets) { + $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$($t.name)']", $nsMgr) + if (-not $groupEl) { + Write-Host "[WARN] Group with @name=`"$($t.name)`" not found — skipped" + continue + } + + $giEl = $null + foreach ($ch in $groupEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { + $giEl = $ch; break + } + } + $groupIndent = Get-ChildIndent $groupEl + if (-not $giEl) { + # Create <groupItems> after <name>, before <order>/<selection>/... + $nameEl = $null + $refAfterName = $null + foreach ($ch in $groupEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $setNs) { + $nameEl = $ch + } elseif ($ch.NodeType -eq 'Element' -and $nameEl -and -not $refAfterName) { + $refAfterName = $ch; break + } + } + $giFrag = "$groupIndent<dcsset:groupItems></dcsset:groupItems>" + $nodes = Import-Fragment $xmlDoc $giFrag + foreach ($node in $nodes) { + Insert-BeforeElement $groupEl $node $refAfterName $groupIndent + } + # Re-find + foreach ($ch in $groupEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { + $giEl = $ch; break + } + } + } + + $toRemove = @() + foreach ($ch in $giEl.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $toRemove += $ch } + } + foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } + + $itemIndent = "$groupIndent`t" + + foreach ($field in $t.groupBy) { + $lines = @() + $lines += "$itemIndent<dcsset:item xsi:type=`"dcsset:GroupItemField`">" + $lines += "$itemIndent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + $lines += "$itemIndent`t<dcsset:groupType>Items</dcsset:groupType>" + $lines += "$itemIndent`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>" + $lines += "$itemIndent`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>" + $lines += "$itemIndent`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>" + $lines += "$itemIndent</dcsset:item>" + $fragXml = $lines -join "`n" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $giEl $node $null $itemIndent + } + } + + $desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' } + $script:Dirty = $true; Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc" + } + } + + "add-dataSetLink" { + foreach ($val in $values) { + $parsed = Parse-DataSetLinkShorthand $val + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + $fragXml = Build-DataSetLinkFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last dataSetLink, or before calculatedField/totalField/parameter/... + $lastLink = Find-LastElement $root "dataSetLink" $schNs + if ($lastLink) { + $refNode = $lastLink.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)" + if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" } + $script:Dirty = $true; Write-Host "[OK] DataSetLink `"$desc`" added" + } + } + + "add-dataSet" { + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + $parsed = Parse-DataSetShorthand $Value + $parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir + + # Auto-name if empty + if (-not $parsed.name) { + $count = 0 + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'dataSet' -and $ch.NamespaceURI -eq $schNs) { $count++ } + } + $parsed.name = "НаборДанных$($count + 1)" + } + + # Duplicate check + $existing = Find-ElementByChildValue $root "dataSet" "name" $parsed.name $schNs + if ($existing) { + Write-Host "[WARN] DataSet `"$($parsed.name)`" already exists — skipped" + } else { + # Get dataSource name from first existing <dataSource> + $dsSourceEl = Find-FirstElement $root @("dataSource") $schNs + $dsSourceName = "ИсточникДанных1" + if ($dsSourceEl) { + $nameEl = Find-FirstElement $dsSourceEl @("name") $schNs + if ($nameEl) { $dsSourceName = $nameEl.InnerText.Trim() } + } + $parsed["dataSource"] = $dsSourceName + + $fragXml = Build-DataSetQueryFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last <dataSet>, or after <dataSource> if none + $lastDS = Find-LastElement $root "dataSet" $schNs + if ($lastDS) { + $refNode = $lastDS.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("dataSetLink","calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)" + } + } + + "add-variant" { + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + foreach ($val in $values) { + $parsed = Parse-VariantShorthand $val + + # Duplicate check — search for settingsVariant with matching dcsset:name + $isDup = $false + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'settingsVariant' -and $ch.NamespaceURI -eq $schNs) { + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs -and $gc.InnerText -eq $parsed.name) { + $isDup = $true; break + } + } + if ($isDup) { break } + } + } + if ($isDup) { + Write-Host "[WARN] Variant `"$($parsed.name)`" already exists — skipped" + continue + } + + $fragXml = Build-VariantFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last <settingsVariant> + $lastSV = Find-LastElement $root "settingsVariant" $schNs + if ($lastSV) { + $refNode = $lastSV.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = $null + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added" + } + } + + "add-conditionalAppearance" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-ConditionalAppearanceShorthand $val + + $caEl = Ensure-SettingsChild $settings "conditionalAppearance" @("outputParameters","order","filter","selection") + $caIndent = Get-ContainerChildIndent $caEl + + $fragXml = Build-ConditionalAppearanceItemFragment -parsed $parsed -indent $caIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $caEl $node $null $caIndent + } + + $desc = "$($parsed.param) = $($parsed.value)" + if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" } + if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" } + $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`"" + } + } + + "clear-selection" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + Clear-ContainerChildren $selection + $script:Dirty = $true; Write-Host "[OK] Selection cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No selection section in variant `"$varName`"" + } + } + + "clear-order" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $orderEl = Find-FirstElement $settings @("order") $setNs + if ($orderEl) { + Clear-ContainerChildren $orderEl + $script:Dirty = $true; Write-Host "[OK] Order cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No order section in variant `"$varName`"" + } + } + + "clear-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $filterEl = Find-FirstElement $settings @("filter") $setNs + if ($filterEl) { + Clear-ContainerChildren $filterEl + $script:Dirty = $true; Write-Host "[OK] Filter cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No filter section in variant `"$varName`"" + } + } + + "clear-conditionalAppearance" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $caEl = Find-FirstElement $settings @("conditionalAppearance") $setNs + if ($caEl) { + Clear-ContainerChildren $caEl + $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No conditionalAppearance section in variant `"$varName`"" + } + } + + "modify-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-FilterShorthand $val + + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + Write-Host "[WARN] No filter section in variant `"$varName`"" + continue + } + + $filterItem = Find-ElementByChildValue $filterEl "item" "left" $parsed.field $setNs + if (-not $filterItem) { + Write-Host "[WARN] Filter for `"$($parsed.field)`" not found in variant `"$varName`"" + continue + } + + $itemIndent = Get-ChildIndent $filterItem + + # Update comparisonType + Set-OrCreateChildElement $filterItem "comparisonType" $setNs $parsed.op $itemIndent + + # Update right value + if ($null -ne $parsed.value) { + $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } + Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent + } + + # Update use (only when explicitly set via @off / @on) + if ($parsed.use -eq $false) { + Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent + } elseif ($parsed.use -eq $true) { + # @on: remove existing use=false if any + $useEl = $null + foreach ($ch in $filterItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) { + $useEl = $ch; break + } + } + if ($useEl -and $useEl.InnerText -eq 'false') { + Remove-NodeWithWhitespace $useEl + } + } + + # Update viewMode + if ($parsed.viewMode) { + Set-OrCreateChildElement $filterItem "viewMode" $setNs $parsed.viewMode $itemIndent + } + + # Update userSettingID + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent + } + + $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`"" + } + } + + "modify-dataParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-DataParamShorthand $val + + $dpEl = Find-FirstElement $settings @("dataParameters") $setNs + if (-not $dpEl) { + Write-Host "[WARN] No dataParameters section in variant `"$varName`"" + continue + } + + $dpItem = Find-ElementByChildValue $dpEl "item" "parameter" $parsed.parameter $corNs + if (-not $dpItem) { + Write-Host "[WARN] DataParameter `"$($parsed.parameter)`" not found in variant `"$varName`"" + continue + } + + $itemIndent = Get-ChildIndent $dpItem + + # Update value + if ($null -ne $parsed.value) { + # Remove existing value element first + $existingVal = $null + foreach ($ch in $dpItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $corNs) { + $existingVal = $ch; break + } + } + if ($existingVal) { + Remove-NodeWithWhitespace $existingVal + } + + # Build new value fragment + $valLines = @() + if ($parsed.value -is [hashtable] -and $parsed.value.variant) { + $valLines += "$itemIndent<dcscor:value xsi:type=`"v8:StandardPeriod`">" + $valLines += "$itemIndent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>" + $valLines += "$itemIndent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + $valLines += "$itemIndent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + $valLines += "$itemIndent</dcscor:value>" + } elseif (Test-EmptyValue $parsed.value) { + $valLines += "$itemIndent<dcscor:value xsi:nil=`"true`"/>" + } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { + $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { + $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } else { + $valLines += "$itemIndent<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($parsed.value)")</dcscor:value>" + } + $valXml = $valLines -join "`n" + $valNodes = Import-Fragment $xmlDoc $valXml + foreach ($node in $valNodes) { + Insert-BeforeElement $dpItem $node $null $itemIndent + } + } + + # Update use (only when explicitly set via @off / @on) + if ($parsed.use -eq $false) { + Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent + } elseif ($parsed.use -eq $true) { + # @on: remove existing use=false if any + $useEl = $null + foreach ($ch in $dpItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) { + $useEl = $ch; break + } + } + if ($useEl -and $useEl.InnerText -eq 'false') { + Remove-NodeWithWhitespace $useEl + } + } + + # Update viewMode + if ($parsed.viewMode) { + Set-OrCreateChildElement $dpItem "viewMode" $setNs $parsed.viewMode $itemIndent + } + + # Update userSettingID + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent + } + + $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`"" + } + } + + "modify-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $parsed = Parse-FieldShorthand $val + $fieldName = $parsed.dataPath + + # Find existing field + $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs + if (-not $fieldEl) { + Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" + continue + } + + # Read existing properties + $existing = Read-FieldProperties $fieldEl + + # Merge: parsed overrides existing for non-empty values + $merged = @{ + dataPath = $existing.dataPath + field = $existing.field + title = if ($parsed.title) { $parsed.title } else { $existing.title } + type = if ($parsed.type) { $parsed.type } else { $existing.type } + roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles } + restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict } + # Preserve raw <valueType> only when user did NOT override type via shorthand — + # otherwise the override path rebuilds valueType from $parsed.type. + rawValueType = if ($parsed.type) { $null } else { $existing._rawValueType } + # Preserve raw multi-lang title; pass existing ru content for change detection. + _rawTitle = $existing._rawTitle + _existingTitleRu = $existing.title + # Pass-through unknown children (e.g. <editFormat>, <appearance>, custom extensions). + _unknownChildren = $existing._unknownChildren + } + + # Remember position (NextSibling after whitespace) + $nextSib = $fieldEl.NextSibling + while ($nextSib -and ($nextSib.NodeType -eq 'Whitespace' -or $nextSib.NodeType -eq 'SignificantWhitespace')) { + $nextSib = $nextSib.NextSibling + } + + # Remove old field + $childIndent = Get-ChildIndent $dsNode + Remove-NodeWithWhitespace $fieldEl + + # Build new field fragment with merged data + $fragXml = Build-FieldFragment -parsed $merged -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert at saved position + foreach ($node in $nodes) { + Insert-BeforeElement $dsNode $node $nextSib $childIndent + } + + $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`"" + } + } + + "set-field-role" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + # Parse shorthand: "dataPath [@flag ...] [kv=value ...]" + $s = $val.Trim() + + # Extract @flags + $flags = @() + $flagMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $flagMatches) { $flags += $m.Groups[1].Value } + $s = [regex]::Replace($s, '\s*@\w+', '').Trim() + + # Extract kv=value (value is non-whitespace) + $kv = [ordered]@{} + $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') + foreach ($m in $kvMatches) { $kv[$m.Groups[1].Value] = $m.Groups[2].Value } + $s = [regex]::Replace($s, '\s*\w+=\S+', '').Trim() + + $dataPath = $s + if (-not $dataPath) { + Write-Host "[WARN] set-field-role: empty dataPath in `"$val`"" + continue + } + + $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $dataPath $schNs + if (-not $fieldEl) { + Write-Host "[WARN] Field `"$dataPath`" not found in dataset `"$dsName`"" + continue + } + + $fieldIndent = Get-ChildIndent $fieldEl + + # Remove existing <role> — but first capture OuterXml of any sub-children that + # Build-RoleXml won't re-emit (e.g. <dcscom:addition>, <dcscom:groupFields>, + # custom extension elements). Preserved across rebuild. + $oldRole = $null + foreach ($ch in $fieldEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'role' -and $ch.NamespaceURI -eq $schNs) { $oldRole = $ch; break } + } + $knownRoleChildren = @('periodNumber','periodType','dimension','ignoreNullsInGroups','balance','account','accountTypeExpression','additionType','addition') + $preservedRoleChildren = @() + if ($oldRole) { + foreach ($gc in $oldRole.ChildNodes) { + if ($gc.NodeType -ne 'Element') { continue } + if ($knownRoleChildren -contains $gc.LocalName) { continue } + # kv keys override the same-named sub-element on rebuild — don't preserve + # what the user explicitly set. + if ($kv.Contains($gc.LocalName)) { continue } + $raw = $gc.OuterXml + $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') + $preservedRoleChildren += $raw + } + Remove-NodeWithWhitespace $oldRole + } + + # Empty spec — remove only + if ($flags.Count -eq 0 -and $kv.Count -eq 0) { + $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role cleared" + continue + } + + # Build new <role> + $lines = @() + $lines += "$fieldIndent<role>" + foreach ($flag in $flags) { + if ($flag -eq 'period') { + $lines += "$fieldIndent`t<dcscom:periodNumber>1</dcscom:periodNumber>" + $lines += "$fieldIndent`t<dcscom:periodType>Main</dcscom:periodType>" + } else { + $lines += "$fieldIndent`t<dcscom:$flag>true</dcscom:$flag>" + } + } + foreach ($k in $kv.Keys) { + $lines += "$fieldIndent`t<dcscom:$k>$(Esc-Xml $kv[$k])</dcscom:$k>" + } + foreach ($raw in $preservedRoleChildren) { + $lines += "$fieldIndent`t" + $raw + } + $lines += "$fieldIndent</role>" + $fragXml = $lines -join "`n" + + # Insert before <valueType>, else before <inputParameters>, else at end + $refNode = $null + foreach ($ch in $fieldEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -in @('valueType','inputParameters') -and $ch.NamespaceURI -eq $schNs) { $refNode = $ch; break } + } + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $fieldEl $node $refNode $fieldIndent + } + + $desc = @() + if ($flags.Count -gt 0) { $desc += ($flags | ForEach-Object { "@$_" }) -join ' ' } + if ($kv.Count -gt 0) { $desc += ($kv.Keys | ForEach-Object { "$_=$($kv[$_])" }) -join ' ' } + $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')" + } + } + + "remove-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $fieldName = $val.Trim() + + $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs + if (-not $fieldEl) { + Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" + continue + } + + Remove-NodeWithWhitespace $fieldEl + $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`"" + + # Also remove from selection in variant + try { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs + if ($selItem) { + Remove-NodeWithWhitespace $selItem + $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`"" + } + } + } catch { + # No variant — that's fine + } + } + } + + "remove-total" { + foreach ($val in $values) { + $dataPath = $val.Trim() + $root = $xmlDoc.DocumentElement + + $totalEl = Find-ElementByChildValue $root "totalField" "dataPath" $dataPath $schNs + if (-not $totalEl) { + Write-Host "[WARN] TotalField `"$dataPath`" not found" + continue + } + + Remove-NodeWithWhitespace $totalEl + $script:Dirty = $true; Write-Host "[OK] TotalField `"$dataPath`" removed" + } + } + + "remove-calculated-field" { + foreach ($val in $values) { + $dataPath = $val.Trim() + $root = $xmlDoc.DocumentElement + + $calcEl = Find-ElementByChildValue $root "calculatedField" "dataPath" $dataPath $schNs + if (-not $calcEl) { + Write-Host "[WARN] CalculatedField `"$dataPath`" not found" + continue + } + + Remove-NodeWithWhitespace $calcEl + $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$dataPath`" removed" + + # Also remove from selection + try { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs + if ($selItem) { + Remove-NodeWithWhitespace $selItem + $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`"" + } + } + } catch { } + } + } + + "remove-parameter" { + foreach ($val in $values) { + $paramName = $val.Trim() + $root = $xmlDoc.DocumentElement + + $paramEl = Find-ElementByChildValue $root "parameter" "name" $paramName $schNs + if (-not $paramEl) { + Write-Host "[WARN] Parameter `"$paramName`" not found" + continue + } + + Remove-NodeWithWhitespace $paramEl + $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`" removed" + } + } + + "remove-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $fieldName = $val.Trim() + + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + Write-Host "[WARN] No filter section in variant `"$varName`"" + continue + } + + $filterItem = Find-ElementByChildValue $filterEl "item" "left" $fieldName $setNs + if (-not $filterItem) { + Write-Host "[WARN] Filter for `"$fieldName`" not found in variant `"$varName`"" + continue + } + + Remove-NodeWithWhitespace $filterItem + $script:Dirty = $true; Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`"" + } + } + + "add-drilldown" { + # String-based manipulation — templates use dcsat namespace with inline xmlns + $rawText = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) + $nl = "`r`n" + $dcsatNsDecl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"' + + # Find all outer <template> blocks by nesting-aware scan + $tplStarts = [System.Collections.ArrayList]::new() + $nameRegex = [regex]'<template>\s*<name>([^<]+)</name>' + foreach ($m in $nameRegex.Matches($rawText)) { + [void]$tplStarts.Add(@{ pos = $m.Index; name = $m.Groups[1].Value }) + } + + # For each start, find closing </template> at nesting depth 0 + $tplBlocks = [System.Collections.ArrayList]::new() + foreach ($ts in $tplStarts) { + $depth = 1 + $scanPos = $ts.pos + 10 # skip past opening <template> + while ($depth -gt 0 -and $scanPos -lt $rawText.Length) { + $nextOpen = $rawText.IndexOf("<template", $scanPos) + $nextClose = $rawText.IndexOf("</template>", $scanPos) + if ($nextClose -lt 0) { break } + if ($nextOpen -ge 0 -and $nextOpen -lt $nextClose) { + $depth++ + $scanPos = $nextOpen + 10 + } else { + $depth-- + if ($depth -eq 0) { + $endPos = $nextClose + "</template>".Length + [void]$tplBlocks.Add(@{ name = $ts.name; start = $ts.pos; text = $rawText.Substring($ts.pos, $endPos - $ts.pos) }) + } + $scanPos = $nextClose + 11 + } + } + } + + if ($tplBlocks.Count -eq 0) { + Write-Host "[WARN] No named templates found in schema" + } + + # Collect all insertions as (position, text) — apply in reverse order + $insertions = [System.Collections.ArrayList]::new() + + foreach ($tplBlock in $tplBlocks) { + $tplName = $tplBlock.name + $tplText = $tplBlock.text + $tplStart = $tplBlock.start + + # Build map: expression → paramName from ExpressionAreaTemplateParameter + $exprMap = @{} + $exprRegex = [regex]'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*<dcsat:name>([^<]+)</dcsat:name>\s*<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>' + foreach ($em in $exprRegex.Matches($tplText)) { + $pName = $em.Groups[1].Value + $pExpr = $em.Groups[2].Value + $exprMap[$pExpr] = $pName + } + + foreach ($resource in $values) { + $drillName = "Расшифровка_$resource" + + # Idempotency: check if already exists + if ($tplText.Contains($drillName)) { + Write-Host "[INFO] $drillName already exists in $tplName — skipped" + continue + } + + # Find ExpressionAreaTemplateParameter by expression + $paramName = $null + if ($exprMap.ContainsKey($resource)) { + $paramName = $exprMap[$resource] + } else { + Write-Host "[WARN] Expression `"$resource`" not found in template $tplName — skipped" + continue + } + + $cellCount = 0 + + # Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template + $lastParamEndTag = "</parameter>" + $lastParamPos = $tplText.LastIndexOf($lastParamEndTag) + if ($lastParamPos -ge 0) { + $insertPos = $tplStart + $lastParamPos + $lastParamEndTag.Length + # Detect indent from context + $prevNewline = $tplText.LastIndexOf("`n", $lastParamPos) + $indent = "`t`t" + if ($prevNewline -ge 0) { + $lineStart = $prevNewline + 1 + $indentMatch = [regex]::Match($tplText.Substring($lineStart), '^(\s*)') + if ($indentMatch.Success) { $indent = $indentMatch.Groups[1].Value } + } + $detailsXml = "$nl$indent<parameter $dcsatNsDecl xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" + + "$nl$indent`t<dcsat:name>$drillName</dcsat:name>" + + "$nl$indent`t<dcsat:fieldExpression>" + + "$nl$indent`t`t<dcsat:field>ИмяРесурса</dcsat:field>" + + "$nl$indent`t`t<dcsat:expression>`"$resource`"</dcsat:expression>" + + "$nl$indent`t</dcsat:fieldExpression>" + + "$nl$indent`t<dcsat:mainAction>DrillDown</dcsat:mainAction>" + + "$nl$indent</parameter>" + [void]$insertions.Add(@{ pos = $insertPos; text = $detailsXml }) + } + + # Step 2: Insert appearance binding in cells referencing this parameter + $cellTag = '<dcsat:value xsi:type="dcscor:Parameter">' + $paramName + '</dcsat:value>' + $searchStart = 0 + while (($cellIdx = $tplText.IndexOf($cellTag, $searchStart)) -ge 0) { + $cellEnd = $tplText.IndexOf("</dcsat:tableCell>", $cellIdx) + if ($cellEnd -lt 0) { break } + $appEnd = $tplText.LastIndexOf("</dcsat:appearance>", $cellEnd) + if ($appEnd -lt $cellIdx) { $searchStart = $cellEnd + 1; continue } + + # Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance> + $appPrevNl = $tplText.LastIndexOf("`n", $appEnd) + $appIndent = "`t`t`t`t`t`t" + if ($appPrevNl -ge 0) { + $appLineStart = $appPrevNl + 1 + $appIndentMatch = [regex]::Match($tplText.Substring($appLineStart), '^(\s*)') + if ($appIndentMatch.Success) { $appIndent = $appIndentMatch.Groups[1].Value } + } + $itemIndent = $appIndent + "`t" + $appearanceXml = "$itemIndent<dcscor:item>$nl" + + "$itemIndent`t<dcscor:parameter>Расшифровка</dcscor:parameter>$nl" + + "$itemIndent`t<dcscor:value xsi:type=`"dcscor:Parameter`">$drillName</dcscor:value>$nl" + + "$itemIndent</dcscor:item>$nl" + # Insert after \n (before indent of closing tag), not before the tag itself + $insertAt = if ($appPrevNl -ge 0) { $tplStart + $appPrevNl + 1 } else { $tplStart + $appEnd } + [void]$insertions.Add(@{ pos = $insertAt; text = $appearanceXml }) + $cellCount++ + $searchStart = $cellEnd + 1 + } + + $script:Dirty = $true; Write-Host "[OK] $drillName → $tplName (param + $cellCount cell(s))" + } + } + + # Apply insertions in reverse order to preserve offsets. + # For same position: reverse insertion order so first resource ends up first in file. + $idx = 0; foreach ($ins in $insertions) { $ins.seq = $idx; $idx++ } + $sorted = $insertions | Sort-Object { $_.pos }, { $_.seq } -Descending + foreach ($ins in $sorted) { + $rawText = $rawText.Insert($ins.pos, $ins.text) + } + + # Write directly — skip DOM save + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($resolvedPath, $rawText, $enc) + $script:Dirty = $true; Write-Host "[OK] Saved $resolvedPath" + exit 0 + } +} + +# --- 9. Save --- + +if (-not $script:Dirty) { + Write-Host "[INFO] No changes -- file untouched" + exit 0 +} + +$content = $xmlDoc.OuterXml +$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8' + +# Format-preserve post-processing: +# (1) restore the original raw <DataCompositionSchema ...> opening tag — DOM collapses +# multi-line xmlns declarations into one line. +if ($script:RawRootOpening) { + $content = [regex]::Replace($content, '<DataCompositionSchema\b[^>]*>', { param($m) $script:RawRootOpening }) +} + +# (2) normalize self-closing tags: `.NET XmlDocument` adds a space before `/>` +# (`<foo bar="x" />`) but 1C-Designer writes `<foo bar="x"/>`. Strip the space. +$content = [regex]::Replace($content, '(?<=\S) />', '/>') + +# (3) normalize line endings to match source — operations may mix LF (from new +# fragments) with whatever the source used (CRLF on Windows, LF on Linux/git). +if ($script:LineEnding -eq "`r`n") { + $content = $content -replace '(?<!\r)\n', "`r`n" +} else { + $content = $content -replace "`r`n", "`n" +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $content, $enc) + +Write-Host "[OK] Saved $resolvedPath" diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 962e3b15..6c01d524 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,80 +1,80 @@ ---- -name: skd-info -description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты. Используй для понимания отчёта — источник данных (запрос), доступные поля, параметры -argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>] [-Raw] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /skd-info — Анализ схемы компоновки данных - -Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | -| `Mode` | Режим анализа (по умолчанию `overview`) | -| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) | -| `Batch` | Номер пакета запроса, 0 = все (только query) | -| `Raw` | (только query) сырой текст запроса целиком, без заголовков/оглавления/разделителей пакетов. Для выгрузки в `.sql` и возврата через `skd-edit set-query @file` | -| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк; `-Raw` не усекается) | -| `OutFile` | Записать результат в файл (UTF-8 BOM) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -TemplatePath "<путь>" -``` - -С указанием режима: -```powershell -... -Mode query -Name НоменклатураСЦенами -... -Mode query -Name ДанныеТ13 -Batch 3 -... -Mode query -Name ДанныеТ13 -Raw -OutFile query.sql -... -Mode fields -Name КадастроваяСтоимость -... -Mode calculated -Name КоэффициентКи -... -Mode resources -Name СуммаНалога -... -Mode trace -Name "Коэффициент Ки" -... -Mode variant -Name 1 -... -Mode templates -... -Mode templates -Name ВидНалоговойБазы -``` - -## Режимы - -| Режим | Без `-Name` | С `-Name` | -|-------|-------------|-----------| -| `overview` | Навигационная карта схемы + подсказки Next | — | -| `query` | — | Текст запроса набора (с оглавлением батчей); `-Raw` — чистая выгрузка для правки | -| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат | -| `links` | Все связи наборов | — | -| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения | -| `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам | -| `params` | Таблица параметров: тип, значение, видимость | — | -| `variant` | Список вариантов | Структура группировок + фильтры + вывод | -| `templates` | Карта привязок шаблонов (field/group) | Содержимое шаблона: строки, ячейки, выражения | -| `trace` | — | Полная цепочка: набор → вычисление → ресурс | -| `full` | Полная сводка: overview + query + fields + resources + params + variant | — | - -Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента. Режим `full` объединяет 6 ключевых режимов в один вызов. - -## Типичный workflow - -1. `overview` — понять структуру, увидеть подсказки -2. `trace -Name <поле>` — узнать как считается колонка отчёта (от заголовка до запроса за один вызов) -3. `query -Name <набор>` — посмотреть текст SQL-запроса -4. `variant -Name <N>` — посмотреть группировки и фильтры варианта - -Переработка запроса (round-trip): `query -Name <набор> -Raw -OutFile q.sql` → -правка `q.sql` → `/skd-edit <tpl> -Operation set-query -Value "@q.sql"`. Флаг -`-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат -точны (включая многопакетные запросы с временными таблицами). - -## Верификация - -``` -/skd-info <path> — overview (точка входа) -/skd-info <path> -Mode trace -Name <field> — трассировка поля -``` +--- +name: skd-info +description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты. Используй для понимания отчёта — источник данных (запрос), доступные поля, параметры +argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>] [-Raw] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /skd-info — Анализ схемы компоновки данных + +Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | +| `Mode` | Режим анализа (по умолчанию `overview`) | +| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) | +| `Batch` | Номер пакета запроса, 0 = все (только query) | +| `Raw` | (только query) сырой текст запроса целиком, без заголовков/оглавления/разделителей пакетов. Для выгрузки в `.sql` и возврата через `skd-edit set-query @file` | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк; `-Raw` не усекается) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -TemplatePath "<путь>" +``` + +С указанием режима: +```powershell +... -Mode query -Name НоменклатураСЦенами +... -Mode query -Name ДанныеТ13 -Batch 3 +... -Mode query -Name ДанныеТ13 -Raw -OutFile query.sql +... -Mode fields -Name КадастроваяСтоимость +... -Mode calculated -Name КоэффициентКи +... -Mode resources -Name СуммаНалога +... -Mode trace -Name "Коэффициент Ки" +... -Mode variant -Name 1 +... -Mode templates +... -Mode templates -Name ВидНалоговойБазы +``` + +## Режимы + +| Режим | Без `-Name` | С `-Name` | +|-------|-------------|-----------| +| `overview` | Навигационная карта схемы + подсказки Next | — | +| `query` | — | Текст запроса набора (с оглавлением батчей); `-Raw` — чистая выгрузка для правки | +| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат | +| `links` | Все связи наборов | — | +| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения | +| `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам | +| `params` | Таблица параметров: тип, значение, видимость | — | +| `variant` | Список вариантов | Структура группировок + фильтры + вывод | +| `templates` | Карта привязок шаблонов (field/group) | Содержимое шаблона: строки, ячейки, выражения | +| `trace` | — | Полная цепочка: набор → вычисление → ресурс | +| `full` | Полная сводка: overview + query + fields + resources + params + variant | — | + +Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента. Режим `full` объединяет 6 ключевых режимов в один вызов. + +## Типичный workflow + +1. `overview` — понять структуру, увидеть подсказки +2. `trace -Name <поле>` — узнать как считается колонка отчёта (от заголовка до запроса за один вызов) +3. `query -Name <набор>` — посмотреть текст SQL-запроса +4. `variant -Name <N>` — посмотреть группировки и фильтры варианта + +Переработка запроса (round-trip): `query -Name <набор> -Raw -OutFile q.sql` → +правка `q.sql` → `/skd-edit <tpl> -Operation set-query -Value "@q.sql"`. Флаг +`-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат +точны (включая многопакетные запросы с временными таблицами). + +## Верификация + +``` +/skd-info <path> — overview (точка входа) +/skd-info <path> -Mode trace -Name <field> — трассировка поля +``` diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 8f7cf5e3..14cdefaa 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,1972 +1,1972 @@ -# skd-info v1.7 — Analyze 1C DCS structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory=$true)] - [Alias('Path')] - [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates", "full")] - [string]$Mode = "overview", - [string]$Name, - [int]$Batch = 0, - [int]$Limit = 150, - [int]$Offset = 0, - [string]$OutFile, - [switch]$Raw -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- - -$originalPath = $TemplatePath - -if (-not $TemplatePath.EndsWith(".xml")) { - $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" - if (Test-Path $candidate) { - $TemplatePath = $candidate - } -} - -# If still not a file, try resolving from object directory (Reports/X, DataProcessors/X) -if (-not (Test-Path $TemplatePath -PathType Leaf)) { - $templatesDir = Join-Path $originalPath "Templates" - if (Test-Path $templatesDir) { - $dcsTemplates = @() - foreach ($metaXml in (Get-ChildItem $templatesDir -Filter "*.xml" -File)) { - [xml]$meta = Get-Content $metaXml.FullName -Encoding UTF8 - $tt = $meta.SelectSingleNode("//*[local-name()='TemplateType']") - if ($tt -and $tt.InnerText -eq "DataCompositionSchema") { - $tplName = [System.IO.Path]::GetFileNameWithoutExtension($metaXml.Name) - $tplPath = Join-Path (Join-Path (Join-Path $templatesDir $tplName) "Ext") "Template.xml" - if (Test-Path $tplPath) { - $dcsTemplates += $tplPath - } - } - } - if ($dcsTemplates.Count -eq 1) { - $TemplatePath = $dcsTemplates[0] - $resolvedMsg = (Resolve-Path $TemplatePath).Path - $cwd = (Get-Location).Path - if ($resolvedMsg.StartsWith($cwd)) { - $resolvedMsg = $resolvedMsg.Substring($cwd.Length + 1) - } - Write-Host "[i] Resolved: $resolvedMsg" - } elseif ($dcsTemplates.Count -gt 1) { - Write-Host "Multiple DCS templates found in: $originalPath" - $cwd = (Get-Location).Path - for ($i = 0; $i -lt $dcsTemplates.Count; $i++) { - $p = (Resolve-Path $dcsTemplates[$i]).Path - if ($p.StartsWith($cwd)) { $p = $p.Substring($cwd.Length + 1) } - Write-Host " $($i+1). $p" - } - Write-Host "Specify the template path." - exit 1 - } else { - Write-Error "No DCS templates found in: $originalPath" - exit 1 - } - } -} - -if (-not (Test-Path $TemplatePath -PathType Leaf)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -$resolvedPath = (Resolve-Path $TemplatePath).Path - -# --- Load XML --- - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load($resolvedPath) - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") -$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") -$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") -$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") - -$root = $xmlDoc.DocumentElement - -# --- Helpers --- - -function Get-MLText($node) { - if (-not $node) { return "" } - $content = $node.SelectSingleNode("v8:item/v8:content", $ns) - if ($content) { return $content.InnerText } - $text = $node.InnerText.Trim() - if ($text) { return $text } - return "" -} - -function Unescape-Xml([string]$text) { - if (-not $text) { return $text } - $text = $text.Replace("&", "&") - $text = $text.Replace(">", ">") - $text = $text.Replace("<", "<") - $text = $text.Replace(""", '"') - $text = $text.Replace("'", "'") - return $text -} - -function Get-CompactType($valueTypeNode) { - if (-not $valueTypeNode) { return "" } - $types = @() - foreach ($t in $valueTypeNode.SelectNodes("v8:Type", $ns)) { - $raw = $t.InnerText - switch -Wildcard ($raw) { - "xs:string" { $types += "String" } - "xs:decimal" { $types += "Number" } - "xs:boolean" { $types += "Boolean" } - "xs:dateTime" { $types += "DateTime" } - "v8:StandardPeriod" { $types += "StandardPeriod" } - "v8:StandardBeginningDate" { $types += "StandardBeginningDate" } - "v8:AccountType" { $types += "AccountType" } - "v8:Null" { $types += "Null" } - default { - # Strip namespace prefixes like d4p1: cfg: - $clean = $raw -replace '^[a-zA-Z0-9]+:', '' - $types += $clean - } - } - } - if ($types.Count -eq 0) { return "" } - return ($types -join " | ") -} - -function Get-DataSetType($dsNode) { - $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -like "*DataSetQuery*") { return "Query" } - if ($xsiType -like "*DataSetObject*") { return "Object" } - if ($xsiType -like "*DataSetUnion*") { return "Union" } - return "Unknown" -} - -function Get-FieldCount($dsNode) { - return $dsNode.SelectNodes("s:field", $ns).Count -} - -function Get-QueryLineCount($dsNode) { - $queryNode = $dsNode.SelectSingleNode("s:query", $ns) - if (-not $queryNode) { return 0 } - $text = $queryNode.InnerText - return ($text -split "`n").Count -} - -function Get-StructureItemType($itemNode) { - $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -like "*StructureItemGroup*") { return "Group" } - if ($xsiType -like "*StructureItemTable*") { return "Table" } - if ($xsiType -like "*StructureItemChart*") { return "Chart" } - return "Unknown" -} - -function Get-GroupFields($itemNode) { - $fields = @() - foreach ($gi in $itemNode.SelectNodes("dcsset:groupItems/dcsset:item", $ns)) { - $fieldNode = $gi.SelectSingleNode("dcsset:field", $ns) - $groupType = $gi.SelectSingleNode("dcsset:groupType", $ns) - if ($fieldNode) { - $f = $fieldNode.InnerText - $gt = if ($groupType) { $groupType.InnerText } else { "" } - if ($gt -and $gt -ne "Items") { $f += "($gt)" } - $fields += $f - } - } - return $fields -} - -function Get-SelectionFields($itemNode) { - $fields = @() - foreach ($si in $itemNode.SelectNodes("dcsset:selection/dcsset:item", $ns)) { - $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -like "*SelectedItemAuto*") { - $fields += "Auto" - } elseif ($xsiType -like "*SelectedItemField*") { - $f = $si.SelectSingleNode("dcsset:field", $ns) - if ($f) { $fields += $f.InnerText } - } elseif ($xsiType -like "*SelectedItemFolder*") { - $fields += "Folder" - } - } - return $fields -} - -function Get-FilterSummary($settingsNode) { - $filters = @() - foreach ($fi in $settingsNode.SelectNodes("dcsset:filter/dcsset:item", $ns)) { - $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - - if ($xsiType -like "*FilterItemGroup*") { - $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) - $gt = if ($groupType) { $groupType.InnerText } else { "And" } - $subCount = $fi.SelectNodes("dcsset:item", $ns).Count - $filters += "[Group:$gt $subCount items]" - continue - } - - $use = $fi.SelectSingleNode("dcsset:use", $ns) - $isActive = if ($use -and $use.InnerText -eq "false") { "[ ]" } else { "[x]" } - - $left = $fi.SelectSingleNode("dcsset:left", $ns) - $comp = $fi.SelectSingleNode("dcsset:comparisonType", $ns) - $right = $fi.SelectSingleNode("dcsset:right", $ns) - $pres = $fi.SelectSingleNode("dcsset:presentation", $ns) - $userSetting = $fi.SelectSingleNode("dcsset:userSettingID", $ns) - - $leftStr = if ($left) { $left.InnerText } else { "?" } - $compStr = if ($comp) { $comp.InnerText } else { "?" } - $rightStr = "" - if ($right) { - $rightStr = " $($right.InnerText)" - } - - $presStr = "" - if ($pres) { - $pt = Get-MLText $pres - if ($pt) { $presStr = " `"$pt`"" } - } - - $userStr = "" - if ($userSetting) { $userStr = " [user]" } - - $filters += "$isActive $leftStr $compStr$rightStr$presStr$userStr" - } - return $filters -} - -function Build-StructureTree { - param($itemNode, [string]$prefix, [bool]$isLast, [System.Collections.Generic.List[string]]$outLines) - - $itemType = Get-StructureItemType $itemNode - $nameNode = $itemNode.SelectSingleNode("dcsset:name", $ns) - $itemName = if ($nameNode) { $nameNode.InnerText } else { "" } - - $groupFields = Get-GroupFields $itemNode - $groupStr = if ($groupFields.Count -gt 0) { "[" + ($groupFields -join ", ") + "]" } else { "(detail)" } - - $selFields = Get-SelectionFields $itemNode - $selStr = if ($selFields.Count -gt 0) { "Selection: " + ($selFields -join ", ") } else { "" } - - $line = "" - switch ($itemType) { - "Group" { - $line = "$itemType $groupStr" - if ($itemName) { $line = "$itemType `"$itemName`" $groupStr" } - } - "Table" { - $line = "Table" - if ($itemName) { $line = "Table `"$itemName`"" } - } - "Chart" { - $line = "Chart" - if ($itemName) { $line = "Chart `"$itemName`"" } - } - } - - $outLines.Add("$prefix$line") - if ($selStr -and $itemType -eq "Group") { - $outLines.Add("$prefix $selStr") - } - - # For Table, show columns and rows - if ($itemType -eq "Table") { - $columns = $itemNode.SelectNodes("dcsset:column", $ns) - $rows = $itemNode.SelectNodes("dcsset:row", $ns) - - foreach ($col in $columns) { - $colGroup = Get-GroupFields $col - $colGroupStr = if ($colGroup.Count -gt 0) { "[" + ($colGroup -join ", ") + "]" } else { "(detail)" } - $colSel = Get-SelectionFields $col - $colSelStr = if ($colSel.Count -gt 0) { "Selection: " + ($colSel -join ", ") } else { "" } - $connC = if ($rows.Count -gt 0) { [string][char]0x251C + [string][char]0x2500 + [string][char]0x2500 } else { [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 } - $contC = if ($rows.Count -gt 0) { [string][char]0x2502 + " " } else { " " } - $outLines.Add("$prefix$connC Columns: $colGroupStr") - if ($colSelStr) { $outLines.Add("$prefix$contC $colSelStr") } - } - - foreach ($row in $rows) { - $rowGroup = Get-GroupFields $row - $rowGroupStr = if ($rowGroup.Count -gt 0) { "[" + ($rowGroup -join ", ") + "]" } else { "(detail)" } - $rowSel = Get-SelectionFields $row - $rowSelStr = if ($rowSel.Count -gt 0) { "Selection: " + ($rowSel -join ", ") } else { "" } - $outLines.Add("$prefix" + [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 + " Rows: $rowGroupStr") - if ($rowSelStr) { $outLines.Add("$prefix $rowSelStr") } - } - } - - # Recurse into nested structure items (for Group) - if ($itemType -eq "Group") { - $children = $itemNode.SelectNodes("dcsset:item", $ns) - for ($i = 0; $i -lt $children.Count; $i++) { - $child = $children[$i] - $childType = $child.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($childType -like "*StructureItem*") { - $last = ($i -eq $children.Count - 1) - $connector = if ($last) { [string][char]0x2514 + [string][char]0x2500 + " " } else { [string][char]0x251C + [string][char]0x2500 + " " } - $continuation = if ($last) { " " } else { [string][char]0x2502 + " " } - Build-StructureTree -itemNode $child -prefix "$prefix$continuation" -isLast $last -outLines $outLines - } - } - } -} - -# --- Output collector --- - -$lines = [System.Collections.Generic.List[string]]::new() - -# Determine template name from path -$pathParts = $resolvedPath -split '[/\\]' -$templateName = $resolvedPath -for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { - if ($pathParts[$i] -eq "Ext" -and $i -ge 1) { - $templateName = $pathParts[$i - 1] - break - } -} - -$totalXmlLines = (Get-Content $resolvedPath).Count - -function Get-SupportStatusForPath([string]$targetPath) { - try { - $rp = (Resolve-Path $targetPath).Path - $elemUuid = $null - $binPath = $null - # Reads the uuid of the first metadata element in an .xml file (or $null). - function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null - } - # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). - $elemUuid = Get-RootUuid $rp - $d = [System.IO.Path]::GetDirectoryName($rp) - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $binPath) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } - } - if ($elemUuid -and $binPath) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return "не на поддержке" } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return "снято с поддержки (правки свободны)" } - if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } - if (-not $elemUuid) { return "не на поддержке" } - $u = [regex]::Escape($elemUuid.ToLower()) - $best = $null - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - if ($null -eq $best) { return "не на поддержке" } - switch ($best) { - 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } - 1 { return "редактируется с сохранением поддержки" } - 2 { return "снято с поддержки (правки свободны)" } - } - return "не на поддержке" - } catch { return "не на поддержке" } -} - -function Show-Overview { - $lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===") - $lines.Add("Поддержка: $(Get-SupportStatusForPath $TemplatePath)") - $lines.Add("") - - # Sources - $sources = @() - foreach ($ds in $root.SelectNodes("s:dataSource", $ns)) { - $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText - $dsType = $ds.SelectSingleNode("s:dataSourceType", $ns).InnerText - $sources += "$dsName ($dsType)" - } - $lines.Add("Sources: " + ($sources -join ", ")) - $lines.Add("") - - # Datasets (recursive for Union) - $lines.Add("Datasets:") - foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { - $dsType = Get-DataSetType $ds - $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText - $fieldCount = Get-FieldCount $ds - - switch ($dsType) { - "Query" { - $queryLines = Get-QueryLineCount $ds - $lines.Add(" [Query] $dsName $fieldCount fields, query $queryLines lines") - } - "Object" { - $objName = $ds.SelectSingleNode("s:objectName", $ns) - $objStr = if ($objName) { " objectName=$($objName.InnerText)" } else { "" } - $lines.Add(" [Object] $dsName$objStr $fieldCount fields") - } - "Union" { - $lines.Add(" [Union] $dsName $fieldCount fields") - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - $subType = Get-DataSetType $subDs - $subName = $subDs.SelectSingleNode("s:name", $ns) - $subNameStr = if ($subName) { $subName.InnerText } else { "?" } - $subFields = Get-FieldCount $subDs - switch ($subType) { - "Query" { - $subQueryLines = Get-QueryLineCount $subDs - $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Query] $subNameStr $subFields fields, query $subQueryLines lines") - } - "Object" { - $subObjName = $subDs.SelectSingleNode("s:objectName", $ns) - $subObjStr = if ($subObjName) { " objectName=$($subObjName.InnerText)" } else { "" } - $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Object] $subNameStr$subObjStr $subFields fields") - } - default { - $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [$subType] $subNameStr $subFields fields") - } - } - } - } - } - } - - # Links — only dataset pairs (not field-level) - $links = $root.SelectNodes("s:dataSetLink", $ns) - if ($links.Count -gt 0) { - $linkPairs = [ordered]@{} - foreach ($lnk in $links) { - $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText - $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText - $key = "$srcDs -> $dstDs" - if (-not $linkPairs.Contains($key)) { $linkPairs[$key] = 0 } - $linkPairs[$key] = $linkPairs[$key] + 1 - } - $linkStrs = @() - foreach ($key in $linkPairs.Keys) { - $cnt = $linkPairs[$key] - if ($cnt -gt 1) { $linkStrs += "$key (${cnt} fields)" } - else { $linkStrs += $key } - } - $lines.Add("Links: " + ($linkStrs -join ", ")) - } - - # Calculated fields — count only - $calcFields = $root.SelectNodes("s:calculatedField", $ns) - if ($calcFields.Count -gt 0) { - $lines.Add("Calculated: $($calcFields.Count)") - } - - # Totals — count + group flag - $totalFields = $root.SelectNodes("s:totalField", $ns) - if ($totalFields.Count -gt 0) { - $hasGrouped = $false - $uniquePaths = @{} - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $uniquePaths[$tfPath] = $true - if ($tf.SelectSingleNode("s:group", $ns)) { $hasGrouped = $true } - } - $groupNote = if ($hasGrouped) { ", with group formulas" } else { "" } - if ($uniquePaths.Count -eq $totalFields.Count) { - $lines.Add("Resources: $($totalFields.Count)$groupNote") - } else { - $lines.Add("Resources: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") - } - } - - # Templates — count with binding types - $tplDefs = $root.SelectNodes("s:template", $ns) - $fieldTpls = $root.SelectNodes("s:fieldTemplate", $ns) - $groupTpls = $root.SelectNodes("s:groupTemplate", $ns) - $groupHeaderTpls = $root.SelectNodes("s:groupHeaderTemplate", $ns) - $groupFooterTpls = $root.SelectNodes("s:groupFooterTemplate", $ns) - $totalBindings = $fieldTpls.Count + $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count - if ($tplDefs.Count -gt 0) { - $parts = @() - if ($fieldTpls.Count -gt 0) { $parts += "$($fieldTpls.Count) field" } - $grpCount = $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count - if ($grpCount -gt 0) { $parts += "$grpCount group" } - if ($parts.Count -gt 0) { - $lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)") - } else { - $lines.Add("Templates: $($tplDefs.Count) defined") - } - } - - # Parameters — split visible/hidden - $params = $root.SelectNodes("s:parameter", $ns) - if ($params.Count -gt 0) { - $visibleNames = @() - $hiddenCount = 0 - foreach ($p in $params) { - $pName = $p.SelectSingleNode("s:name", $ns).InnerText - $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) - $isHidden = ($useRestrict -and $useRestrict.InnerText -eq "true") - if ($isHidden) { $hiddenCount++ } else { $visibleNames += $pName } - } - $paramLine = "Params: $($params.Count)" - if ($hiddenCount -gt 0 -and $visibleNames.Count -gt 0) { - $paramLine += " ($($visibleNames.Count) visible, $hiddenCount hidden)" - } elseif ($hiddenCount -eq $params.Count) { - $paramLine += " (all hidden)" - } - if ($visibleNames.Count -gt 0 -and $visibleNames.Count -le 8) { - $paramLine += ": " + ($visibleNames -join ", ") - } - $lines.Add($paramLine) - } else { - $lines.Add("Params: (none)") - } - - $lines.Add("") - - # Variants - $variants = $root.SelectNodes("s:settingsVariant", $ns) - if ($variants.Count -gt 0) { - $lines.Add("Variants:") - $varIdx = 0 - foreach ($v in $variants) { - $varIdx++ - $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText - $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) - $vPresStr = "" - if ($vPres) { - $pt = Get-MLText $vPres - if ($pt) { $vPresStr = " `"$pt`"" } - } - - $settings = $v.SelectSingleNode("dcsset:settings", $ns) - $structItems = @() - if ($settings) { - foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { - $siType = Get-StructureItemType $si - $groupFields = Get-GroupFields $si - $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } - $structItems += "$siType$groupStr" - } - } - # Compact: if many identical items, show count - if ($structItems.Count -gt 3) { - $grouped = $structItems | Group-Object | Sort-Object Count -Descending - $compactParts = @() - foreach ($g in $grouped) { - if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } - else { $compactParts += $g.Name } - } - $structItems = $compactParts - } - $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } - - $filterCount = 0 - if ($settings) { - $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count - } - $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } - - $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") - } - } -} - -function Show-OverviewHints { - # Hints — suggest next commands - $lines.Add("") - $hints = @() - # Collect query dataset names for hint - $queryDsNames = @() - foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { - $dsType = Get-DataSetType $ds - if ($dsType -eq "Query") { - $queryDsNames += $ds.SelectSingleNode("s:name", $ns).InnerText - } elseif ($dsType -eq "Union") { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - if ((Get-DataSetType $subDs) -eq "Query") { - $sn = $subDs.SelectSingleNode("s:name", $ns) - if ($sn) { $queryDsNames += $sn.InnerText } - } - } - } - } - if ($queryDsNames.Count -eq 1) { - $hints += "-Mode query query text" - } elseif ($queryDsNames.Count -gt 1) { - $hints += "-Mode query -Name <ds> query text ($($queryDsNames -join ', '))" - } - $hints += "-Mode fields field tables by dataset" - $linkCount = $root.SelectNodes("s:dataSetLink", $ns).Count - if ($linkCount -gt 0) { - $hints += "-Mode links dataset connections ($linkCount)" - } - $calcCount = $root.SelectNodes("s:calculatedField", $ns).Count - $totalCount = $root.SelectNodes("s:totalField", $ns).Count - if ($calcCount -gt 0) { - $hints += "-Mode calculated calculated field expressions ($calcCount)" - } - if ($totalCount -gt 0) { - $hints += "-Mode resources resource aggregation ($totalCount)" - } - $params = $root.SelectNodes("s:parameter", $ns) - if ($params.Count -gt 0) { - $hints += "-Mode params parameter details" - } - $variants = $root.SelectNodes("s:settingsVariant", $ns) - if ($variants.Count -eq 1) { - $hints += "-Mode variant variant structure" - } elseif ($variants.Count -gt 1) { - $hints += "-Mode variant -Name <N> variant structure (1..$($variants.Count))" - } - $tplDefs = $root.SelectNodes("s:template", $ns) - if ($tplDefs.Count -gt 0) { - $hints += "-Mode templates template bindings and expressions" - } - $hints += "-Mode trace -Name <f> trace field origin (by name or title)" - $hints += "-Mode full all sections at once" - $lines.Add("Next:") - foreach ($h in $hints) { $lines.Add(" $h") } -} - -# ============================================================ -# MODE: overview -# ============================================================ -if ($Mode -eq "overview") { - Show-Overview - Show-OverviewHints -} - -function Show-Query { - # Find dataset - $dataSets = $root.SelectNodes("s:dataSet", $ns) - $targetDs = $null - - if ($Name) { - # Search by name: prefer nested Query items over parent Union - # Pass 1: search nested items first - foreach ($ds in $dataSets) { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - $subNameNode = $subDs.SelectSingleNode("s:name", $ns) - if ($subNameNode -and $subNameNode.InnerText -eq $Name) { $targetDs = $subDs; break } - } - if ($targetDs) { break } - } - # Pass 2: search top-level - if (-not $targetDs) { - foreach ($ds in $dataSets) { - $dsNameNode = $ds.SelectSingleNode("s:name", $ns) - if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break } - } - } - if (-not $targetDs) { - Write-Error "Dataset '$Name' not found" - exit 1 - } - } else { - # Take first Query dataset - foreach ($ds in $dataSets) { - $dsType = Get-DataSetType $ds - if ($dsType -eq "Query") { $targetDs = $ds; break } - if ($dsType -eq "Union") { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - if ((Get-DataSetType $subDs) -eq "Query") { $targetDs = $subDs; break } - } - if ($targetDs) { break } - } - } - if (-not $targetDs) { - Write-Error "No Query dataset found" - exit 1 - } - } - - $queryNode = $targetDs.SelectSingleNode("s:query", $ns) - if (-not $queryNode) { - # If this is a Union, list nested query datasets - $dsType = Get-DataSetType $targetDs - if ($dsType -eq "Union") { - $subNames = @() - foreach ($subDs in $targetDs.SelectNodes("s:item", $ns)) { - $sn = $subDs.SelectSingleNode("s:name", $ns) - if ($sn) { $subNames += $sn.InnerText } - } - Write-Error "Dataset '$($targetDs.SelectSingleNode("s:name", $ns).InnerText)' is a Union. Specify nested: $($subNames -join ', ')" - } else { - Write-Error "Dataset has no query element" - } - exit 1 - } - - $rawQuery = Unescape-Xml $queryNode.InnerText - - # Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip - if ($Raw) { - foreach ($ql in ($rawQuery.Trim() -split "`n")) { $lines.Add($ql.TrimEnd()) } - return - } - - $dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText - - # Split into batches - $batches = @() - $batchTexts = $rawQuery -split ';\s*\r?\n\s*/{16,}\s*\r?\n' - foreach ($bt in $batchTexts) { - $trimmed = $bt.Trim() - if ($trimmed) { $batches += $trimmed } - } - - $totalQueryLines = ($rawQuery -split "`n").Count - - if ($batches.Count -le 1) { - # Single query - $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines) ===") - $lines.Add("") - foreach ($ql in ($rawQuery.Trim() -split "`n")) { - $lines.Add($ql.TrimEnd()) - } - } else { - $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines, $($batches.Count) batches) ===") - - if ($Batch -eq 0) { - # Show TOC - $lineNum = 1 - for ($bi = 0; $bi -lt $batches.Count; $bi++) { - $batchLines = ($batches[$bi] -split "`n") - $endLine = $lineNum + $batchLines.Count - 1 - # Detect ПОМЕСТИТЬ target - $target = "" - foreach ($bl in $batchLines) { - if ($bl -match '^\s*(?:ПОМЕСТИТЬ|INTO)\s+(\S+)') { - $target = [char]0x2192 + " " + $Matches[1] - break - } - } - $lines.Add(" Batch $($bi + 1): lines $lineNum-$endLine $target") - $lineNum = $endLine + 3 # +separator - } - $lines.Add("") - - # Show all batches - for ($bi = 0; $bi -lt $batches.Count; $bi++) { - $lines.Add("--- Batch $($bi + 1) ---") - foreach ($ql in ($batches[$bi] -split "`n")) { - $lines.Add($ql.TrimEnd()) - } - $lines.Add("") - } - } else { - # Show specific batch - if ($Batch -gt $batches.Count) { - Write-Error "Batch $Batch not found (total: $($batches.Count))" - exit 1 - } - $lines.Add("") - $lines.Add("--- Batch $Batch ---") - foreach ($ql in ($batches[$Batch - 1] -split "`n")) { - $lines.Add($ql.TrimEnd()) - } - } - } -} - -# ============================================================ -# MODE: query -# ============================================================ -if ($Mode -eq "query") { - Show-Query -} - -function Show-Fields { - $dataSets = $root.SelectNodes("s:dataSet", $ns) - - function Show-DataSetFields($dsNode) { - $dsType = Get-DataSetType $dsNode - $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText - $fields = $dsNode.SelectNodes("s:field", $ns) - - $lines.Add("=== Fields: $dsNameStr [$dsType] ($($fields.Count)) ===") - $lines.Add(" dataPath title role restrict format") - - foreach ($f in $fields) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - $dpStr = if ($dp) { $dp.InnerText } else { "-" } - - $titleNode = $f.SelectSingleNode("s:title", $ns) - $titleStr = if ($titleNode) { Get-MLText $titleNode } else { "" } - if (-not $titleStr) { $titleStr = "-" } - - # Role - $role = $f.SelectSingleNode("s:role", $ns) - $roleStr = "-" - if ($role) { - $roleParts = @() - foreach ($child in $role.ChildNodes) { - if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { - $roleParts += $child.LocalName - } - } - if ($roleParts.Count -gt 0) { $roleStr = $roleParts -join "," } - } - - # UseRestriction - $restrict = $f.SelectSingleNode("s:useRestriction", $ns) - $restrictStr = "-" - if ($restrict) { - $restrictParts = @() - foreach ($child in $restrict.ChildNodes) { - if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { - $restrictParts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length)) - } - } - if ($restrictParts.Count -gt 0) { $restrictStr = $restrictParts -join "," } - } - - # Appearance format - $formatStr = "-" - $appearance = $f.SelectSingleNode("s:appearance", $ns) - if ($appearance) { - foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { - $paramNode = $appItem.SelectSingleNode("dcscor:parameter", $ns) - $valNode = $appItem.SelectSingleNode("dcscor:value", $ns) - if ($paramNode -and ($paramNode.InnerText -eq "Формат" -or $paramNode.InnerText -eq "Format") -and $valNode) { - $formatStr = $valNode.InnerText - } - } - } - - # presentationExpression - $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) - $presStr = "" - if ($presExpr) { $presStr = " presExpr" } - - $dpPad = $dpStr.PadRight(35) - $titlePad = $titleStr.PadRight(22) - $rolePad = $roleStr.PadRight(10) - $restrictPad = $restrictStr.PadRight(12) - - $lines.Add(" $dpPad $titlePad $rolePad $restrictPad $formatStr$presStr") - } - } - - if ($Name) { - # Detail for specific field by dataPath — search all datasets - $found = $false - $matchedIn = @() - - function Collect-FieldInfo($dsNode) { - $dsType = Get-DataSetType $dsNode - $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText - foreach ($f in $dsNode.SelectNodes("s:field", $ns)) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - if (-not $dp -or $dp.InnerText -ne $Name) { continue } - - $info = @{ dataset = "$dsNameStr [$dsType]" } - - $titleNode = $f.SelectSingleNode("s:title", $ns) - $info.title = if ($titleNode) { Get-MLText $titleNode } else { "" } - - # ValueType - $vt = $f.SelectSingleNode("s:valueType", $ns) - $info.type = if ($vt) { Get-CompactType $vt } else { "" } - - # Role - $role = $f.SelectSingleNode("s:role", $ns) - $roleParts = @() - if ($role) { - foreach ($child in $role.ChildNodes) { - if ($child.NodeType -ne "Element") { continue } - $txt = $child.InnerText.Trim() - if ($txt -eq "true") { - $roleParts += $child.LocalName - } elseif ($txt -eq "false") { - # skip default-false flags - } else { - $roleParts += "$($child.LocalName)=$txt" - } - } - } - $info.role = $roleParts -join ", " - - # UseRestriction - $restrict = $f.SelectSingleNode("s:useRestriction", $ns) - $restrictParts = @() - if ($restrict) { - foreach ($child in $restrict.ChildNodes) { - if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { - $restrictParts += $child.LocalName - } - } - } - $info.restrict = $restrictParts -join ", " - - # Format - $formatStr = "" - $appearance = $f.SelectSingleNode("s:appearance", $ns) - if ($appearance) { - foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { - $pn = $appItem.SelectSingleNode("dcscor:parameter", $ns) - $vn = $appItem.SelectSingleNode("dcscor:value", $ns) - if ($pn -and ($pn.InnerText -eq "Формат" -or $pn.InnerText -eq "Format") -and $vn) { - $formatStr = $vn.InnerText - } - } - } - $info.format = $formatStr - - # PresentationExpression - $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) - $info.presExpr = if ($presExpr) { $presExpr.InnerText } else { "" } - - return $info - } - return $null - } - - # Search all datasets and nested items - $fieldInfos = @() - foreach ($ds in $dataSets) { - $info = Collect-FieldInfo $ds - if ($info) { $fieldInfos += $info } - $dsType = Get-DataSetType $ds - if ($dsType -eq "Union") { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - $info = Collect-FieldInfo $subDs - if ($info) { $fieldInfos += $info } - } - } - } - - if ($fieldInfos.Count -eq 0) { - Write-Error "Field '$Name' not found in any dataset" - exit 1 - } - - # Use first match for detail (they usually share the same properties) - $first = $fieldInfos[0] - $titleStr = if ($first.title) { " `"$($first.title)`"" } else { "" } - $lines.Add("=== Field: $Name$titleStr ===") - $lines.Add("") - - # Datasets - $dsList = ($fieldInfos | ForEach-Object { $_.dataset }) -join ", " - $lines.Add("Dataset: $dsList") - - if ($first.type) { $lines.Add("Type: $($first.type)") } - if ($first.role) { $lines.Add("Role: $($first.role)") } - if ($first.restrict) { $lines.Add("Restrict: $($first.restrict)") } - if ($first.format) { $lines.Add("Format: $($first.format)") } - if ($first.presExpr) { - $lines.Add("PresentationExpression:") - foreach ($el in ($first.presExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } - } - } else { - # Compact map: field names per dataset - $lines.Add("=== Fields map ===") - - function Show-DataSetFieldMap($dsNode, $indent) { - $dsType = Get-DataSetType $dsNode - $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText - $fields = $dsNode.SelectNodes("s:field", $ns) - $fieldNames = @() - foreach ($f in $fields) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - if ($dp) { $fieldNames += $dp.InnerText } - } - $nameList = $fieldNames -join ", " - if ($nameList.Length -gt 100) { - $nameList = $nameList.Substring(0, 97) + "..." - } - $lines.Add("$indent$dsNameStr [$dsType] ($($fields.Count)): $nameList") - } - - foreach ($ds in $dataSets) { - Show-DataSetFieldMap $ds "" - $dsType = Get-DataSetType $ds - if ($dsType -eq "Union") { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - Show-DataSetFieldMap $subDs " " - } - } - } - - $lines.Add("") - $lines.Add("Use -Name <field> for details.") - } -} - -# ============================================================ -# MODE: fields -# ============================================================ -if ($Mode -eq "fields") { - Show-Fields -} - -# ============================================================ -# MODE: links -# ============================================================ -elseif ($Mode -eq "links") { - - $links = $root.SelectNodes("s:dataSetLink", $ns) - if ($links.Count -eq 0) { - $lines.Add("(no links)") - } else { - $lines.Add("=== Links ($($links.Count)) ===") - $lines.Add("") - # Group by source->dest pair - $currentPair = "" - foreach ($lnk in $links) { - $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText - $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText - $srcExpr = $lnk.SelectSingleNode("s:sourceExpression", $ns).InnerText - $dstExpr = $lnk.SelectSingleNode("s:destinationExpression", $ns).InnerText - $paramNode = $lnk.SelectSingleNode("s:parameter", $ns) - $paramListNode = $lnk.SelectSingleNode("s:parameterListAllowed", $ns) - - $pair = "$srcDs -> $dstDs" - if ($pair -ne $currentPair) { - if ($currentPair) { $lines.Add("") } - $lines.Add("$pair :") - $currentPair = $pair - } - - $paramStr = "" - if ($paramNode) { $paramStr = " param=$($paramNode.InnerText)" } - - $lines.Add(" $srcExpr -> $dstExpr$paramStr") - } - } -} - -# ============================================================ -# MODE: calculated -# ============================================================ -elseif ($Mode -eq "calculated") { - - $calcFields = $root.SelectNodes("s:calculatedField", $ns) - if ($calcFields.Count -eq 0) { - $lines.Add("(no calculated fields)") - } elseif ($Name) { - $found = $false - foreach ($cf in $calcFields) { - $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText - if ($cfPath -eq $Name) { - $lines.Add("=== Calculated: $cfPath ===") - $lines.Add("") - - $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText - $lines.Add("Expression:") - foreach ($el in ($cfExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } - - $cfTitle = $cf.SelectSingleNode("s:title", $ns) - if ($cfTitle) { - $t = Get-MLText $cfTitle - if ($t) { $lines.Add("Title: $t") } - } - - $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) - if ($cfRestrict) { - $parts = @() - foreach ($child in $cfRestrict.ChildNodes) { - if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { - $parts += $child.LocalName - } - } - if ($parts.Count -gt 0) { $lines.Add("Restrict: $($parts -join ', ')") } - } - - $found = $true - break - } - } - if (-not $found) { - Write-Error "Calculated field '$Name' not found" - exit 1 - } - } else { - # Map - $lines.Add("=== Calculated fields ($($calcFields.Count)) ===") - foreach ($cf in $calcFields) { - $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText - $cfTitle = $cf.SelectSingleNode("s:title", $ns) - $titleStr = "" - if ($cfTitle) { - $t = Get-MLText $cfTitle - if ($t -and $t -ne $cfPath) { $titleStr = " `"$t`"" } - } - $lines.Add(" $cfPath$titleStr") - } - $lines.Add("") - $lines.Add("Use -Name <field> for full expression.") - } -} - -function Show-Resources { - $totalFields = $root.SelectNodes("s:totalField", $ns) - if ($totalFields.Count -eq 0) { - $lines.Add("(no resources)") - } elseif ($Name) { - $matched = @() - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - if ($tfPath -eq $Name) { $matched += $tf } - } - if ($matched.Count -eq 0) { - Write-Error "Resource '$Name' not found" - exit 1 - } - $lines.Add("=== Resource: $Name ===") - $lines.Add("") - foreach ($tf in $matched) { - $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "(overall)" - if ($tfGroup) { $groupStr = $tfGroup.InnerText } - $lines.Add(" [$groupStr] $tfExpr") - } - } else { - # Map - $lines.Add("=== Resources ($($totalFields.Count)) ===") - $resMap = [ordered]@{} - foreach ($tf in $totalFields) { - $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $tfGroup = $tf.SelectSingleNode("s:group", $ns) - if (-not $resMap.Contains($tfPath)) { - $resMap[$tfPath] = @{ hasGroup = $false } - } - if ($tfGroup) { $resMap[$tfPath].hasGroup = $true } - } - foreach ($key in $resMap.Keys) { - $groupMark = if ($resMap[$key].hasGroup) { " *" } else { "" } - $lines.Add(" $key$groupMark") - } - $lines.Add("") - $lines.Add(" * = has group-level formulas") - $lines.Add("") - $lines.Add("Use -Name <field> for full formula.") - } -} - -# ============================================================ -# MODE: resources -# ============================================================ -if ($Mode -eq "resources") { - Show-Resources -} - -function Show-Params { - $params = $root.SelectNodes("s:parameter", $ns) - $lines.Add("=== Parameters ($($params.Count)) ===") - $lines.Add(" Name Type Default Visible Expression") - - foreach ($p in $params) { - $pName = $p.SelectSingleNode("s:name", $ns).InnerText - $pType = Get-CompactType $p.SelectSingleNode("s:valueType", $ns) - if (-not $pType) { $pType = "-" } - - # Default value - $valNode = $p.SelectSingleNode("s:value", $ns) - $valStr = "-" - if ($valNode) { - $nilAttr = $valNode.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance") - if ($nilAttr -eq "true") { - $valStr = "null" - } else { - $raw = $valNode.InnerText.Trim() - if ($raw -eq "0001-01-01T00:00:00") { - $valStr = "-" - } elseif ($raw) { - # Check for StandardPeriod variant - $variant = $valNode.SelectSingleNode("v8:variant", $ns) - if ($variant) { - $valStr = $variant.InnerText - } else { - $valStr = $raw - if ($valStr.Length -gt 15) { $valStr = $valStr.Substring(0, 12) + "..." } - } - } - } - } - - # Visibility - $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) - $visStr = "yes" - if ($useRestrict -and $useRestrict.InnerText -eq "true") { $visStr = "hidden" } - if ($useRestrict -and $useRestrict.InnerText -eq "false") { $visStr = "yes" } - - # Expression - $exprNode = $p.SelectSingleNode("s:expression", $ns) - $exprStr = "-" - if ($exprNode -and $exprNode.InnerText.Trim()) { - $exprStr = Unescape-Xml $exprNode.InnerText.Trim() - } - - # availableAsField - $availField = $p.SelectSingleNode("s:availableAsField", $ns) - $availStr = "" - if ($availField -and $availField.InnerText -eq "false") { $availStr = " [noField]" } - - $namePad = $pName.PadRight(33) - $typePad = $pType.PadRight(22) - $valPad = $valStr.PadRight(16) - $visPad = $visStr.PadRight(8) - - $lines.Add(" $namePad $typePad $valPad $visPad $exprStr$availStr") - } -} - -# ============================================================ -# MODE: params -# ============================================================ -if ($Mode -eq "params") { - Show-Params -} - -function Show-Variant { - $variants = $root.SelectNodes("s:settingsVariant", $ns) - - if (-not $Name) { - # --- Variant list (map) --- - if ($variants.Count -eq 0) { - $lines.Add("=== Variants: (none) ===") - } else { - $lines.Add("=== Variants ($($variants.Count)) ===") - $varIdx = 0 - foreach ($v in $variants) { - $varIdx++ - $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText - $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) - $vPresStr = "" - if ($vPres) { - $pt = Get-MLText $vPres - if ($pt) { $vPresStr = " `"$pt`"" } - } - - $settings = $v.SelectSingleNode("dcsset:settings", $ns) - $structItems = @() - if ($settings) { - foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { - $siType = Get-StructureItemType $si - $groupFields = Get-GroupFields $si - $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } - $structItems += "$siType$groupStr" - } - } - if ($structItems.Count -gt 3) { - $grouped = $structItems | Group-Object | Sort-Object Count -Descending - $compactParts = @() - foreach ($g in $grouped) { - if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } - else { $compactParts += $g.Name } - } - $structItems = $compactParts - } - $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } - - $filterCount = 0 - if ($settings) { - $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count - } - $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } - - # Selection fields - $selFields = @() - if ($settings) { $selFields = Get-SelectionFields $settings } - $selStr = if ($selFields.Count -gt 0) { " sel: " + ($selFields -join ", ") } else { "" } - - $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") - if ($selStr) { $lines.Add(" $selStr") } - } - } - } else { - # --- Variant detail --- - - $targetVariant = $null - $varIdx = 0 - foreach ($v in $variants) { - $varIdx++ - $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText - if ($vName -eq $Name -or "$varIdx" -eq $Name) { - $targetVariant = $v - $matchIdx = $varIdx - break - } - } - if (-not $targetVariant) { - Write-Error "Variant '$Name' not found. Use -Mode variant without -Name to see list." - exit 1 - } - - $vName = $targetVariant.SelectSingleNode("dcsset:name", $ns).InnerText - $vPres = $targetVariant.SelectSingleNode("dcsset:presentation", $ns) - $vPresStr = "" - if ($vPres) { - $pt = Get-MLText $vPres - if ($pt) { $vPresStr = " `"$pt`"" } - } - - $lines.Add("=== Variant [$matchIdx]: $vName$vPresStr ===") - - $settings = $targetVariant.SelectSingleNode("dcsset:settings", $ns) - if (-not $settings) { - $lines.Add(" (empty settings)") - } else { - # Selection at settings level - $topSel = Get-SelectionFields $settings - if ($topSel.Count -gt 0) { - $lines.Add("") - $lines.Add("Selection: " + ($topSel -join ", ")) - } - - # Structure - $structItems = $settings.SelectNodes("dcsset:item", $ns) - $hasStruct = $false - foreach ($si in $structItems) { - $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($siXsiType -like "*StructureItem*") { $hasStruct = $true; break } - } - - if ($hasStruct) { - $lines.Add("") - $lines.Add("Structure:") - foreach ($si in $structItems) { - $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($siXsiType -like "*StructureItem*") { - Build-StructureTree -itemNode $si -prefix " " -isLast $false -outLines $lines - } - } - } - - # Filter - $filters = Get-FilterSummary $settings - if ($filters.Count -gt 0) { - $lines.Add("") - $lines.Add("Filter:") - foreach ($f in $filters) { - $lines.Add(" $f") - } - } - - # Data parameters - $dataParams = $settings.SelectNodes("dcsset:dataParameters/dcsset:item", $ns) - if ($dataParams.Count -gt 0) { - $dpStrs = @() - foreach ($dp in $dataParams) { - $dpParam = $dp.SelectSingleNode("dcscor:parameter", $ns) - $dpVal = $dp.SelectSingleNode("dcscor:value", $ns) - if ($dpParam -and $dpVal) { - $dpStrs += "$($dpParam.InnerText)=`"$($dpVal.InnerText)`"" - } - } - if ($dpStrs.Count -gt 0) { - $lines.Add("") - $lines.Add("DataParams: " + ($dpStrs -join ", ")) - } - } - - # Output parameters - $outParams = $settings.SelectNodes("dcsset:outputParameters/dcscor:item", $ns) - if ($outParams.Count -gt 0) { - $opStrs = @() - foreach ($op in $outParams) { - $opParam = $op.SelectSingleNode("dcscor:parameter", $ns) - $opVal = $op.SelectSingleNode("dcscor:value", $ns) - if ($opParam -and $opVal) { - $paramName = $opParam.InnerText - $paramVal = $opVal.InnerText - # Shorten known long names - switch ($paramName) { - "МакетОформления" { $opStrs += "style=$paramVal" } - "РасположениеПолейГруппировки" { $opStrs += "groups=$paramVal" } - "ГоризонтальноеРасположениеОбщихИтогов" { $opStrs += "totalsH=$paramVal" } - "ВертикальноеРасположениеОбщихИтогов" { $opStrs += "totalsV=$paramVal" } - "ВыводитьЗаголовок" { $opStrs += "header=$paramVal" } - "ВыводитьОтбор" { $opStrs += "filter=$paramVal" } - "ВыводитьПараметрыДанных" { $opStrs += "dataParams=$paramVal" } - "РасположениеРеквизитов" { $opStrs += "attrs=$paramVal" } - default { $opStrs += "$paramName=$paramVal" } - } - } - } - if ($opStrs.Count -gt 0) { - $lines.Add("") - $lines.Add("Output: " + ($opStrs -join " ")) - } - } - } - } # end else (variant detail) -} - -# ============================================================ -# MODE: variant -# ============================================================ -if ($Mode -eq "variant") { - Show-Variant -} - -# ============================================================ -# MODE: full -# ============================================================ -elseif ($Mode -eq "full") { - Show-Overview - $lines.Add(""); $lines.Add("--- query ---"); $lines.Add("") - $hasQuery = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetQuery']", $ns).Count -gt 0 - if ($hasQuery) { - Show-Query - } else { - $objNodes = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetObject']/s:objectName", $ns) - if ($objNodes.Count -gt 0) { - $names = @(); foreach ($n in $objNodes) { $names += $n.InnerText } - $lines.Add("(no query datasets; external datasets: $($names -join ', '))") - } else { - $lines.Add("(no query datasets)") - } - } - $lines.Add(""); $lines.Add("--- fields ---"); $lines.Add("") - Show-Fields - $lines.Add(""); $lines.Add("--- resources ---"); $lines.Add("") - Show-Resources - $lines.Add(""); $lines.Add("--- params ---"); $lines.Add("") - Show-Params - $lines.Add(""); $lines.Add("--- variant ---"); $lines.Add("") - Show-Variant -} - -# ============================================================ -# MODE: trace -# ============================================================ -elseif ($Mode -eq "trace") { - - if (-not $Name) { - Write-Error "Trace mode requires -Name <field_name_or_title>" - exit 1 - } - - # --- Build field index --- - - $dsFields = @{} # dataPath -> @{ datasets=@(); title="" } - $calcFields = @{} # dataPath -> @{ expression=""; title="" } - $resFields = @{} # dataPath -> @(@{ expression=""; group="" }) - $titleMap = @{} # title -> dataPath - - # Scan dataset fields (including nested Union items) - $dataSets = $root.SelectNodes("s:dataSet", $ns) - foreach ($ds in $dataSets) { - $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText - $dsType = Get-DataSetType $ds - - foreach ($f in $ds.SelectNodes("s:field", $ns)) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - if (-not $dp) { continue } - $dpStr = $dp.InnerText - if (-not $dsFields.ContainsKey($dpStr)) { - $dsFields[$dpStr] = @{ datasets = @(); title = "" } - } - $dsFields[$dpStr].datasets += "$dsName [$dsType]" - $titleNode = $f.SelectSingleNode("s:title", $ns) - if ($titleNode) { - $t = Get-MLText $titleNode - if ($t) { - if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } - if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } - } - } - } - - if ($dsType -eq "Union") { - foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { - $subName = $subDs.SelectSingleNode("s:name", $ns).InnerText - $subType = Get-DataSetType $subDs - foreach ($f in $subDs.SelectNodes("s:field", $ns)) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - if (-not $dp) { continue } - $dpStr = $dp.InnerText - if (-not $dsFields.ContainsKey($dpStr)) { - $dsFields[$dpStr] = @{ datasets = @(); title = "" } - } - $dsFields[$dpStr].datasets += "$subName [$subType]" - $titleNode = $f.SelectSingleNode("s:title", $ns) - if ($titleNode) { - $t = Get-MLText $titleNode - if ($t) { - if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } - if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } - } - } - } - } - } - } - - # Scan calculated fields - foreach ($cf in $root.SelectNodes("s:calculatedField", $ns)) { - $dpStr = $cf.SelectSingleNode("s:dataPath", $ns).InnerText - $expr = $cf.SelectSingleNode("s:expression", $ns).InnerText - $cfTitle = $cf.SelectSingleNode("s:title", $ns) - $t = "" - if ($cfTitle) { $t = Get-MLText $cfTitle } - $calcFields[$dpStr] = @{ expression = $expr; title = $t } - if ($t -and -not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } - } - - # Scan resources - foreach ($tf in $root.SelectNodes("s:totalField", $ns)) { - $dpStr = $tf.SelectSingleNode("s:dataPath", $ns).InnerText - $expr = $tf.SelectSingleNode("s:expression", $ns).InnerText - $grp = $tf.SelectSingleNode("s:group", $ns) - $groupStr = "(overall)" - if ($grp) { $groupStr = $grp.InnerText } - if (-not $resFields.ContainsKey($dpStr)) { $resFields[$dpStr] = @() } - $resFields[$dpStr] += @{ expression = $expr; group = $groupStr } - } - - # --- Resolve name: try dataPath, then exact title, then substring title --- - $targetPath = $Name - $knownPaths = @() - $knownPaths += $dsFields.Keys - $knownPaths += $calcFields.Keys - $knownPaths += $resFields.Keys - $isKnown = $knownPaths -contains $Name - - if (-not $isKnown) { - if ($titleMap.ContainsKey($Name)) { - $targetPath = $titleMap[$Name] - } else { - # Substring match in titles - $matchedTitle = $null - foreach ($key in $titleMap.Keys) { - if ($key -like "*$Name*") { - $matchedTitle = $key - break - } - } - if ($matchedTitle) { - $targetPath = $titleMap[$matchedTitle] - } else { - Write-Error "Field '$Name' not found by dataPath or title" - exit 1 - } - } - } - - # --- Build output --- - $title = "" - if ($calcFields.ContainsKey($targetPath) -and $calcFields[$targetPath].title) { - $title = $calcFields[$targetPath].title - } elseif ($dsFields.ContainsKey($targetPath) -and $dsFields[$targetPath].title) { - $title = $dsFields[$targetPath].title - } - $titleStr = if ($title) { " `"$title`"" } else { "" } - - $lines.Add("=== Trace: $targetPath$titleStr ===") - $lines.Add("") - - # Dataset origin - if ($dsFields.ContainsKey($targetPath)) { - $uniqueDs = $dsFields[$targetPath].datasets | Select-Object -Unique - $lines.Add("Dataset: $($uniqueDs -join ', ')") - } else { - $lines.Add("Dataset: (schema-level only, not in dataset fields)") - } - - # Calculated field - if ($calcFields.ContainsKey($targetPath)) { - $cf = $calcFields[$targetPath] - $lines.Add("") - $lines.Add("Calculated:") - foreach ($el in ($cf.expression -split "`n")) { $lines.Add(" $($el.TrimEnd())") } - - # Extract operands: find known field names in expression - $operands = @() - $allKnown = @() - $allKnown += $dsFields.Keys - $allKnown += $calcFields.Keys - $allKnown = $allKnown | Select-Object -Unique | Where-Object { $_ -ne $targetPath } - # Sort by length descending to match longer names first - $allKnown = $allKnown | Sort-Object -Property Length -Descending - - foreach ($fieldName in $allKnown) { - $escaped = [regex]::Escape($fieldName) - if ($cf.expression -match "(?<![а-яА-ЯёЁa-zA-Z0-9_.])$escaped(?![а-яА-ЯёЁa-zA-Z0-9_.])") { - $operands += $fieldName - } - } - - if ($operands.Count -gt 0) { - $lines.Add(" Operands:") - foreach ($op in $operands) { - if ($calcFields.ContainsKey($op)) { - $lines.Add(" $op -> calculated") - } elseif ($dsFields.ContainsKey($op)) { - $opDs = ($dsFields[$op].datasets | Select-Object -Unique) -join ", " - $lines.Add(" $op -> $opDs") - } else { - $lines.Add(" $op") - } - } - } - } - - # Resource - if ($resFields.ContainsKey($targetPath)) { - $lines.Add("") - $lines.Add("Resource:") - foreach ($r in $resFields[$targetPath]) { - $lines.Add(" [$($r.group)] $($r.expression)") - } - } - - # Simple dataset field, no calc/resource - if (-not $calcFields.ContainsKey($targetPath) -and -not $resFields.ContainsKey($targetPath)) { - if ($dsFields.ContainsKey($targetPath)) { - $lines.Add("") - $lines.Add("(direct dataset field, no calculated expression or resource)") - } - } -} - -# ============================================================ -# MODE: templates -# ============================================================ -elseif ($Mode -eq "templates") { - - # --- Helper: check if expression is trivial --- - function Is-TrivialExpr([string]$paramName, [string]$expr) { - $e = $expr.Trim() - $n = $paramName.Trim() - if ($e -eq $n) { return $true } - if ($e -eq "Представление($n)") { return $true } - return $false - } - - # --- Helper: parse template content (rows/cells) --- - function Get-TemplateContent([System.Xml.XmlNode]$tplNode) { - $innerT = $tplNode.SelectSingleNode("s:template", $ns) - if (-not $innerT) { return @{ rows = 0; cells = @(); params = @(); nonTrivial = @() } } - - $rows = $innerT.SelectNodes("dcsat:item", $ns) - $rowCount = $rows.Count - $cellData = [System.Collections.ArrayList]::new() - $rowIdx = 0 - foreach ($row in $rows) { - $rowIdx++ - $rowCells = [System.Collections.ArrayList]::new() - foreach ($cell in $row.SelectNodes("dcsat:tableCell", $ns)) { - $field = $cell.SelectSingleNode("dcsat:item", $ns) - if (-not $field) { - [void]$rowCells.Add("(empty)") - continue - } - $val = $field.SelectSingleNode("dcsat:value", $ns) - if (-not $val) { - [void]$rowCells.Add("(empty)") - continue - } - $xsiType = $val.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -like "*LocalStringType*") { - $text = Get-MLText $val - if ($text) { [void]$rowCells.Add("`"$text`"") } - else { [void]$rowCells.Add("(empty)") } - } elseif ($xsiType -like "*Parameter*") { - [void]$rowCells.Add("{$($val.InnerText)}") - } else { - [void]$rowCells.Add("(?)") - } - } - [void]$cellData.Add(@{ row = $rowIdx; cells = $rowCells.ToArray() }) - } - - # Parameters - $paramNodes = $tplNode.SelectNodes("s:parameter", $ns) - $paramList = [System.Collections.ArrayList]::new() - $nonTrivialList = [System.Collections.ArrayList]::new() - foreach ($p in $paramNodes) { - $pn = $p.SelectSingleNode("dcsat:name", $ns) - $pe = $p.SelectSingleNode("dcsat:expression", $ns) - if ($pn -and $pe) { - $pName = $pn.InnerText - $pExpr = $pe.InnerText - [void]$paramList.Add(@{ name = $pName; expression = $pExpr }) - if (-not (Is-TrivialExpr $pName $pExpr)) { - [void]$nonTrivialList.Add(@{ name = $pName; expression = $pExpr }) - } - } - } - - return @{ - rows = $rowCount - cells = $cellData.ToArray() - params = $paramList.ToArray() - nonTrivial = $nonTrivialList.ToArray() - } - } - - # --- Build template name -> node index --- - $tplIndex = @{} - foreach ($t in $root.SelectNodes("s:template", $ns)) { - $tn = $t.SelectSingleNode("s:name", $ns) - if ($tn) { $tplIndex[$tn.InnerText] = $t } - } - - # --- Parse bindings --- - # Group bindings: groupTemplate + groupHeaderTemplate - $groupBindings = [ordered]@{} # groupName -> @{ bindings = @(@{type; tplName; tplNode}) } - - foreach ($gt in $root.SelectNodes("s:groupTemplate", $ns)) { - $gn = $gt.SelectSingleNode("s:groupName", $ns) - $gf = $gt.SelectSingleNode("s:groupField", $ns) - $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } - $tt = $gt.SelectSingleNode("s:templateType", $ns) - $tn = $gt.SelectSingleNode("s:template", $ns) - $ttStr = if ($tt) { $tt.InnerText } else { "-" } - $tnStr = if ($tn) { $tn.InnerText } else { "-" } - - if (-not $groupBindings.Contains($gnStr)) { - $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() - } - [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) - } - - foreach ($ght in $root.SelectNodes("s:groupHeaderTemplate", $ns)) { - $gn = $ght.SelectSingleNode("s:groupName", $ns) - $gf = $ght.SelectSingleNode("s:groupField", $ns) - $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } - $tt = $ght.SelectSingleNode("s:templateType", $ns) - $tn = $ght.SelectSingleNode("s:template", $ns) - $ttStr = if ($tt) { "GroupHeader" } else { "GroupHeader" } - $tnStr = if ($tn) { $tn.InnerText } else { "-" } - - if (-not $groupBindings.Contains($gnStr)) { - $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() - } - [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) - } - - foreach ($gft in $root.SelectNodes("s:groupFooterTemplate", $ns)) { - $gn = $gft.SelectSingleNode("s:groupName", $ns) - $gf = $gft.SelectSingleNode("s:groupField", $ns) - $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } - $tn = $gft.SelectSingleNode("s:template", $ns) - $tnStr = if ($tn) { $tn.InnerText } else { "-" } - - if (-not $groupBindings.Contains($gnStr)) { - $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() - } - [void]$groupBindings[$gnStr].Add(@{ type = "GroupFooter"; tplName = $tnStr }) - } - - # Field bindings: fieldTemplate - $fieldBindings = [ordered]@{} # fieldName -> tplName - $fieldNonTrivial = [System.Collections.ArrayList]::new() - - foreach ($ft in $root.SelectNodes("s:fieldTemplate", $ns)) { - $fn = $ft.SelectSingleNode("s:field", $ns) - $tn = $ft.SelectSingleNode("s:template", $ns) - if ($fn -and $tn) { - $fName = $fn.InnerText - $tName = $tn.InnerText - $fieldBindings[$fName] = $tName - # Check params for non-trivial expressions - if ($tplIndex.ContainsKey($tName)) { - $content = Get-TemplateContent $tplIndex[$tName] - foreach ($nt in $content.nonTrivial) { - [void]$fieldNonTrivial.Add(@{ field = $fName; template = $tName; name = $nt.name; expression = $nt.expression }) - } - } - } - } - - $totalTpl = $tplIndex.Count - $fieldCount = $fieldBindings.Count - $groupBindCount = 0 - foreach ($k in $groupBindings.Keys) { $groupBindCount += $groupBindings[$k].Count } - - if (-not $Name) { - # --- MAP mode --- - $lines.Add("=== Templates ($totalTpl defined: $fieldCount field, $groupBindCount group) ===") - - # Field bindings - if ($fieldBindings.Count -gt 0) { - $lines.Add("") - if ($fieldNonTrivial.Count -eq 0) { - $fieldNames = @($fieldBindings.Keys) - if ($fieldNames.Count -le 8) { - $lines.Add("Field bindings ($fieldCount): $($fieldNames -join ', ') (all trivial)") - } else { - $lines.Add("Field bindings ($fieldCount): (all trivial)") - $lines.Add(" $($fieldNames[0..7] -join ', '), ...") - } - } else { - $trivialCount = $fieldBindings.Count - ($fieldNonTrivial | Select-Object -ExpandProperty field -Unique).Count - $lines.Add("Field bindings ($fieldCount, $trivialCount trivial):") - foreach ($nt in $fieldNonTrivial) { - $lines.Add(" $($nt.field): $($nt.name) = $($nt.expression)") - } - } - } - - # Group bindings - if ($groupBindings.Count -gt 0) { - $lines.Add("") - $lines.Add("Group bindings ($groupBindCount):") - foreach ($gName in $groupBindings.Keys) { - $bindings = $groupBindings[$gName] - $parts = @() - foreach ($b in $bindings) { - $info = "$($b.type) -> $($b.tplName)" - if ($tplIndex.ContainsKey($b.tplName)) { - $content = Get-TemplateContent $tplIndex[$b.tplName] - # Check if any cell has content - $hasContent = $false - foreach ($r in $content.cells) { - foreach ($c in $r.cells) { - if ($c -ne "(empty)") { $hasContent = $true; break } - } - if ($hasContent) { break } - } - $info += " ($($content.rows) rows" - if ($content.params.Count -gt 0) { $info += ", $($content.params.Count) params" } - $info += ")" - if (-not $hasContent -and $content.params.Count -eq 0) { - $info += " spacer" - } - if ($content.nonTrivial.Count -gt 0) { - $ntNames = ($content.nonTrivial | ForEach-Object { $_.name }) -join ', ' - $info += " *$ntNames" - } - } - $parts += $info - } - $lines.Add(" $gName") - foreach ($p in $parts) { - $lines.Add(" $p") - } - } - } - - if ($fieldBindings.Count -gt 0 -or $groupBindings.Count -gt 0) { - $lines.Add("") - $lines.Add("Use -Name <group|field> for template details.") - } - } else { - # --- DETAIL mode --- - $found = $false - - # Check group bindings first - if ($groupBindings.Contains($Name)) { - $found = $true - $bindings = $groupBindings[$Name] - $lines.Add("=== Templates: $Name ===") - foreach ($b in $bindings) { - $lines.Add("") - $tName = $b.tplName - if (-not $tplIndex.ContainsKey($tName)) { - $lines.Add("$($b.type) -> $tName (template not found)") - continue - } - $content = Get-TemplateContent $tplIndex[$tName] - $cellCount = 0 - foreach ($r in $content.cells) { $cellCount += $r.cells.Count } - $lines.Add("$($b.type) -> $tName [$($content.rows) rows, $cellCount cells]:") - - foreach ($r in $content.cells) { - $cellStr = $r.cells -join " | " - $lines.Add(" Row $($r.row): $cellStr") - } - - if ($content.nonTrivial.Count -gt 0) { - $lines.Add(" Params:") - foreach ($nt in $content.nonTrivial) { - $lines.Add(" $($nt.name) = $($nt.expression)") - } - } - } - } - - # Check field bindings - if ($fieldBindings.Contains($Name)) { - if ($found) { $lines.Add("") } - $found = $true - $tName = $fieldBindings[$Name] - $lines.Add("=== Field template: $Name -> $tName ===") - if ($tplIndex.ContainsKey($tName)) { - $content = Get-TemplateContent $tplIndex[$tName] - $cellCount = 0 - foreach ($r in $content.cells) { $cellCount += $r.cells.Count } - $lines.Add("[$($content.rows) rows, $cellCount cells]") - - foreach ($r in $content.cells) { - $cellStr = $r.cells -join " | " - $lines.Add(" Row $($r.row): $cellStr") - } - - if ($content.nonTrivial.Count -gt 0) { - $lines.Add(" Non-trivial params:") - foreach ($nt in $content.nonTrivial) { - $lines.Add(" $($nt.name) = $($nt.expression)") - } - } else { - $lines.Add(" (all params trivial)") - } - } - } - - if (-not $found) { - Write-Error "Group or field '$Name' not found in template bindings" - exit 1 - } - } -} - -# --- Output --- - -$result = $lines.ToArray() -$totalLines = $result.Count - -# OutFile -if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - if ([System.IO.Path]::IsPathRooted($OutFile)) { - $outPath = [System.IO.Path]::GetFullPath($OutFile) - } else { - $outPath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $OutFile)) - } - [System.IO.File]::WriteAllLines($outPath, $result, $utf8Bom) - Write-Host "Written $totalLines lines to $OutFile" - exit 0 -} - -# Pagination -if ($Offset -gt 0) { - if ($Offset -ge $totalLines) { - Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." - exit 0 - } - $result = $result[$Offset..($totalLines - 1)] -} - -if (-not $Raw -and $result.Count -gt $Limit) { - $shown = $result[0..($Limit - 1)] - foreach ($l in $shown) { Write-Host $l } - Write-Host "" - Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." -} else { - foreach ($l in $result) { Write-Host $l } -} +# skd-info v1.7 — Analyze 1C DCS structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)] + [Alias('Path')] + [string]$TemplatePath, + [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates", "full")] + [string]$Mode = "overview", + [string]$Name, + [int]$Batch = 0, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile, + [switch]$Raw +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +$originalPath = $TemplatePath + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +# If still not a file, try resolving from object directory (Reports/X, DataProcessors/X) +if (-not (Test-Path $TemplatePath -PathType Leaf)) { + $templatesDir = Join-Path $originalPath "Templates" + if (Test-Path $templatesDir) { + $dcsTemplates = @() + foreach ($metaXml in (Get-ChildItem $templatesDir -Filter "*.xml" -File)) { + [xml]$meta = Get-Content $metaXml.FullName -Encoding UTF8 + $tt = $meta.SelectSingleNode("//*[local-name()='TemplateType']") + if ($tt -and $tt.InnerText -eq "DataCompositionSchema") { + $tplName = [System.IO.Path]::GetFileNameWithoutExtension($metaXml.Name) + $tplPath = Join-Path (Join-Path (Join-Path $templatesDir $tplName) "Ext") "Template.xml" + if (Test-Path $tplPath) { + $dcsTemplates += $tplPath + } + } + } + if ($dcsTemplates.Count -eq 1) { + $TemplatePath = $dcsTemplates[0] + $resolvedMsg = (Resolve-Path $TemplatePath).Path + $cwd = (Get-Location).Path + if ($resolvedMsg.StartsWith($cwd)) { + $resolvedMsg = $resolvedMsg.Substring($cwd.Length + 1) + } + Write-Host "[i] Resolved: $resolvedMsg" + } elseif ($dcsTemplates.Count -gt 1) { + Write-Host "Multiple DCS templates found in: $originalPath" + $cwd = (Get-Location).Path + for ($i = 0; $i -lt $dcsTemplates.Count; $i++) { + $p = (Resolve-Path $dcsTemplates[$i]).Path + if ($p.StartsWith($cwd)) { $p = $p.Substring($cwd.Length + 1) } + Write-Host " $($i+1). $p" + } + Write-Host "Specify the template path." + exit 1 + } else { + Write-Error "No DCS templates found in: $originalPath" + exit 1 + } + } +} + +if (-not (Test-Path $TemplatePath -PathType Leaf)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path + +# --- Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load($resolvedPath) + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") +$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") +$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") + +$root = $xmlDoc.DocumentElement + +# --- Helpers --- + +function Get-MLText($node) { + if (-not $node) { return "" } + $content = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($content) { return $content.InnerText } + $text = $node.InnerText.Trim() + if ($text) { return $text } + return "" +} + +function Unescape-Xml([string]$text) { + if (-not $text) { return $text } + $text = $text.Replace("&", "&") + $text = $text.Replace(">", ">") + $text = $text.Replace("<", "<") + $text = $text.Replace(""", '"') + $text = $text.Replace("'", "'") + return $text +} + +function Get-CompactType($valueTypeNode) { + if (-not $valueTypeNode) { return "" } + $types = @() + foreach ($t in $valueTypeNode.SelectNodes("v8:Type", $ns)) { + $raw = $t.InnerText + switch -Wildcard ($raw) { + "xs:string" { $types += "String" } + "xs:decimal" { $types += "Number" } + "xs:boolean" { $types += "Boolean" } + "xs:dateTime" { $types += "DateTime" } + "v8:StandardPeriod" { $types += "StandardPeriod" } + "v8:StandardBeginningDate" { $types += "StandardBeginningDate" } + "v8:AccountType" { $types += "AccountType" } + "v8:Null" { $types += "Null" } + default { + # Strip namespace prefixes like d4p1: cfg: + $clean = $raw -replace '^[a-zA-Z0-9]+:', '' + $types += $clean + } + } + } + if ($types.Count -eq 0) { return "" } + return ($types -join " | ") +} + +function Get-DataSetType($dsNode) { + $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*DataSetQuery*") { return "Query" } + if ($xsiType -like "*DataSetObject*") { return "Object" } + if ($xsiType -like "*DataSetUnion*") { return "Union" } + return "Unknown" +} + +function Get-FieldCount($dsNode) { + return $dsNode.SelectNodes("s:field", $ns).Count +} + +function Get-QueryLineCount($dsNode) { + $queryNode = $dsNode.SelectSingleNode("s:query", $ns) + if (-not $queryNode) { return 0 } + $text = $queryNode.InnerText + return ($text -split "`n").Count +} + +function Get-StructureItemType($itemNode) { + $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*StructureItemGroup*") { return "Group" } + if ($xsiType -like "*StructureItemTable*") { return "Table" } + if ($xsiType -like "*StructureItemChart*") { return "Chart" } + return "Unknown" +} + +function Get-GroupFields($itemNode) { + $fields = @() + foreach ($gi in $itemNode.SelectNodes("dcsset:groupItems/dcsset:item", $ns)) { + $fieldNode = $gi.SelectSingleNode("dcsset:field", $ns) + $groupType = $gi.SelectSingleNode("dcsset:groupType", $ns) + if ($fieldNode) { + $f = $fieldNode.InnerText + $gt = if ($groupType) { $groupType.InnerText } else { "" } + if ($gt -and $gt -ne "Items") { $f += "($gt)" } + $fields += $f + } + } + return $fields +} + +function Get-SelectionFields($itemNode) { + $fields = @() + foreach ($si in $itemNode.SelectNodes("dcsset:selection/dcsset:item", $ns)) { + $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*SelectedItemAuto*") { + $fields += "Auto" + } elseif ($xsiType -like "*SelectedItemField*") { + $f = $si.SelectSingleNode("dcsset:field", $ns) + if ($f) { $fields += $f.InnerText } + } elseif ($xsiType -like "*SelectedItemFolder*") { + $fields += "Folder" + } + } + return $fields +} + +function Get-FilterSummary($settingsNode) { + $filters = @() + foreach ($fi in $settingsNode.SelectNodes("dcsset:filter/dcsset:item", $ns)) { + $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + + if ($xsiType -like "*FilterItemGroup*") { + $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) + $gt = if ($groupType) { $groupType.InnerText } else { "And" } + $subCount = $fi.SelectNodes("dcsset:item", $ns).Count + $filters += "[Group:$gt $subCount items]" + continue + } + + $use = $fi.SelectSingleNode("dcsset:use", $ns) + $isActive = if ($use -and $use.InnerText -eq "false") { "[ ]" } else { "[x]" } + + $left = $fi.SelectSingleNode("dcsset:left", $ns) + $comp = $fi.SelectSingleNode("dcsset:comparisonType", $ns) + $right = $fi.SelectSingleNode("dcsset:right", $ns) + $pres = $fi.SelectSingleNode("dcsset:presentation", $ns) + $userSetting = $fi.SelectSingleNode("dcsset:userSettingID", $ns) + + $leftStr = if ($left) { $left.InnerText } else { "?" } + $compStr = if ($comp) { $comp.InnerText } else { "?" } + $rightStr = "" + if ($right) { + $rightStr = " $($right.InnerText)" + } + + $presStr = "" + if ($pres) { + $pt = Get-MLText $pres + if ($pt) { $presStr = " `"$pt`"" } + } + + $userStr = "" + if ($userSetting) { $userStr = " [user]" } + + $filters += "$isActive $leftStr $compStr$rightStr$presStr$userStr" + } + return $filters +} + +function Build-StructureTree { + param($itemNode, [string]$prefix, [bool]$isLast, [System.Collections.Generic.List[string]]$outLines) + + $itemType = Get-StructureItemType $itemNode + $nameNode = $itemNode.SelectSingleNode("dcsset:name", $ns) + $itemName = if ($nameNode) { $nameNode.InnerText } else { "" } + + $groupFields = Get-GroupFields $itemNode + $groupStr = if ($groupFields.Count -gt 0) { "[" + ($groupFields -join ", ") + "]" } else { "(detail)" } + + $selFields = Get-SelectionFields $itemNode + $selStr = if ($selFields.Count -gt 0) { "Selection: " + ($selFields -join ", ") } else { "" } + + $line = "" + switch ($itemType) { + "Group" { + $line = "$itemType $groupStr" + if ($itemName) { $line = "$itemType `"$itemName`" $groupStr" } + } + "Table" { + $line = "Table" + if ($itemName) { $line = "Table `"$itemName`"" } + } + "Chart" { + $line = "Chart" + if ($itemName) { $line = "Chart `"$itemName`"" } + } + } + + $outLines.Add("$prefix$line") + if ($selStr -and $itemType -eq "Group") { + $outLines.Add("$prefix $selStr") + } + + # For Table, show columns and rows + if ($itemType -eq "Table") { + $columns = $itemNode.SelectNodes("dcsset:column", $ns) + $rows = $itemNode.SelectNodes("dcsset:row", $ns) + + foreach ($col in $columns) { + $colGroup = Get-GroupFields $col + $colGroupStr = if ($colGroup.Count -gt 0) { "[" + ($colGroup -join ", ") + "]" } else { "(detail)" } + $colSel = Get-SelectionFields $col + $colSelStr = if ($colSel.Count -gt 0) { "Selection: " + ($colSel -join ", ") } else { "" } + $connC = if ($rows.Count -gt 0) { [string][char]0x251C + [string][char]0x2500 + [string][char]0x2500 } else { [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 } + $contC = if ($rows.Count -gt 0) { [string][char]0x2502 + " " } else { " " } + $outLines.Add("$prefix$connC Columns: $colGroupStr") + if ($colSelStr) { $outLines.Add("$prefix$contC $colSelStr") } + } + + foreach ($row in $rows) { + $rowGroup = Get-GroupFields $row + $rowGroupStr = if ($rowGroup.Count -gt 0) { "[" + ($rowGroup -join ", ") + "]" } else { "(detail)" } + $rowSel = Get-SelectionFields $row + $rowSelStr = if ($rowSel.Count -gt 0) { "Selection: " + ($rowSel -join ", ") } else { "" } + $outLines.Add("$prefix" + [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 + " Rows: $rowGroupStr") + if ($rowSelStr) { $outLines.Add("$prefix $rowSelStr") } + } + } + + # Recurse into nested structure items (for Group) + if ($itemType -eq "Group") { + $children = $itemNode.SelectNodes("dcsset:item", $ns) + for ($i = 0; $i -lt $children.Count; $i++) { + $child = $children[$i] + $childType = $child.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($childType -like "*StructureItem*") { + $last = ($i -eq $children.Count - 1) + $connector = if ($last) { [string][char]0x2514 + [string][char]0x2500 + " " } else { [string][char]0x251C + [string][char]0x2500 + " " } + $continuation = if ($last) { " " } else { [string][char]0x2502 + " " } + Build-StructureTree -itemNode $child -prefix "$prefix$continuation" -isLast $last -outLines $outLines + } + } + } +} + +# --- Output collector --- + +$lines = [System.Collections.Generic.List[string]]::new() + +# Determine template name from path +$pathParts = $resolvedPath -split '[/\\]' +$templateName = $resolvedPath +for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { + if ($pathParts[$i] -eq "Ext" -and $i -ge 1) { + $templateName = $pathParts[$i - 1] + break + } +} + +$totalXmlLines = (Get-Content $resolvedPath).Count + +function Get-SupportStatusForPath([string]$targetPath) { + try { + $rp = (Resolve-Path $targetPath).Path + $elemUuid = $null + $binPath = $null + # Reads the uuid of the first metadata element in an .xml file (or $null). + function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null + } + # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). + $elemUuid = Get-RootUuid $rp + $d = [System.IO.Path]::GetDirectoryName($rp) + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $binPath) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } + } + if ($elemUuid -and $binPath) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $elemUuid) { return "не на поддержке" } + $u = [regex]::Escape($elemUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + +function Show-Overview { + $lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===") + $lines.Add("Поддержка: $(Get-SupportStatusForPath $TemplatePath)") + $lines.Add("") + + # Sources + $sources = @() + foreach ($ds in $root.SelectNodes("s:dataSource", $ns)) { + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $dsType = $ds.SelectSingleNode("s:dataSourceType", $ns).InnerText + $sources += "$dsName ($dsType)" + } + $lines.Add("Sources: " + ($sources -join ", ")) + $lines.Add("") + + # Datasets (recursive for Union) + $lines.Add("Datasets:") + foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { + $dsType = Get-DataSetType $ds + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $fieldCount = Get-FieldCount $ds + + switch ($dsType) { + "Query" { + $queryLines = Get-QueryLineCount $ds + $lines.Add(" [Query] $dsName $fieldCount fields, query $queryLines lines") + } + "Object" { + $objName = $ds.SelectSingleNode("s:objectName", $ns) + $objStr = if ($objName) { " objectName=$($objName.InnerText)" } else { "" } + $lines.Add(" [Object] $dsName$objStr $fieldCount fields") + } + "Union" { + $lines.Add(" [Union] $dsName $fieldCount fields") + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subType = Get-DataSetType $subDs + $subName = $subDs.SelectSingleNode("s:name", $ns) + $subNameStr = if ($subName) { $subName.InnerText } else { "?" } + $subFields = Get-FieldCount $subDs + switch ($subType) { + "Query" { + $subQueryLines = Get-QueryLineCount $subDs + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Query] $subNameStr $subFields fields, query $subQueryLines lines") + } + "Object" { + $subObjName = $subDs.SelectSingleNode("s:objectName", $ns) + $subObjStr = if ($subObjName) { " objectName=$($subObjName.InnerText)" } else { "" } + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Object] $subNameStr$subObjStr $subFields fields") + } + default { + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [$subType] $subNameStr $subFields fields") + } + } + } + } + } + } + + # Links — only dataset pairs (not field-level) + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -gt 0) { + $linkPairs = [ordered]@{} + foreach ($lnk in $links) { + $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText + $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText + $key = "$srcDs -> $dstDs" + if (-not $linkPairs.Contains($key)) { $linkPairs[$key] = 0 } + $linkPairs[$key] = $linkPairs[$key] + 1 + } + $linkStrs = @() + foreach ($key in $linkPairs.Keys) { + $cnt = $linkPairs[$key] + if ($cnt -gt 1) { $linkStrs += "$key (${cnt} fields)" } + else { $linkStrs += $key } + } + $lines.Add("Links: " + ($linkStrs -join ", ")) + } + + # Calculated fields — count only + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -gt 0) { + $lines.Add("Calculated: $($calcFields.Count)") + } + + # Totals — count + group flag + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -gt 0) { + $hasGrouped = $false + $uniquePaths = @{} + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $uniquePaths[$tfPath] = $true + if ($tf.SelectSingleNode("s:group", $ns)) { $hasGrouped = $true } + } + $groupNote = if ($hasGrouped) { ", with group formulas" } else { "" } + if ($uniquePaths.Count -eq $totalFields.Count) { + $lines.Add("Resources: $($totalFields.Count)$groupNote") + } else { + $lines.Add("Resources: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") + } + } + + # Templates — count with binding types + $tplDefs = $root.SelectNodes("s:template", $ns) + $fieldTpls = $root.SelectNodes("s:fieldTemplate", $ns) + $groupTpls = $root.SelectNodes("s:groupTemplate", $ns) + $groupHeaderTpls = $root.SelectNodes("s:groupHeaderTemplate", $ns) + $groupFooterTpls = $root.SelectNodes("s:groupFooterTemplate", $ns) + $totalBindings = $fieldTpls.Count + $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count + if ($tplDefs.Count -gt 0) { + $parts = @() + if ($fieldTpls.Count -gt 0) { $parts += "$($fieldTpls.Count) field" } + $grpCount = $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count + if ($grpCount -gt 0) { $parts += "$grpCount group" } + if ($parts.Count -gt 0) { + $lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)") + } else { + $lines.Add("Templates: $($tplDefs.Count) defined") + } + } + + # Parameters — split visible/hidden + $params = $root.SelectNodes("s:parameter", $ns) + if ($params.Count -gt 0) { + $visibleNames = @() + $hiddenCount = 0 + foreach ($p in $params) { + $pName = $p.SelectSingleNode("s:name", $ns).InnerText + $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) + $isHidden = ($useRestrict -and $useRestrict.InnerText -eq "true") + if ($isHidden) { $hiddenCount++ } else { $visibleNames += $pName } + } + $paramLine = "Params: $($params.Count)" + if ($hiddenCount -gt 0 -and $visibleNames.Count -gt 0) { + $paramLine += " ($($visibleNames.Count) visible, $hiddenCount hidden)" + } elseif ($hiddenCount -eq $params.Count) { + $paramLine += " (all hidden)" + } + if ($visibleNames.Count -gt 0 -and $visibleNames.Count -le 8) { + $paramLine += ": " + ($visibleNames -join ", ") + } + $lines.Add($paramLine) + } else { + $lines.Add("Params: (none)") + } + + $lines.Add("") + + # Variants + $variants = $root.SelectNodes("s:settingsVariant", $ns) + if ($variants.Count -gt 0) { + $lines.Add("Variants:") + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + $structItems = @() + if ($settings) { + foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { + $siType = Get-StructureItemType $si + $groupFields = Get-GroupFields $si + $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } + $structItems += "$siType$groupStr" + } + } + # Compact: if many identical items, show count + if ($structItems.Count -gt 3) { + $grouped = $structItems | Group-Object | Sort-Object Count -Descending + $compactParts = @() + foreach ($g in $grouped) { + if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } + else { $compactParts += $g.Name } + } + $structItems = $compactParts + } + $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } + + $filterCount = 0 + if ($settings) { + $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count + } + $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } + + $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") + } + } +} + +function Show-OverviewHints { + # Hints — suggest next commands + $lines.Add("") + $hints = @() + # Collect query dataset names for hint + $queryDsNames = @() + foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { + $dsType = Get-DataSetType $ds + if ($dsType -eq "Query") { + $queryDsNames += $ds.SelectSingleNode("s:name", $ns).InnerText + } elseif ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + if ((Get-DataSetType $subDs) -eq "Query") { + $sn = $subDs.SelectSingleNode("s:name", $ns) + if ($sn) { $queryDsNames += $sn.InnerText } + } + } + } + } + if ($queryDsNames.Count -eq 1) { + $hints += "-Mode query query text" + } elseif ($queryDsNames.Count -gt 1) { + $hints += "-Mode query -Name <ds> query text ($($queryDsNames -join ', '))" + } + $hints += "-Mode fields field tables by dataset" + $linkCount = $root.SelectNodes("s:dataSetLink", $ns).Count + if ($linkCount -gt 0) { + $hints += "-Mode links dataset connections ($linkCount)" + } + $calcCount = $root.SelectNodes("s:calculatedField", $ns).Count + $totalCount = $root.SelectNodes("s:totalField", $ns).Count + if ($calcCount -gt 0) { + $hints += "-Mode calculated calculated field expressions ($calcCount)" + } + if ($totalCount -gt 0) { + $hints += "-Mode resources resource aggregation ($totalCount)" + } + $params = $root.SelectNodes("s:parameter", $ns) + if ($params.Count -gt 0) { + $hints += "-Mode params parameter details" + } + $variants = $root.SelectNodes("s:settingsVariant", $ns) + if ($variants.Count -eq 1) { + $hints += "-Mode variant variant structure" + } elseif ($variants.Count -gt 1) { + $hints += "-Mode variant -Name <N> variant structure (1..$($variants.Count))" + } + $tplDefs = $root.SelectNodes("s:template", $ns) + if ($tplDefs.Count -gt 0) { + $hints += "-Mode templates template bindings and expressions" + } + $hints += "-Mode trace -Name <f> trace field origin (by name or title)" + $hints += "-Mode full all sections at once" + $lines.Add("Next:") + foreach ($h in $hints) { $lines.Add(" $h") } +} + +# ============================================================ +# MODE: overview +# ============================================================ +if ($Mode -eq "overview") { + Show-Overview + Show-OverviewHints +} + +function Show-Query { + # Find dataset + $dataSets = $root.SelectNodes("s:dataSet", $ns) + $targetDs = $null + + if ($Name) { + # Search by name: prefer nested Query items over parent Union + # Pass 1: search nested items first + foreach ($ds in $dataSets) { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subNameNode = $subDs.SelectSingleNode("s:name", $ns) + if ($subNameNode -and $subNameNode.InnerText -eq $Name) { $targetDs = $subDs; break } + } + if ($targetDs) { break } + } + # Pass 2: search top-level + if (-not $targetDs) { + foreach ($ds in $dataSets) { + $dsNameNode = $ds.SelectSingleNode("s:name", $ns) + if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break } + } + } + if (-not $targetDs) { + Write-Error "Dataset '$Name' not found" + exit 1 + } + } else { + # Take first Query dataset + foreach ($ds in $dataSets) { + $dsType = Get-DataSetType $ds + if ($dsType -eq "Query") { $targetDs = $ds; break } + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + if ((Get-DataSetType $subDs) -eq "Query") { $targetDs = $subDs; break } + } + if ($targetDs) { break } + } + } + if (-not $targetDs) { + Write-Error "No Query dataset found" + exit 1 + } + } + + $queryNode = $targetDs.SelectSingleNode("s:query", $ns) + if (-not $queryNode) { + # If this is a Union, list nested query datasets + $dsType = Get-DataSetType $targetDs + if ($dsType -eq "Union") { + $subNames = @() + foreach ($subDs in $targetDs.SelectNodes("s:item", $ns)) { + $sn = $subDs.SelectSingleNode("s:name", $ns) + if ($sn) { $subNames += $sn.InnerText } + } + Write-Error "Dataset '$($targetDs.SelectSingleNode("s:name", $ns).InnerText)' is a Union. Specify nested: $($subNames -join ', ')" + } else { + Write-Error "Dataset has no query element" + } + exit 1 + } + + $rawQuery = Unescape-Xml $queryNode.InnerText + + # Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip + if ($Raw) { + foreach ($ql in ($rawQuery.Trim() -split "`n")) { $lines.Add($ql.TrimEnd()) } + return + } + + $dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText + + # Split into batches + $batches = @() + $batchTexts = $rawQuery -split ';\s*\r?\n\s*/{16,}\s*\r?\n' + foreach ($bt in $batchTexts) { + $trimmed = $bt.Trim() + if ($trimmed) { $batches += $trimmed } + } + + $totalQueryLines = ($rawQuery -split "`n").Count + + if ($batches.Count -le 1) { + # Single query + $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines) ===") + $lines.Add("") + foreach ($ql in ($rawQuery.Trim() -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + } else { + $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines, $($batches.Count) batches) ===") + + if ($Batch -eq 0) { + # Show TOC + $lineNum = 1 + for ($bi = 0; $bi -lt $batches.Count; $bi++) { + $batchLines = ($batches[$bi] -split "`n") + $endLine = $lineNum + $batchLines.Count - 1 + # Detect ПОМЕСТИТЬ target + $target = "" + foreach ($bl in $batchLines) { + if ($bl -match '^\s*(?:ПОМЕСТИТЬ|INTO)\s+(\S+)') { + $target = [char]0x2192 + " " + $Matches[1] + break + } + } + $lines.Add(" Batch $($bi + 1): lines $lineNum-$endLine $target") + $lineNum = $endLine + 3 # +separator + } + $lines.Add("") + + # Show all batches + for ($bi = 0; $bi -lt $batches.Count; $bi++) { + $lines.Add("--- Batch $($bi + 1) ---") + foreach ($ql in ($batches[$bi] -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + $lines.Add("") + } + } else { + # Show specific batch + if ($Batch -gt $batches.Count) { + Write-Error "Batch $Batch not found (total: $($batches.Count))" + exit 1 + } + $lines.Add("") + $lines.Add("--- Batch $Batch ---") + foreach ($ql in ($batches[$Batch - 1] -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + } + } +} + +# ============================================================ +# MODE: query +# ============================================================ +if ($Mode -eq "query") { + Show-Query +} + +function Show-Fields { + $dataSets = $root.SelectNodes("s:dataSet", $ns) + + function Show-DataSetFields($dsNode) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + $fields = $dsNode.SelectNodes("s:field", $ns) + + $lines.Add("=== Fields: $dsNameStr [$dsType] ($($fields.Count)) ===") + $lines.Add(" dataPath title role restrict format") + + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + $dpStr = if ($dp) { $dp.InnerText } else { "-" } + + $titleNode = $f.SelectSingleNode("s:title", $ns) + $titleStr = if ($titleNode) { Get-MLText $titleNode } else { "" } + if (-not $titleStr) { $titleStr = "-" } + + # Role + $role = $f.SelectSingleNode("s:role", $ns) + $roleStr = "-" + if ($role) { + $roleParts = @() + foreach ($child in $role.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $roleParts += $child.LocalName + } + } + if ($roleParts.Count -gt 0) { $roleStr = $roleParts -join "," } + } + + # UseRestriction + $restrict = $f.SelectSingleNode("s:useRestriction", $ns) + $restrictStr = "-" + if ($restrict) { + $restrictParts = @() + foreach ($child in $restrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $restrictParts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length)) + } + } + if ($restrictParts.Count -gt 0) { $restrictStr = $restrictParts -join "," } + } + + # Appearance format + $formatStr = "-" + $appearance = $f.SelectSingleNode("s:appearance", $ns) + if ($appearance) { + foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { + $paramNode = $appItem.SelectSingleNode("dcscor:parameter", $ns) + $valNode = $appItem.SelectSingleNode("dcscor:value", $ns) + if ($paramNode -and ($paramNode.InnerText -eq "Формат" -or $paramNode.InnerText -eq "Format") -and $valNode) { + $formatStr = $valNode.InnerText + } + } + } + + # presentationExpression + $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) + $presStr = "" + if ($presExpr) { $presStr = " presExpr" } + + $dpPad = $dpStr.PadRight(35) + $titlePad = $titleStr.PadRight(22) + $rolePad = $roleStr.PadRight(10) + $restrictPad = $restrictStr.PadRight(12) + + $lines.Add(" $dpPad $titlePad $rolePad $restrictPad $formatStr$presStr") + } + } + + if ($Name) { + # Detail for specific field by dataPath — search all datasets + $found = $false + $matchedIn = @() + + function Collect-FieldInfo($dsNode) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + foreach ($f in $dsNode.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp -or $dp.InnerText -ne $Name) { continue } + + $info = @{ dataset = "$dsNameStr [$dsType]" } + + $titleNode = $f.SelectSingleNode("s:title", $ns) + $info.title = if ($titleNode) { Get-MLText $titleNode } else { "" } + + # ValueType + $vt = $f.SelectSingleNode("s:valueType", $ns) + $info.type = if ($vt) { Get-CompactType $vt } else { "" } + + # Role + $role = $f.SelectSingleNode("s:role", $ns) + $roleParts = @() + if ($role) { + foreach ($child in $role.ChildNodes) { + if ($child.NodeType -ne "Element") { continue } + $txt = $child.InnerText.Trim() + if ($txt -eq "true") { + $roleParts += $child.LocalName + } elseif ($txt -eq "false") { + # skip default-false flags + } else { + $roleParts += "$($child.LocalName)=$txt" + } + } + } + $info.role = $roleParts -join ", " + + # UseRestriction + $restrict = $f.SelectSingleNode("s:useRestriction", $ns) + $restrictParts = @() + if ($restrict) { + foreach ($child in $restrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $restrictParts += $child.LocalName + } + } + } + $info.restrict = $restrictParts -join ", " + + # Format + $formatStr = "" + $appearance = $f.SelectSingleNode("s:appearance", $ns) + if ($appearance) { + foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { + $pn = $appItem.SelectSingleNode("dcscor:parameter", $ns) + $vn = $appItem.SelectSingleNode("dcscor:value", $ns) + if ($pn -and ($pn.InnerText -eq "Формат" -or $pn.InnerText -eq "Format") -and $vn) { + $formatStr = $vn.InnerText + } + } + } + $info.format = $formatStr + + # PresentationExpression + $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) + $info.presExpr = if ($presExpr) { $presExpr.InnerText } else { "" } + + return $info + } + return $null + } + + # Search all datasets and nested items + $fieldInfos = @() + foreach ($ds in $dataSets) { + $info = Collect-FieldInfo $ds + if ($info) { $fieldInfos += $info } + $dsType = Get-DataSetType $ds + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $info = Collect-FieldInfo $subDs + if ($info) { $fieldInfos += $info } + } + } + } + + if ($fieldInfos.Count -eq 0) { + Write-Error "Field '$Name' not found in any dataset" + exit 1 + } + + # Use first match for detail (they usually share the same properties) + $first = $fieldInfos[0] + $titleStr = if ($first.title) { " `"$($first.title)`"" } else { "" } + $lines.Add("=== Field: $Name$titleStr ===") + $lines.Add("") + + # Datasets + $dsList = ($fieldInfos | ForEach-Object { $_.dataset }) -join ", " + $lines.Add("Dataset: $dsList") + + if ($first.type) { $lines.Add("Type: $($first.type)") } + if ($first.role) { $lines.Add("Role: $($first.role)") } + if ($first.restrict) { $lines.Add("Restrict: $($first.restrict)") } + if ($first.format) { $lines.Add("Format: $($first.format)") } + if ($first.presExpr) { + $lines.Add("PresentationExpression:") + foreach ($el in ($first.presExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + } + } else { + # Compact map: field names per dataset + $lines.Add("=== Fields map ===") + + function Show-DataSetFieldMap($dsNode, $indent) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + $fields = $dsNode.SelectNodes("s:field", $ns) + $fieldNames = @() + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if ($dp) { $fieldNames += $dp.InnerText } + } + $nameList = $fieldNames -join ", " + if ($nameList.Length -gt 100) { + $nameList = $nameList.Substring(0, 97) + "..." + } + $lines.Add("$indent$dsNameStr [$dsType] ($($fields.Count)): $nameList") + } + + foreach ($ds in $dataSets) { + Show-DataSetFieldMap $ds "" + $dsType = Get-DataSetType $ds + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + Show-DataSetFieldMap $subDs " " + } + } + } + + $lines.Add("") + $lines.Add("Use -Name <field> for details.") + } +} + +# ============================================================ +# MODE: fields +# ============================================================ +if ($Mode -eq "fields") { + Show-Fields +} + +# ============================================================ +# MODE: links +# ============================================================ +elseif ($Mode -eq "links") { + + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -eq 0) { + $lines.Add("(no links)") + } else { + $lines.Add("=== Links ($($links.Count)) ===") + $lines.Add("") + # Group by source->dest pair + $currentPair = "" + foreach ($lnk in $links) { + $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText + $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText + $srcExpr = $lnk.SelectSingleNode("s:sourceExpression", $ns).InnerText + $dstExpr = $lnk.SelectSingleNode("s:destinationExpression", $ns).InnerText + $paramNode = $lnk.SelectSingleNode("s:parameter", $ns) + $paramListNode = $lnk.SelectSingleNode("s:parameterListAllowed", $ns) + + $pair = "$srcDs -> $dstDs" + if ($pair -ne $currentPair) { + if ($currentPair) { $lines.Add("") } + $lines.Add("$pair :") + $currentPair = $pair + } + + $paramStr = "" + if ($paramNode) { $paramStr = " param=$($paramNode.InnerText)" } + + $lines.Add(" $srcExpr -> $dstExpr$paramStr") + } + } +} + +# ============================================================ +# MODE: calculated +# ============================================================ +elseif ($Mode -eq "calculated") { + + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -eq 0) { + $lines.Add("(no calculated fields)") + } elseif ($Name) { + $found = $false + foreach ($cf in $calcFields) { + $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + if ($cfPath -eq $Name) { + $lines.Add("=== Calculated: $cfPath ===") + $lines.Add("") + + $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $lines.Add("Expression:") + foreach ($el in ($cfExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + if ($cfTitle) { + $t = Get-MLText $cfTitle + if ($t) { $lines.Add("Title: $t") } + } + + $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) + if ($cfRestrict) { + $parts = @() + foreach ($child in $cfRestrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $parts += $child.LocalName + } + } + if ($parts.Count -gt 0) { $lines.Add("Restrict: $($parts -join ', ')") } + } + + $found = $true + break + } + } + if (-not $found) { + Write-Error "Calculated field '$Name' not found" + exit 1 + } + } else { + # Map + $lines.Add("=== Calculated fields ($($calcFields.Count)) ===") + foreach ($cf in $calcFields) { + $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $titleStr = "" + if ($cfTitle) { + $t = Get-MLText $cfTitle + if ($t -and $t -ne $cfPath) { $titleStr = " `"$t`"" } + } + $lines.Add(" $cfPath$titleStr") + } + $lines.Add("") + $lines.Add("Use -Name <field> for full expression.") + } +} + +function Show-Resources { + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -eq 0) { + $lines.Add("(no resources)") + } elseif ($Name) { + $matched = @() + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + if ($tfPath -eq $Name) { $matched += $tf } + } + if ($matched.Count -eq 0) { + Write-Error "Resource '$Name' not found" + exit 1 + } + $lines.Add("=== Resource: $Name ===") + $lines.Add("") + foreach ($tf in $matched) { + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($tfGroup) { $groupStr = $tfGroup.InnerText } + $lines.Add(" [$groupStr] $tfExpr") + } + } else { + # Map + $lines.Add("=== Resources ($($totalFields.Count)) ===") + $resMap = [ordered]@{} + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + if (-not $resMap.Contains($tfPath)) { + $resMap[$tfPath] = @{ hasGroup = $false } + } + if ($tfGroup) { $resMap[$tfPath].hasGroup = $true } + } + foreach ($key in $resMap.Keys) { + $groupMark = if ($resMap[$key].hasGroup) { " *" } else { "" } + $lines.Add(" $key$groupMark") + } + $lines.Add("") + $lines.Add(" * = has group-level formulas") + $lines.Add("") + $lines.Add("Use -Name <field> for full formula.") + } +} + +# ============================================================ +# MODE: resources +# ============================================================ +if ($Mode -eq "resources") { + Show-Resources +} + +function Show-Params { + $params = $root.SelectNodes("s:parameter", $ns) + $lines.Add("=== Parameters ($($params.Count)) ===") + $lines.Add(" Name Type Default Visible Expression") + + foreach ($p in $params) { + $pName = $p.SelectSingleNode("s:name", $ns).InnerText + $pType = Get-CompactType $p.SelectSingleNode("s:valueType", $ns) + if (-not $pType) { $pType = "-" } + + # Default value + $valNode = $p.SelectSingleNode("s:value", $ns) + $valStr = "-" + if ($valNode) { + $nilAttr = $valNode.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance") + if ($nilAttr -eq "true") { + $valStr = "null" + } else { + $raw = $valNode.InnerText.Trim() + if ($raw -eq "0001-01-01T00:00:00") { + $valStr = "-" + } elseif ($raw) { + # Check for StandardPeriod variant + $variant = $valNode.SelectSingleNode("v8:variant", $ns) + if ($variant) { + $valStr = $variant.InnerText + } else { + $valStr = $raw + if ($valStr.Length -gt 15) { $valStr = $valStr.Substring(0, 12) + "..." } + } + } + } + } + + # Visibility + $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) + $visStr = "yes" + if ($useRestrict -and $useRestrict.InnerText -eq "true") { $visStr = "hidden" } + if ($useRestrict -and $useRestrict.InnerText -eq "false") { $visStr = "yes" } + + # Expression + $exprNode = $p.SelectSingleNode("s:expression", $ns) + $exprStr = "-" + if ($exprNode -and $exprNode.InnerText.Trim()) { + $exprStr = Unescape-Xml $exprNode.InnerText.Trim() + } + + # availableAsField + $availField = $p.SelectSingleNode("s:availableAsField", $ns) + $availStr = "" + if ($availField -and $availField.InnerText -eq "false") { $availStr = " [noField]" } + + $namePad = $pName.PadRight(33) + $typePad = $pType.PadRight(22) + $valPad = $valStr.PadRight(16) + $visPad = $visStr.PadRight(8) + + $lines.Add(" $namePad $typePad $valPad $visPad $exprStr$availStr") + } +} + +# ============================================================ +# MODE: params +# ============================================================ +if ($Mode -eq "params") { + Show-Params +} + +function Show-Variant { + $variants = $root.SelectNodes("s:settingsVariant", $ns) + + if (-not $Name) { + # --- Variant list (map) --- + if ($variants.Count -eq 0) { + $lines.Add("=== Variants: (none) ===") + } else { + $lines.Add("=== Variants ($($variants.Count)) ===") + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + $structItems = @() + if ($settings) { + foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { + $siType = Get-StructureItemType $si + $groupFields = Get-GroupFields $si + $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } + $structItems += "$siType$groupStr" + } + } + if ($structItems.Count -gt 3) { + $grouped = $structItems | Group-Object | Sort-Object Count -Descending + $compactParts = @() + foreach ($g in $grouped) { + if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } + else { $compactParts += $g.Name } + } + $structItems = $compactParts + } + $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } + + $filterCount = 0 + if ($settings) { + $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count + } + $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } + + # Selection fields + $selFields = @() + if ($settings) { $selFields = Get-SelectionFields $settings } + $selStr = if ($selFields.Count -gt 0) { " sel: " + ($selFields -join ", ") } else { "" } + + $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") + if ($selStr) { $lines.Add(" $selStr") } + } + } + } else { + # --- Variant detail --- + + $targetVariant = $null + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + if ($vName -eq $Name -or "$varIdx" -eq $Name) { + $targetVariant = $v + $matchIdx = $varIdx + break + } + } + if (-not $targetVariant) { + Write-Error "Variant '$Name' not found. Use -Mode variant without -Name to see list." + exit 1 + } + + $vName = $targetVariant.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $targetVariant.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $lines.Add("=== Variant [$matchIdx]: $vName$vPresStr ===") + + $settings = $targetVariant.SelectSingleNode("dcsset:settings", $ns) + if (-not $settings) { + $lines.Add(" (empty settings)") + } else { + # Selection at settings level + $topSel = Get-SelectionFields $settings + if ($topSel.Count -gt 0) { + $lines.Add("") + $lines.Add("Selection: " + ($topSel -join ", ")) + } + + # Structure + $structItems = $settings.SelectNodes("dcsset:item", $ns) + $hasStruct = $false + foreach ($si in $structItems) { + $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($siXsiType -like "*StructureItem*") { $hasStruct = $true; break } + } + + if ($hasStruct) { + $lines.Add("") + $lines.Add("Structure:") + foreach ($si in $structItems) { + $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($siXsiType -like "*StructureItem*") { + Build-StructureTree -itemNode $si -prefix " " -isLast $false -outLines $lines + } + } + } + + # Filter + $filters = Get-FilterSummary $settings + if ($filters.Count -gt 0) { + $lines.Add("") + $lines.Add("Filter:") + foreach ($f in $filters) { + $lines.Add(" $f") + } + } + + # Data parameters + $dataParams = $settings.SelectNodes("dcsset:dataParameters/dcsset:item", $ns) + if ($dataParams.Count -gt 0) { + $dpStrs = @() + foreach ($dp in $dataParams) { + $dpParam = $dp.SelectSingleNode("dcscor:parameter", $ns) + $dpVal = $dp.SelectSingleNode("dcscor:value", $ns) + if ($dpParam -and $dpVal) { + $dpStrs += "$($dpParam.InnerText)=`"$($dpVal.InnerText)`"" + } + } + if ($dpStrs.Count -gt 0) { + $lines.Add("") + $lines.Add("DataParams: " + ($dpStrs -join ", ")) + } + } + + # Output parameters + $outParams = $settings.SelectNodes("dcsset:outputParameters/dcscor:item", $ns) + if ($outParams.Count -gt 0) { + $opStrs = @() + foreach ($op in $outParams) { + $opParam = $op.SelectSingleNode("dcscor:parameter", $ns) + $opVal = $op.SelectSingleNode("dcscor:value", $ns) + if ($opParam -and $opVal) { + $paramName = $opParam.InnerText + $paramVal = $opVal.InnerText + # Shorten known long names + switch ($paramName) { + "МакетОформления" { $opStrs += "style=$paramVal" } + "РасположениеПолейГруппировки" { $opStrs += "groups=$paramVal" } + "ГоризонтальноеРасположениеОбщихИтогов" { $opStrs += "totalsH=$paramVal" } + "ВертикальноеРасположениеОбщихИтогов" { $opStrs += "totalsV=$paramVal" } + "ВыводитьЗаголовок" { $opStrs += "header=$paramVal" } + "ВыводитьОтбор" { $opStrs += "filter=$paramVal" } + "ВыводитьПараметрыДанных" { $opStrs += "dataParams=$paramVal" } + "РасположениеРеквизитов" { $opStrs += "attrs=$paramVal" } + default { $opStrs += "$paramName=$paramVal" } + } + } + } + if ($opStrs.Count -gt 0) { + $lines.Add("") + $lines.Add("Output: " + ($opStrs -join " ")) + } + } + } + } # end else (variant detail) +} + +# ============================================================ +# MODE: variant +# ============================================================ +if ($Mode -eq "variant") { + Show-Variant +} + +# ============================================================ +# MODE: full +# ============================================================ +elseif ($Mode -eq "full") { + Show-Overview + $lines.Add(""); $lines.Add("--- query ---"); $lines.Add("") + $hasQuery = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetQuery']", $ns).Count -gt 0 + if ($hasQuery) { + Show-Query + } else { + $objNodes = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetObject']/s:objectName", $ns) + if ($objNodes.Count -gt 0) { + $names = @(); foreach ($n in $objNodes) { $names += $n.InnerText } + $lines.Add("(no query datasets; external datasets: $($names -join ', '))") + } else { + $lines.Add("(no query datasets)") + } + } + $lines.Add(""); $lines.Add("--- fields ---"); $lines.Add("") + Show-Fields + $lines.Add(""); $lines.Add("--- resources ---"); $lines.Add("") + Show-Resources + $lines.Add(""); $lines.Add("--- params ---"); $lines.Add("") + Show-Params + $lines.Add(""); $lines.Add("--- variant ---"); $lines.Add("") + Show-Variant +} + +# ============================================================ +# MODE: trace +# ============================================================ +elseif ($Mode -eq "trace") { + + if (-not $Name) { + Write-Error "Trace mode requires -Name <field_name_or_title>" + exit 1 + } + + # --- Build field index --- + + $dsFields = @{} # dataPath -> @{ datasets=@(); title="" } + $calcFields = @{} # dataPath -> @{ expression=""; title="" } + $resFields = @{} # dataPath -> @(@{ expression=""; group="" }) + $titleMap = @{} # title -> dataPath + + # Scan dataset fields (including nested Union items) + $dataSets = $root.SelectNodes("s:dataSet", $ns) + foreach ($ds in $dataSets) { + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $dsType = Get-DataSetType $ds + + foreach ($f in $ds.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$dsName [$dsType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subName = $subDs.SelectSingleNode("s:name", $ns).InnerText + $subType = Get-DataSetType $subDs + foreach ($f in $subDs.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$subName [$subType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + } + } + } + + # Scan calculated fields + foreach ($cf in $root.SelectNodes("s:calculatedField", $ns)) { + $dpStr = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $t = "" + if ($cfTitle) { $t = Get-MLText $cfTitle } + $calcFields[$dpStr] = @{ expression = $expr; title = $t } + if ($t -and -not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + + # Scan resources + foreach ($tf in $root.SelectNodes("s:totalField", $ns)) { + $dpStr = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $grp = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($grp) { $groupStr = $grp.InnerText } + if (-not $resFields.ContainsKey($dpStr)) { $resFields[$dpStr] = @() } + $resFields[$dpStr] += @{ expression = $expr; group = $groupStr } + } + + # --- Resolve name: try dataPath, then exact title, then substring title --- + $targetPath = $Name + $knownPaths = @() + $knownPaths += $dsFields.Keys + $knownPaths += $calcFields.Keys + $knownPaths += $resFields.Keys + $isKnown = $knownPaths -contains $Name + + if (-not $isKnown) { + if ($titleMap.ContainsKey($Name)) { + $targetPath = $titleMap[$Name] + } else { + # Substring match in titles + $matchedTitle = $null + foreach ($key in $titleMap.Keys) { + if ($key -like "*$Name*") { + $matchedTitle = $key + break + } + } + if ($matchedTitle) { + $targetPath = $titleMap[$matchedTitle] + } else { + Write-Error "Field '$Name' not found by dataPath or title" + exit 1 + } + } + } + + # --- Build output --- + $title = "" + if ($calcFields.ContainsKey($targetPath) -and $calcFields[$targetPath].title) { + $title = $calcFields[$targetPath].title + } elseif ($dsFields.ContainsKey($targetPath) -and $dsFields[$targetPath].title) { + $title = $dsFields[$targetPath].title + } + $titleStr = if ($title) { " `"$title`"" } else { "" } + + $lines.Add("=== Trace: $targetPath$titleStr ===") + $lines.Add("") + + # Dataset origin + if ($dsFields.ContainsKey($targetPath)) { + $uniqueDs = $dsFields[$targetPath].datasets | Select-Object -Unique + $lines.Add("Dataset: $($uniqueDs -join ', ')") + } else { + $lines.Add("Dataset: (schema-level only, not in dataset fields)") + } + + # Calculated field + if ($calcFields.ContainsKey($targetPath)) { + $cf = $calcFields[$targetPath] + $lines.Add("") + $lines.Add("Calculated:") + foreach ($el in ($cf.expression -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + + # Extract operands: find known field names in expression + $operands = @() + $allKnown = @() + $allKnown += $dsFields.Keys + $allKnown += $calcFields.Keys + $allKnown = $allKnown | Select-Object -Unique | Where-Object { $_ -ne $targetPath } + # Sort by length descending to match longer names first + $allKnown = $allKnown | Sort-Object -Property Length -Descending + + foreach ($fieldName in $allKnown) { + $escaped = [regex]::Escape($fieldName) + if ($cf.expression -match "(?<![а-яА-ЯёЁa-zA-Z0-9_.])$escaped(?![а-яА-ЯёЁa-zA-Z0-9_.])") { + $operands += $fieldName + } + } + + if ($operands.Count -gt 0) { + $lines.Add(" Operands:") + foreach ($op in $operands) { + if ($calcFields.ContainsKey($op)) { + $lines.Add(" $op -> calculated") + } elseif ($dsFields.ContainsKey($op)) { + $opDs = ($dsFields[$op].datasets | Select-Object -Unique) -join ", " + $lines.Add(" $op -> $opDs") + } else { + $lines.Add(" $op") + } + } + } + } + + # Resource + if ($resFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("Resource:") + foreach ($r in $resFields[$targetPath]) { + $lines.Add(" [$($r.group)] $($r.expression)") + } + } + + # Simple dataset field, no calc/resource + if (-not $calcFields.ContainsKey($targetPath) -and -not $resFields.ContainsKey($targetPath)) { + if ($dsFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("(direct dataset field, no calculated expression or resource)") + } + } +} + +# ============================================================ +# MODE: templates +# ============================================================ +elseif ($Mode -eq "templates") { + + # --- Helper: check if expression is trivial --- + function Is-TrivialExpr([string]$paramName, [string]$expr) { + $e = $expr.Trim() + $n = $paramName.Trim() + if ($e -eq $n) { return $true } + if ($e -eq "Представление($n)") { return $true } + return $false + } + + # --- Helper: parse template content (rows/cells) --- + function Get-TemplateContent([System.Xml.XmlNode]$tplNode) { + $innerT = $tplNode.SelectSingleNode("s:template", $ns) + if (-not $innerT) { return @{ rows = 0; cells = @(); params = @(); nonTrivial = @() } } + + $rows = $innerT.SelectNodes("dcsat:item", $ns) + $rowCount = $rows.Count + $cellData = [System.Collections.ArrayList]::new() + $rowIdx = 0 + foreach ($row in $rows) { + $rowIdx++ + $rowCells = [System.Collections.ArrayList]::new() + foreach ($cell in $row.SelectNodes("dcsat:tableCell", $ns)) { + $field = $cell.SelectSingleNode("dcsat:item", $ns) + if (-not $field) { + [void]$rowCells.Add("(empty)") + continue + } + $val = $field.SelectSingleNode("dcsat:value", $ns) + if (-not $val) { + [void]$rowCells.Add("(empty)") + continue + } + $xsiType = $val.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*LocalStringType*") { + $text = Get-MLText $val + if ($text) { [void]$rowCells.Add("`"$text`"") } + else { [void]$rowCells.Add("(empty)") } + } elseif ($xsiType -like "*Parameter*") { + [void]$rowCells.Add("{$($val.InnerText)}") + } else { + [void]$rowCells.Add("(?)") + } + } + [void]$cellData.Add(@{ row = $rowIdx; cells = $rowCells.ToArray() }) + } + + # Parameters + $paramNodes = $tplNode.SelectNodes("s:parameter", $ns) + $paramList = [System.Collections.ArrayList]::new() + $nonTrivialList = [System.Collections.ArrayList]::new() + foreach ($p in $paramNodes) { + $pn = $p.SelectSingleNode("dcsat:name", $ns) + $pe = $p.SelectSingleNode("dcsat:expression", $ns) + if ($pn -and $pe) { + $pName = $pn.InnerText + $pExpr = $pe.InnerText + [void]$paramList.Add(@{ name = $pName; expression = $pExpr }) + if (-not (Is-TrivialExpr $pName $pExpr)) { + [void]$nonTrivialList.Add(@{ name = $pName; expression = $pExpr }) + } + } + } + + return @{ + rows = $rowCount + cells = $cellData.ToArray() + params = $paramList.ToArray() + nonTrivial = $nonTrivialList.ToArray() + } + } + + # --- Build template name -> node index --- + $tplIndex = @{} + foreach ($t in $root.SelectNodes("s:template", $ns)) { + $tn = $t.SelectSingleNode("s:name", $ns) + if ($tn) { $tplIndex[$tn.InnerText] = $t } + } + + # --- Parse bindings --- + # Group bindings: groupTemplate + groupHeaderTemplate + $groupBindings = [ordered]@{} # groupName -> @{ bindings = @(@{type; tplName; tplNode}) } + + foreach ($gt in $root.SelectNodes("s:groupTemplate", $ns)) { + $gn = $gt.SelectSingleNode("s:groupName", $ns) + $gf = $gt.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tt = $gt.SelectSingleNode("s:templateType", $ns) + $tn = $gt.SelectSingleNode("s:template", $ns) + $ttStr = if ($tt) { $tt.InnerText } else { "-" } + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) + } + + foreach ($ght in $root.SelectNodes("s:groupHeaderTemplate", $ns)) { + $gn = $ght.SelectSingleNode("s:groupName", $ns) + $gf = $ght.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tt = $ght.SelectSingleNode("s:templateType", $ns) + $tn = $ght.SelectSingleNode("s:template", $ns) + $ttStr = if ($tt) { "GroupHeader" } else { "GroupHeader" } + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) + } + + foreach ($gft in $root.SelectNodes("s:groupFooterTemplate", $ns)) { + $gn = $gft.SelectSingleNode("s:groupName", $ns) + $gf = $gft.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tn = $gft.SelectSingleNode("s:template", $ns) + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = "GroupFooter"; tplName = $tnStr }) + } + + # Field bindings: fieldTemplate + $fieldBindings = [ordered]@{} # fieldName -> tplName + $fieldNonTrivial = [System.Collections.ArrayList]::new() + + foreach ($ft in $root.SelectNodes("s:fieldTemplate", $ns)) { + $fn = $ft.SelectSingleNode("s:field", $ns) + $tn = $ft.SelectSingleNode("s:template", $ns) + if ($fn -and $tn) { + $fName = $fn.InnerText + $tName = $tn.InnerText + $fieldBindings[$fName] = $tName + # Check params for non-trivial expressions + if ($tplIndex.ContainsKey($tName)) { + $content = Get-TemplateContent $tplIndex[$tName] + foreach ($nt in $content.nonTrivial) { + [void]$fieldNonTrivial.Add(@{ field = $fName; template = $tName; name = $nt.name; expression = $nt.expression }) + } + } + } + } + + $totalTpl = $tplIndex.Count + $fieldCount = $fieldBindings.Count + $groupBindCount = 0 + foreach ($k in $groupBindings.Keys) { $groupBindCount += $groupBindings[$k].Count } + + if (-not $Name) { + # --- MAP mode --- + $lines.Add("=== Templates ($totalTpl defined: $fieldCount field, $groupBindCount group) ===") + + # Field bindings + if ($fieldBindings.Count -gt 0) { + $lines.Add("") + if ($fieldNonTrivial.Count -eq 0) { + $fieldNames = @($fieldBindings.Keys) + if ($fieldNames.Count -le 8) { + $lines.Add("Field bindings ($fieldCount): $($fieldNames -join ', ') (all trivial)") + } else { + $lines.Add("Field bindings ($fieldCount): (all trivial)") + $lines.Add(" $($fieldNames[0..7] -join ', '), ...") + } + } else { + $trivialCount = $fieldBindings.Count - ($fieldNonTrivial | Select-Object -ExpandProperty field -Unique).Count + $lines.Add("Field bindings ($fieldCount, $trivialCount trivial):") + foreach ($nt in $fieldNonTrivial) { + $lines.Add(" $($nt.field): $($nt.name) = $($nt.expression)") + } + } + } + + # Group bindings + if ($groupBindings.Count -gt 0) { + $lines.Add("") + $lines.Add("Group bindings ($groupBindCount):") + foreach ($gName in $groupBindings.Keys) { + $bindings = $groupBindings[$gName] + $parts = @() + foreach ($b in $bindings) { + $info = "$($b.type) -> $($b.tplName)" + if ($tplIndex.ContainsKey($b.tplName)) { + $content = Get-TemplateContent $tplIndex[$b.tplName] + # Check if any cell has content + $hasContent = $false + foreach ($r in $content.cells) { + foreach ($c in $r.cells) { + if ($c -ne "(empty)") { $hasContent = $true; break } + } + if ($hasContent) { break } + } + $info += " ($($content.rows) rows" + if ($content.params.Count -gt 0) { $info += ", $($content.params.Count) params" } + $info += ")" + if (-not $hasContent -and $content.params.Count -eq 0) { + $info += " spacer" + } + if ($content.nonTrivial.Count -gt 0) { + $ntNames = ($content.nonTrivial | ForEach-Object { $_.name }) -join ', ' + $info += " *$ntNames" + } + } + $parts += $info + } + $lines.Add(" $gName") + foreach ($p in $parts) { + $lines.Add(" $p") + } + } + } + + if ($fieldBindings.Count -gt 0 -or $groupBindings.Count -gt 0) { + $lines.Add("") + $lines.Add("Use -Name <group|field> for template details.") + } + } else { + # --- DETAIL mode --- + $found = $false + + # Check group bindings first + if ($groupBindings.Contains($Name)) { + $found = $true + $bindings = $groupBindings[$Name] + $lines.Add("=== Templates: $Name ===") + foreach ($b in $bindings) { + $lines.Add("") + $tName = $b.tplName + if (-not $tplIndex.ContainsKey($tName)) { + $lines.Add("$($b.type) -> $tName (template not found)") + continue + } + $content = Get-TemplateContent $tplIndex[$tName] + $cellCount = 0 + foreach ($r in $content.cells) { $cellCount += $r.cells.Count } + $lines.Add("$($b.type) -> $tName [$($content.rows) rows, $cellCount cells]:") + + foreach ($r in $content.cells) { + $cellStr = $r.cells -join " | " + $lines.Add(" Row $($r.row): $cellStr") + } + + if ($content.nonTrivial.Count -gt 0) { + $lines.Add(" Params:") + foreach ($nt in $content.nonTrivial) { + $lines.Add(" $($nt.name) = $($nt.expression)") + } + } + } + } + + # Check field bindings + if ($fieldBindings.Contains($Name)) { + if ($found) { $lines.Add("") } + $found = $true + $tName = $fieldBindings[$Name] + $lines.Add("=== Field template: $Name -> $tName ===") + if ($tplIndex.ContainsKey($tName)) { + $content = Get-TemplateContent $tplIndex[$tName] + $cellCount = 0 + foreach ($r in $content.cells) { $cellCount += $r.cells.Count } + $lines.Add("[$($content.rows) rows, $cellCount cells]") + + foreach ($r in $content.cells) { + $cellStr = $r.cells -join " | " + $lines.Add(" Row $($r.row): $cellStr") + } + + if ($content.nonTrivial.Count -gt 0) { + $lines.Add(" Non-trivial params:") + foreach ($nt in $content.nonTrivial) { + $lines.Add(" $($nt.name) = $($nt.expression)") + } + } else { + $lines.Add(" (all params trivial)") + } + } + } + + if (-not $found) { + Write-Error "Group or field '$Name' not found in template bindings" + exit 1 + } + } +} + +# --- Output --- + +$result = $lines.ToArray() +$totalLines = $result.Count + +# OutFile +if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + if ([System.IO.Path]::IsPathRooted($OutFile)) { + $outPath = [System.IO.Path]::GetFullPath($OutFile) + } else { + $outPath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $OutFile)) + } + [System.IO.File]::WriteAllLines($outPath, $result, $utf8Bom) + Write-Host "Written $totalLines lines to $OutFile" + exit 0 +} + +# Pagination +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $result = $result[$Offset..($totalLines - 1)] +} + +if (-not $Raw -and $result.Count -gt $Limit) { + $shown = $result[0..($Limit - 1)] + foreach ($l in $shown) { Write-Host $l } + Write-Host "" + Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." +} else { + foreach ($l in $result) { Write-Host $l } +} diff --git a/.claude/skills/skd-validate/SKILL.md b/.claude/skills/skd-validate/SKILL.md index 7bfea969..f302a802 100644 --- a/.claude/skills/skd-validate/SKILL.md +++ b/.claude/skills/skd-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: skd-validate -description: Валидация схемы компоновки данных 1С (СКД). Используй после создания или модификации СКД для проверки корректности -argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /skd-validate — валидация СКД (DataCompositionSchema) - -Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|--------------|:-----:|---------|---------------------------------------------------------| -| TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 20 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml" -``` +--- +name: skd-validate +description: Валидация схемы компоновки данных 1С (СКД). Используй после создания или модификации СКД для проверки корректности +argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /skd-validate — валидация СКД (DataCompositionSchema) + +Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|--------------|:-----:|---------|---------------------------------------------------------| +| TemplatePath | да | — | Путь к Template.xml или каталогу макета | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 20 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml" +``` diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1 index ead1a77e..a8c9458f 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.ps1 +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -1,935 +1,935 @@ -# skd-validate v1.2 — Validate 1C DCS structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$TemplatePath, - - [switch]$Detailed, - - [int]$MaxErrors = 20, - - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- - -if (-not [System.IO.Path]::IsPathRooted($TemplatePath)) { - $TemplatePath = Join-Path (Get-Location).Path $TemplatePath -} -# A: Directory → Ext/Template.xml -if (Test-Path $TemplatePath -PathType Container) { - $TemplatePath = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" -} -# B1: Missing Ext/ (e.g. Templates/СКД/Template.xml → Templates/СКД/Ext/Template.xml) -if (-not (Test-Path $TemplatePath)) { - $fn = [System.IO.Path]::GetFileName($TemplatePath) - if ($fn -eq "Template.xml") { - $c = Join-Path (Join-Path (Split-Path $TemplatePath) "Ext") $fn - if (Test-Path $c) { $TemplatePath = $c } - } -} -# B2: Descriptor (Templates/СКД.xml → Templates/СКД/Ext/Template.xml) -if (-not (Test-Path $TemplatePath) -and $TemplatePath.EndsWith(".xml")) { - $stem = [System.IO.Path]::GetFileNameWithoutExtension($TemplatePath) - $dir = Split-Path $TemplatePath - $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Template.xml" - if (Test-Path $c) { $TemplatePath = $c } -} - -if (-not (Test-Path $TemplatePath)) { - Write-Error "File not found: $TemplatePath" - exit 1 -} - -$resolvedPath = (Resolve-Path $TemplatePath).Path -$fileName = [System.IO.Path]::GetFileName($resolvedPath) - -# --- Output infrastructure --- - -$script:errors = 0 -$script:warnings = 0 -$script:okCount = 0 -$script:stopped = $false -$script:output = New-Object System.Text.StringBuilder 4096 - -function Out-Line { - param([string]$msg) - $script:output.AppendLine($msg) | Out-Null -} - -function Report-OK { - param([string]$msg) - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} - -function Report-Error { - param([string]$msg) - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { - $script:stopped = $true - } -} - -function Report-Warn { - param([string]$msg) - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$finalize = { - $checks = $script:okCount + $script:errors + $script:warnings - if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: $fileName ($checks checks) ===" - } else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() - } - Write-Host $result - - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" - } -} - -Out-Line "=== Validation: $fileName ===" -Out-Line "" - -# --- 1. Parse XML --- - -$xmlDoc = $null -try { - $xmlDoc = New-Object System.Xml.XmlDocument - $xmlDoc.PreserveWhitespace = $false - $xmlDoc.Load($resolvedPath) - Report-OK "XML parsed successfully" -} catch { - Report-Error "XML parse failed: $($_.Exception.Message)" - # Cannot continue - $result = $script:output.ToString() - Write-Host $result - if ($OutFile) { - $utf8Bom = New-Object System.Text.UTF8Encoding $true - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - } - exit 1 -} - -# --- 2. Register namespaces --- - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") -$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") -$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") -$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") -$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") -$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") -$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") -$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") -$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") - -$root = $xmlDoc.DocumentElement - -# --- 3. Root element checks --- - -if ($root.LocalName -ne "DataCompositionSchema") { - Report-Error "Root element is '$($root.LocalName)', expected 'DataCompositionSchema'" -} else { - Report-OK "Root element: DataCompositionSchema" -} - -$expectedNs = "http://v8.1c.ru/8.1/data-composition-system/schema" -if ($root.NamespaceURI -ne $expectedNs) { - Report-Error "Default namespace is '$($root.NamespaceURI)', expected '$expectedNs'" -} else { - Report-OK "Default namespace correct" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 4. Collect inventories --- - -# DataSources -$dataSourceNodes = $root.SelectNodes("s:dataSource", $ns) -$dataSourceNames = @{} -foreach ($dsn in $dataSourceNodes) { - $name = $dsn.SelectSingleNode("s:name", $ns) - if ($name) { $dataSourceNames[$name.InnerText] = $true } -} - -# DataSets (recursive for unions) -$dataSetNodes = $root.SelectNodes("s:dataSet", $ns) -$dataSetNames = @{} -$allFieldPaths = @{} # Global: dataPath → dataSet name - -function Collect-DataSetFields { - param($dsNode, [string]$dsName) - - $fields = $dsNode.SelectNodes("s:field", $ns) - $localPaths = @{} - foreach ($f in $fields) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - if ($dp) { - $path = $dp.InnerText - $localPaths[$path] = $true - $allFieldPaths[$path] = $dsName - } - } - - # Union items - $items = $dsNode.SelectNodes("s:item", $ns) - foreach ($item in $items) { - $itemName = $item.SelectSingleNode("s:name", $ns) - if ($itemName) { - Collect-DataSetFields -dsNode $item -dsName $itemName.InnerText - } - } - - return $localPaths -} - -$dataSetFieldMap = @{} # dsName → hashtable of dataPath -foreach ($ds in $dataSetNodes) { - $nameNode = $ds.SelectSingleNode("s:name", $ns) - if ($nameNode) { - $dsName = $nameNode.InnerText - $dataSetNames[$dsName] = $true - $dataSetFieldMap[$dsName] = Collect-DataSetFields -dsNode $ds -dsName $dsName - } -} - -# CalculatedFields -$calcFieldNodes = $root.SelectNodes("s:calculatedField", $ns) -$calcFieldPaths = @{} -foreach ($cf in $calcFieldNodes) { - $dp = $cf.SelectSingleNode("s:dataPath", $ns) - if ($dp) { $calcFieldPaths[$dp.InnerText] = $true } -} - -# TotalFields -$totalFieldNodes = $root.SelectNodes("s:totalField", $ns) - -# Parameters -$paramNodes = $root.SelectNodes("s:parameter", $ns) -$paramNames = @{} -foreach ($p in $paramNodes) { - $nameNode = $p.SelectSingleNode("s:name", $ns) - if ($nameNode) { $paramNames[$nameNode.InnerText] = $true } -} - -# Templates -$templateNodes = $root.SelectNodes("s:template", $ns) -$templateNames = @{} -foreach ($t in $templateNodes) { - $nameNode = $t.SelectSingleNode("s:name", $ns) - if ($nameNode) { $templateNames[$nameNode.InnerText] = $true } -} - -# GroupTemplates -$groupTemplateNodes = $root.SelectNodes("s:groupTemplate", $ns) - -# SettingsVariants -$variantNodes = $root.SelectNodes("s:settingsVariant", $ns) - -# Known fields = dataset fields + calculated fields -$knownFields = @{} -foreach ($key in $allFieldPaths.Keys) { $knownFields[$key] = $true } -foreach ($key in $calcFieldPaths.Keys) { $knownFields[$key] = $true } - -# --- 5. DataSource checks --- - -if ($dataSourceNodes.Count -eq 0) { - Report-Warn "No dataSource elements found (settings-only DCS?)" -} else { - $dsNamesSeen = @{} - $dsOk = $true - foreach ($dsn in $dataSourceNodes) { - $name = $dsn.SelectSingleNode("s:name", $ns) - $type = $dsn.SelectSingleNode("s:dataSourceType", $ns) - if (-not $name -or -not $name.InnerText) { - Report-Error "DataSource has empty name" - $dsOk = $false - } elseif ($dsNamesSeen.ContainsKey($name.InnerText)) { - Report-Error "Duplicate dataSource name: $($name.InnerText)" - $dsOk = $false - } else { - $dsNamesSeen[$name.InnerText] = $true - } - if ($type) { - $tv = $type.InnerText - if ($tv -ne "Local" -and $tv -ne "External") { - Report-Warn "DataSource '$($name.InnerText)' has unusual type: $tv" - } - } - } - if ($dsOk) { - Report-OK "$($dataSourceNodes.Count) dataSource(s) found, names unique" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 6. DataSet checks --- - -$validDsTypes = @("DataSetQuery", "DataSetObject", "DataSetUnion") - -if ($dataSetNodes.Count -eq 0) { - Report-Warn "No dataSet elements found (settings-only DCS?)" -} else { - $dsNamesSeen = @{} - $dsOk = $true - foreach ($ds in $dataSetNodes) { - $xsiType = $ds.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - $nameNode = $ds.SelectSingleNode("s:name", $ns) - $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } - - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "DataSet has empty name" - $dsOk = $false - } elseif ($dsNamesSeen.ContainsKey($dsName)) { - Report-Error "Duplicate dataSet name: $dsName" - $dsOk = $false - } else { - $dsNamesSeen[$dsName] = $true - } - - if (-not $xsiType) { - Report-Error "DataSet '$dsName' missing xsi:type" - $dsOk = $false - } elseif ($validDsTypes -notcontains $xsiType) { - Report-Warn "DataSet '$dsName' has unusual xsi:type: $xsiType" - } - - # Check dataSource reference - if ($xsiType -ne "DataSetUnion") { - $srcNode = $ds.SelectSingleNode("s:dataSource", $ns) - if ($srcNode -and $srcNode.InnerText) { - if (-not $dataSourceNames.ContainsKey($srcNode.InnerText)) { - Report-Error "DataSet '$dsName' references unknown dataSource: $($srcNode.InnerText)" - $dsOk = $false - } - } - } - - # Check query not empty for Query type - if ($xsiType -eq "DataSetQuery") { - $queryNode = $ds.SelectSingleNode("s:query", $ns) - if (-not $queryNode -or -not $queryNode.InnerText.Trim()) { - Report-Warn "DataSet '$dsName' (Query) has empty query" - } - } - - # Check objectName for Object type - if ($xsiType -eq "DataSetObject") { - $objNode = $ds.SelectSingleNode("s:objectName", $ns) - if (-not $objNode -or -not $objNode.InnerText.Trim()) { - Report-Error "DataSet '$dsName' (Object) has empty objectName" - $dsOk = $false - } - } - } - - if ($dsOk) { - Report-OK "$($dataSetNodes.Count) dataSet(s) found, names unique" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 7. Field checks --- - -function Check-DataSetFields { - param($dsNode, [string]$dsName) - - $fields = $dsNode.SelectNodes("s:field", $ns) - if ($fields.Count -eq 0) { return } - - $pathsSeen = @{} - $fieldOk = $true - - foreach ($f in $fields) { - $dp = $f.SelectSingleNode("s:dataPath", $ns) - $fn = $f.SelectSingleNode("s:field", $ns) - - if (-not $dp -or -not $dp.InnerText) { - Report-Error "DataSet '$dsName': field has empty dataPath" - $fieldOk = $false - continue - } - - $path = $dp.InnerText - if ($pathsSeen.ContainsKey($path)) { - Report-Warn "DataSet '$dsName': duplicate dataPath '$path'" - } else { - $pathsSeen[$path] = $true - } - - if (-not $fn -or -not $fn.InnerText) { - Report-Warn "DataSet '$dsName': field '$path' has empty <field> element" - } - } - - if ($fieldOk) { - Report-OK "DataSet `"$dsName`": $($fields.Count) fields, dataPath unique" - } - - # Check union items recursively - $items = $dsNode.SelectNodes("s:item", $ns) - foreach ($item in $items) { - $itemName = $item.SelectSingleNode("s:name", $ns) - $iName = if ($itemName) { $itemName.InnerText } else { "(unnamed item)" } - Check-DataSetFields -dsNode $item -dsName $iName - } -} - -foreach ($ds in $dataSetNodes) { - $nameNode = $ds.SelectSingleNode("s:name", $ns) - $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } - Check-DataSetFields -dsNode $ds -dsName $dsName -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 8. DataSetLink checks --- - -$linkNodes = $root.SelectNodes("s:dataSetLink", $ns) -if ($linkNodes.Count -gt 0) { - $linkOk = $true - foreach ($link in $linkNodes) { - $src = $link.SelectSingleNode("s:sourceDataSet", $ns) - $dst = $link.SelectSingleNode("s:destinationDataSet", $ns) - $srcExpr = $link.SelectSingleNode("s:sourceExpression", $ns) - $dstExpr = $link.SelectSingleNode("s:destinationExpression", $ns) - - if ($src -and $src.InnerText -and -not $dataSetNames.ContainsKey($src.InnerText)) { - Report-Error "DataSetLink: sourceDataSet '$($src.InnerText)' not found" - $linkOk = $false - } - if ($dst -and $dst.InnerText -and -not $dataSetNames.ContainsKey($dst.InnerText)) { - Report-Error "DataSetLink: destinationDataSet '$($dst.InnerText)' not found" - $linkOk = $false - } - if (-not $srcExpr -or -not $srcExpr.InnerText.Trim()) { - Report-Error "DataSetLink: empty sourceExpression" - $linkOk = $false - } - if (-not $dstExpr -or -not $dstExpr.InnerText.Trim()) { - Report-Error "DataSetLink: empty destinationExpression" - $linkOk = $false - } - } - if ($linkOk) { - Report-OK "$($linkNodes.Count) dataSetLink(s): references valid" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 9. CalculatedField checks --- - -if ($calcFieldNodes.Count -gt 0) { - $cfOk = $true - $cfSeen = @{} - # Collect totalField dataPaths — an empty calculatedField is legitimate if a - # totalField with the same dataPath provides the expression (real-world - # pattern in vendor ERP/БП reports for fields visible only in totals). - $tfPaths = @{} - foreach ($tf in $totalFieldNodes) { - $tfDp = $tf.SelectSingleNode("s:dataPath", $ns) - if ($tfDp -and $tfDp.InnerText) { - $tfPaths[$tfDp.InnerText] = $true - } - } - - foreach ($cf in $calcFieldNodes) { - $dp = $cf.SelectSingleNode("s:dataPath", $ns) - $expr = $cf.SelectSingleNode("s:expression", $ns) - - if (-not $dp -or -not $dp.InnerText) { - Report-Error "CalculatedField has empty dataPath" - $cfOk = $false - continue - } - - $path = $dp.InnerText - if ($cfSeen.ContainsKey($path)) { - Report-Error "Duplicate calculatedField dataPath: $path" - $cfOk = $false - } else { - $cfSeen[$path] = $true - } - - if (-not $expr -or -not $expr.InnerText.Trim()) { - # Empty expression is legitimate in several vendor patterns: - # - totalField with same dataPath provides the calculation - # - groupTemplate uses the field as group name (declarative only) - # - field is referenced only by settingsVariants for grouping - # Surface as warning, not error, to avoid false positives on real - # ERP/БП reports while still flagging the unusual shape. - if (-not $tfPaths.ContainsKey($path)) { - Report-Warn "CalculatedField '$path' has empty expression (declarative-only?)" - } - } - - # Warn if collides with a dataset field - if ($allFieldPaths.ContainsKey($path)) { - Report-Warn "CalculatedField '$path' shadows dataSet field in '$($allFieldPaths[$path])'" - } - } - - if ($cfOk) { - Report-OK "$($calcFieldNodes.Count) calculatedField(s): dataPath and expression valid" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 10. TotalField checks --- - -if ($totalFieldNodes.Count -gt 0) { - $tfOk = $true - foreach ($tf in $totalFieldNodes) { - $dp = $tf.SelectSingleNode("s:dataPath", $ns) - $expr = $tf.SelectSingleNode("s:expression", $ns) - - if (-not $dp -or -not $dp.InnerText) { - Report-Error "TotalField has empty dataPath" - $tfOk = $false - continue - } - - if (-not $expr -or -not $expr.InnerText.Trim()) { - Report-Error "TotalField '$($dp.InnerText)' has empty expression" - $tfOk = $false - } - } - - if ($tfOk) { - Report-OK "$($totalFieldNodes.Count) totalField(s): dataPath and expression present" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 11. Parameter checks --- - -if ($paramNodes.Count -gt 0) { - $paramOk = $true - $paramSeen = @{} - foreach ($p in $paramNodes) { - $nameNode = $p.SelectSingleNode("s:name", $ns) - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "Parameter has empty name" - $paramOk = $false - continue - } - $pName = $nameNode.InnerText - if ($paramSeen.ContainsKey($pName)) { - Report-Error "Duplicate parameter name: $pName" - $paramOk = $false - } else { - $paramSeen[$pName] = $true - } - } - if ($paramOk) { - Report-OK "$($paramNodes.Count) parameter(s): names unique" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 12. Template checks --- - -if ($templateNodes.Count -gt 0) { - $tplOk = $true - $tplSeen = @{} - foreach ($t in $templateNodes) { - $nameNode = $t.SelectSingleNode("s:name", $ns) - if (-not $nameNode -or -not $nameNode.InnerText) { - Report-Error "Template has empty name" - $tplOk = $false - continue - } - $tName = $nameNode.InnerText - if ($tplSeen.ContainsKey($tName)) { - # Vendor configs (ERP/БП) ship templates with repeating names — the - # platform identifies them by position/context, not by <name>. Demote - # to warning so the check still surfaces the collision without failing. - Report-Warn "Duplicate template name: $tName (allowed by platform but ambiguous)" - } else { - $tplSeen[$tName] = $true - } - } - if ($tplOk) { - Report-OK "$($templateNodes.Count) template(s) found" - } -} - -# --- 13. GroupTemplate checks --- - -if ($groupTemplateNodes.Count -gt 0) { - $gtOk = $true - $validTplTypes = @("Header", "Footer", "Overall", "OverallHeader", "OverallFooter") - foreach ($gt in $groupTemplateNodes) { - $tplRef = $gt.SelectSingleNode("s:template", $ns) - $tplType = $gt.SelectSingleNode("s:templateType", $ns) - - if ($tplRef -and $tplRef.InnerText -and -not $templateNames.ContainsKey($tplRef.InnerText)) { - Report-Error "GroupTemplate references unknown template: $($tplRef.InnerText)" - $gtOk = $false - } - if ($tplType -and $validTplTypes -notcontains $tplType.InnerText) { - Report-Warn "GroupTemplate has unusual templateType: $($tplType.InnerText)" - } - } - if ($gtOk) { - Report-OK "$($groupTemplateNodes.Count) groupTemplate(s): references valid" - } -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 14. Settings helper functions --- - -$validComparisonTypes = @( - "Equal","NotEqual","Greater","GreaterOrEqual","Less","LessOrEqual", - "InList","NotInList","InHierarchy","NotInHierarchy", - "InListByHierarchy","NotInListByHierarchy", - "Contains","NotContains","BeginsWith","NotBeginsWith", - "Filled","NotFilled" -) - -$validStructureTypes = @( - "dcsset:StructureItemGroup", - "dcsset:StructureItemTable", - "dcsset:StructureItemChart", - "dcsset:StructureItemNestedObject" -) - -function Check-FilterItems { - param($parentNode, [string]$variantName) - - $filterItems = $parentNode.SelectNodes("dcsset:filter/dcsset:item", $ns) - foreach ($fi in $filterItems) { - if ($script:stopped) { return } - $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -eq "dcsset:FilterItemComparison") { - $compType = $fi.SelectSingleNode("dcsset:comparisonType", $ns) - if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { - Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" - } - } elseif ($xsiType -eq "dcsset:FilterItemGroup") { - $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) - if ($groupType) { - $validGroupTypes = @("AndGroup","OrGroup","NotGroup") - if ($validGroupTypes -notcontains $groupType.InnerText) { - Report-Warn "Variant '$variantName' filter group: unusual groupType '$($groupType.InnerText)'" - } - } - # Recurse into nested items - $nestedItems = $fi.SelectNodes("dcsset:item", $ns) - foreach ($ni in $nestedItems) { - $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($niType -eq "dcsset:FilterItemComparison") { - $compType = $ni.SelectSingleNode("dcsset:comparisonType", $ns) - if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { - Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" - } - } - } - } - } -} - -function Check-StructureItem { - param($itemNode, [string]$variantName) - - if ($script:stopped) { return } - - $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if (-not $xsiType) { - Report-Error "Variant '$variantName': structure item missing xsi:type" - return - } - if ($validStructureTypes -notcontains $xsiType) { - Report-Warn "Variant '$variantName': unusual structure item type '$xsiType'" - } - - # Recurse into nested items (groups can contain groups) - $nestedItems = $itemNode.SelectNodes("dcsset:item", $ns) - foreach ($ni in $nestedItems) { - Check-StructureItem -itemNode $ni -variantName $variantName - } - - # Check column/row in tables - if ($xsiType -eq "dcsset:StructureItemTable") { - $columns = $itemNode.SelectNodes("dcsset:column", $ns) - $rows = $itemNode.SelectNodes("dcsset:row", $ns) - if ($columns.Count -eq 0) { - Report-Warn "Variant '$variantName': table has no columns" - } - if ($rows.Count -eq 0) { - Report-Warn "Variant '$variantName': table has no rows" - } - } -} - -function Check-Settings { - param($settingsNode, [string]$variantName) - - if ($script:stopped) { return } - - # Selection - $selItems = $settingsNode.SelectNodes("dcsset:selection/dcsset:item", $ns) - foreach ($si in $selItems) { - $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -eq "dcsset:SelectedItemField") { - $field = $si.SelectSingleNode("dcsset:field", $ns) - if ($field -and $field.InnerText -and $field.InnerText -ne "SystemFields.Number") { - $basePath = ($field.InnerText -split '\.')[0] - if (-not $knownFields.ContainsKey($field.InnerText) -and -not $knownFields.ContainsKey($basePath)) { - # Soft check — autoFillFields may add fields not listed explicitly - } - } - } - } - - # Filter - Check-FilterItems -parentNode $settingsNode -variantName $variantName - - # Order - $orderItems = $settingsNode.SelectNodes("dcsset:order/dcsset:item", $ns) - foreach ($oi in $orderItems) { - $xsiType = $oi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - if ($xsiType -eq "dcsset:OrderItemField") { - $orderType = $oi.SelectSingleNode("dcsset:orderType", $ns) - if ($orderType -and $orderType.InnerText -ne "Asc" -and $orderType.InnerText -ne "Desc") { - Report-Warn "Variant '$variantName' order: invalid orderType '$($orderType.InnerText)'" - } - } - } - - # Structure items - $structItems = $settingsNode.SelectNodes("dcsset:item", $ns) - foreach ($si in $structItems) { - Check-StructureItem -itemNode $si -variantName $variantName - } -} - -# --- 15. SettingsVariant checks --- - -if ($variantNodes.Count -eq 0) { - Report-Warn "No settingsVariant elements found" -} else { - $vOk = $true - $vIdx = 0 - foreach ($v in $variantNodes) { - $vIdx++ - $vName = $v.SelectSingleNode("dcsset:name", $ns) - if (-not $vName -or -not $vName.InnerText) { - Report-Error "SettingsVariant #$vIdx has empty name" - $vOk = $false - } - - $settings = $v.SelectSingleNode("dcsset:settings", $ns) - if (-not $settings) { - Report-Error "SettingsVariant '$($vName.InnerText)' has no settings element" - $vOk = $false - continue - } - - # Check settings internals - Check-Settings -settingsNode $settings -variantName "$($vName.InnerText)" - } - - if ($vOk) { - Report-OK "$($variantNodes.Count) settingsVariant(s) found" - } -} - -# --- 16. valueType structural checks --- -# Catches broken XDTO that XML/structural checks miss (decimal without xs:, -# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens). - -$validTypeQualifier = @{ - 'xs:decimal' = 'v8:NumberQualifiers' - 'xs:string' = 'v8:StringQualifiers' - 'xs:dateTime' = 'v8:DateQualifiers' - 'xs:boolean' = '' - 'v8:StandardPeriod' = '' - 'v8:UUID' = '' - 'v8:Null' = '' - 'v8:Type' = '' - 'v8:ValueStorage' = '' -} -$validSign = @('Any', 'Nonnegative', 'Negative') -$validLength = @('Variable', 'Fixed') -$validFractions = @('Date', 'DateTime', 'Time') - -# DCS supports composite types: multiple <v8:Type> blocks may share a single -# trailing qualifier block (e.g. xs:string + CatalogRef.X + StringQualifiers). -# So we collect all types and qualifiers per valueType, then check consistency. -$qualifierProducers = @{ - 'v8:NumberQualifiers' = 'xs:decimal' - 'v8:StringQualifiers' = 'xs:string' - 'v8:DateQualifiers' = 'xs:dateTime' -} - -$valueTypeNodes = $root.SelectNodes("//s:valueType", $ns) -$vtChecked = 0 -$vtOk = $true -foreach ($vt in $valueTypeNodes) { - $vtChecked++ - $types = @() # list of short type strings; '' marks a ref type - $qualifiers = @() # list of @{ name = 'v8:XQualifiers'; node = $child } - - foreach ($child in $vt.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.NamespaceURI -ne 'http://v8.1c.ru/8.1/data/core') { continue } - $localName = $child.LocalName - - if ($localName -eq 'Type') { - $t = "$($child.InnerText)".Trim() - if (-not $t) { - Report-Error "valueType: <v8:Type> is empty" - $vtOk = $false - continue - } - if ($t -match '^([A-Za-z][A-Za-z0-9]*):(.+)$') { - $prefix = $Matches[1] - $localT = $Matches[2] - if ($prefix -eq 'xs' -or $prefix -eq 'v8') { - if (-not $validTypeQualifier.ContainsKey($t)) { - Report-Error "valueType: unknown type '$t' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or <prefix>:*Ref.X)" - $vtOk = $false - } else { - $types += $t - } - } else { - $prefixNs = $child.GetNamespaceOfPrefix($prefix) - if ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise/current-config') { - if (-not ($localT -match '^[A-Za-z]+(Ref)?\.')) { - Report-Error "valueType: ref type '$t' must look like '<prefix>:<Kind>.<Name>' (e.g. d5p1:CatalogRef.X)" - $vtOk = $false - } else { - $types += '' # ref — no qualifier needed - } - } elseif ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise') { - # System types: AccumulationRecordType etc. — no qualifiers - if (-not ($localT -match '^[A-Za-z][A-Za-z0-9]*$')) { - Report-Error "valueType: system type '$t' has unexpected local-name shape" - $vtOk = $false - } else { - $types += '' - } - } else { - Report-Error "valueType: type '$t' uses prefix '$prefix' bound to unexpected namespace '$prefixNs'" - $vtOk = $false - } - } - } else { - Report-Error "valueType: type '$t' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)" - $vtOk = $false - } - } elseif ($localName -match 'Qualifiers$') { - $qName = "v8:$localName" - $qualifiers += @{ name = $qName; node = $child } - # Validate qualifier internals - if ($qName -eq 'v8:NumberQualifiers') { - $digits = $child.SelectSingleNode("v8:Digits", $ns) - $frac = $child.SelectSingleNode("v8:FractionDigits", $ns) - $sign = $child.SelectSingleNode("v8:AllowedSign", $ns) - if (-not $digits -or -not ($digits.InnerText -match '^\d+$')) { - Report-Error "v8:NumberQualifiers: <v8:Digits> missing or not a non-negative integer" - $vtOk = $false - } - if (-not $frac -or -not ($frac.InnerText -match '^\d+$')) { - Report-Error "v8:NumberQualifiers: <v8:FractionDigits> missing or not a non-negative integer" - $vtOk = $false - } - if ($sign -and $sign.InnerText -and $sign.InnerText -notin $validSign) { - Report-Error "v8:NumberQualifiers: <v8:AllowedSign>$($sign.InnerText)</v8:AllowedSign> — must be one of: $($validSign -join ', ')" - $vtOk = $false - } - } elseif ($qName -eq 'v8:StringQualifiers') { - $len = $child.SelectSingleNode("v8:Length", $ns) - $al = $child.SelectSingleNode("v8:AllowedLength", $ns) - if (-not $len -or -not ($len.InnerText -match '^\d+$')) { - Report-Error "v8:StringQualifiers: <v8:Length> missing or not a non-negative integer" - $vtOk = $false - } - if ($al -and $al.InnerText -and $al.InnerText -notin $validLength) { - Report-Error "v8:StringQualifiers: <v8:AllowedLength>$($al.InnerText)</v8:AllowedLength> — must be one of: $($validLength -join ', ')" - $vtOk = $false - } - } elseif ($qName -eq 'v8:DateQualifiers') { - $df = $child.SelectSingleNode("v8:DateFractions", $ns) - if ($df -and $df.InnerText -and $df.InnerText -notin $validFractions) { - Report-Error "v8:DateQualifiers: <v8:DateFractions>$($df.InnerText)</v8:DateFractions> — must be one of: $($validFractions -join ', ')" - $vtOk = $false - } - } - } - } - - # Cross-check: every qualifier must have a matching scalar type in this valueType - foreach ($q in $qualifiers) { - $producer = $qualifierProducers[$q.name] - if (-not $producer) { continue } - if ($types -notcontains $producer) { - Report-Error "valueType: <$($q.name)> has no matching <v8:Type>$producer</v8:Type> in this valueType" - $vtOk = $false - } - } -} -if ($vtChecked -gt 0 -and $vtOk) { - Report-OK "$vtChecked valueType block(s): structure and qualifiers OK" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- 17. value content checks --- -# Catches literal placeholders ("_") and empty strings in DesignTimeValue refs -# that XDTO would reject at db-load-xml. - -$valueNodes = @() -$valueNodes += @($root.SelectNodes("//s:value[@xsi:type]", $ns)) -$valueNodes += @($root.SelectNodes("//dcscor:value[@xsi:type]", $ns)) -$vChecked = 0 -$vOk = $true -foreach ($vn in $valueNodes) { - if (-not $vn) { continue } - $vChecked++ - $xsiType = $vn.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - $text = $vn.InnerText - if ($xsiType -eq 'dcscor:DesignTimeValue') { - if (-not $text -or $text.Trim() -eq '' -or $text.Trim() -eq '_') { - Report-Error "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '$text'" - $vOk = $false - } elseif (-not ($text -match '^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+')) { - Report-Warn "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — doesn't look like a typical ref path" - } - } -} -if ($vChecked -gt 0 -and $vOk) { - Report-OK "$vChecked <value> element(s) with xsi:type: content OK" -} - -if ($script:stopped) { & $finalize; exit 1 } - -# --- Final output --- - -& $finalize - -if ($script:errors -gt 0) { - exit 1 -} -exit 0 +# skd-validate v1.2 — Validate 1C DCS structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$TemplatePath, + + [switch]$Detailed, + + [int]$MaxErrors = 20, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($TemplatePath)) { + $TemplatePath = Join-Path (Get-Location).Path $TemplatePath +} +# A: Directory → Ext/Template.xml +if (Test-Path $TemplatePath -PathType Container) { + $TemplatePath = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" +} +# B1: Missing Ext/ (e.g. Templates/СКД/Template.xml → Templates/СКД/Ext/Template.xml) +if (-not (Test-Path $TemplatePath)) { + $fn = [System.IO.Path]::GetFileName($TemplatePath) + if ($fn -eq "Template.xml") { + $c = Join-Path (Join-Path (Split-Path $TemplatePath) "Ext") $fn + if (Test-Path $c) { $TemplatePath = $c } + } +} +# B2: Descriptor (Templates/СКД.xml → Templates/СКД/Ext/Template.xml) +if (-not (Test-Path $TemplatePath) -and $TemplatePath.EndsWith(".xml")) { + $stem = [System.IO.Path]::GetFileNameWithoutExtension($TemplatePath) + $dir = Split-Path $TemplatePath + $c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Template.xml" + if (Test-Path $c) { $TemplatePath = $c } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path +$fileName = [System.IO.Path]::GetFileName($resolvedPath) + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 4096 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: $fileName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +Out-Line "=== Validation: $fileName ===" +Out-Line "" + +# --- 1. Parse XML --- + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) + Report-OK "XML parsed successfully" +} catch { + Report-Error "XML parse failed: $($_.Exception.Message)" + # Cannot continue + $result = $script:output.ToString() + Write-Host $result + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + } + exit 1 +} + +# --- 2. Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") +$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") +$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") + +$root = $xmlDoc.DocumentElement + +# --- 3. Root element checks --- + +if ($root.LocalName -ne "DataCompositionSchema") { + Report-Error "Root element is '$($root.LocalName)', expected 'DataCompositionSchema'" +} else { + Report-OK "Root element: DataCompositionSchema" +} + +$expectedNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "Default namespace is '$($root.NamespaceURI)', expected '$expectedNs'" +} else { + Report-OK "Default namespace correct" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 4. Collect inventories --- + +# DataSources +$dataSourceNodes = $root.SelectNodes("s:dataSource", $ns) +$dataSourceNames = @{} +foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + if ($name) { $dataSourceNames[$name.InnerText] = $true } +} + +# DataSets (recursive for unions) +$dataSetNodes = $root.SelectNodes("s:dataSet", $ns) +$dataSetNames = @{} +$allFieldPaths = @{} # Global: dataPath → dataSet name + +function Collect-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + $localPaths = @{} + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if ($dp) { + $path = $dp.InnerText + $localPaths[$path] = $true + $allFieldPaths[$path] = $dsName + } + } + + # Union items + $items = $dsNode.SelectNodes("s:item", $ns) + foreach ($item in $items) { + $itemName = $item.SelectSingleNode("s:name", $ns) + if ($itemName) { + Collect-DataSetFields -dsNode $item -dsName $itemName.InnerText + } + } + + return $localPaths +} + +$dataSetFieldMap = @{} # dsName → hashtable of dataPath +foreach ($ds in $dataSetNodes) { + $nameNode = $ds.SelectSingleNode("s:name", $ns) + if ($nameNode) { + $dsName = $nameNode.InnerText + $dataSetNames[$dsName] = $true + $dataSetFieldMap[$dsName] = Collect-DataSetFields -dsNode $ds -dsName $dsName + } +} + +# CalculatedFields +$calcFieldNodes = $root.SelectNodes("s:calculatedField", $ns) +$calcFieldPaths = @{} +foreach ($cf in $calcFieldNodes) { + $dp = $cf.SelectSingleNode("s:dataPath", $ns) + if ($dp) { $calcFieldPaths[$dp.InnerText] = $true } +} + +# TotalFields +$totalFieldNodes = $root.SelectNodes("s:totalField", $ns) + +# Parameters +$paramNodes = $root.SelectNodes("s:parameter", $ns) +$paramNames = @{} +foreach ($p in $paramNodes) { + $nameNode = $p.SelectSingleNode("s:name", $ns) + if ($nameNode) { $paramNames[$nameNode.InnerText] = $true } +} + +# Templates +$templateNodes = $root.SelectNodes("s:template", $ns) +$templateNames = @{} +foreach ($t in $templateNodes) { + $nameNode = $t.SelectSingleNode("s:name", $ns) + if ($nameNode) { $templateNames[$nameNode.InnerText] = $true } +} + +# GroupTemplates +$groupTemplateNodes = $root.SelectNodes("s:groupTemplate", $ns) + +# SettingsVariants +$variantNodes = $root.SelectNodes("s:settingsVariant", $ns) + +# Known fields = dataset fields + calculated fields +$knownFields = @{} +foreach ($key in $allFieldPaths.Keys) { $knownFields[$key] = $true } +foreach ($key in $calcFieldPaths.Keys) { $knownFields[$key] = $true } + +# --- 5. DataSource checks --- + +if ($dataSourceNodes.Count -eq 0) { + Report-Warn "No dataSource elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + $type = $dsn.SelectSingleNode("s:dataSourceType", $ns) + if (-not $name -or -not $name.InnerText) { + Report-Error "DataSource has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($name.InnerText)) { + Report-Error "Duplicate dataSource name: $($name.InnerText)" + $dsOk = $false + } else { + $dsNamesSeen[$name.InnerText] = $true + } + if ($type) { + $tv = $type.InnerText + if ($tv -ne "Local" -and $tv -ne "External") { + Report-Warn "DataSource '$($name.InnerText)' has unusual type: $tv" + } + } + } + if ($dsOk) { + Report-OK "$($dataSourceNodes.Count) dataSource(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 6. DataSet checks --- + +$validDsTypes = @("DataSetQuery", "DataSetObject", "DataSetUnion") + +if ($dataSetNodes.Count -eq 0) { + Report-Warn "No dataSet elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($ds in $dataSetNodes) { + $xsiType = $ds.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $nameNode = $ds.SelectSingleNode("s:name", $ns) + $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } + + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "DataSet has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($dsName)) { + Report-Error "Duplicate dataSet name: $dsName" + $dsOk = $false + } else { + $dsNamesSeen[$dsName] = $true + } + + if (-not $xsiType) { + Report-Error "DataSet '$dsName' missing xsi:type" + $dsOk = $false + } elseif ($validDsTypes -notcontains $xsiType) { + Report-Warn "DataSet '$dsName' has unusual xsi:type: $xsiType" + } + + # Check dataSource reference + if ($xsiType -ne "DataSetUnion") { + $srcNode = $ds.SelectSingleNode("s:dataSource", $ns) + if ($srcNode -and $srcNode.InnerText) { + if (-not $dataSourceNames.ContainsKey($srcNode.InnerText)) { + Report-Error "DataSet '$dsName' references unknown dataSource: $($srcNode.InnerText)" + $dsOk = $false + } + } + } + + # Check query not empty for Query type + if ($xsiType -eq "DataSetQuery") { + $queryNode = $ds.SelectSingleNode("s:query", $ns) + if (-not $queryNode -or -not $queryNode.InnerText.Trim()) { + Report-Warn "DataSet '$dsName' (Query) has empty query" + } + } + + # Check objectName for Object type + if ($xsiType -eq "DataSetObject") { + $objNode = $ds.SelectSingleNode("s:objectName", $ns) + if (-not $objNode -or -not $objNode.InnerText.Trim()) { + Report-Error "DataSet '$dsName' (Object) has empty objectName" + $dsOk = $false + } + } + } + + if ($dsOk) { + Report-OK "$($dataSetNodes.Count) dataSet(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 7. Field checks --- + +function Check-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + if ($fields.Count -eq 0) { return } + + $pathsSeen = @{} + $fieldOk = $true + + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + $fn = $f.SelectSingleNode("s:field", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "DataSet '$dsName': field has empty dataPath" + $fieldOk = $false + continue + } + + $path = $dp.InnerText + if ($pathsSeen.ContainsKey($path)) { + Report-Warn "DataSet '$dsName': duplicate dataPath '$path'" + } else { + $pathsSeen[$path] = $true + } + + if (-not $fn -or -not $fn.InnerText) { + Report-Warn "DataSet '$dsName': field '$path' has empty <field> element" + } + } + + if ($fieldOk) { + Report-OK "DataSet `"$dsName`": $($fields.Count) fields, dataPath unique" + } + + # Check union items recursively + $items = $dsNode.SelectNodes("s:item", $ns) + foreach ($item in $items) { + $itemName = $item.SelectSingleNode("s:name", $ns) + $iName = if ($itemName) { $itemName.InnerText } else { "(unnamed item)" } + Check-DataSetFields -dsNode $item -dsName $iName + } +} + +foreach ($ds in $dataSetNodes) { + $nameNode = $ds.SelectSingleNode("s:name", $ns) + $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } + Check-DataSetFields -dsNode $ds -dsName $dsName +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 8. DataSetLink checks --- + +$linkNodes = $root.SelectNodes("s:dataSetLink", $ns) +if ($linkNodes.Count -gt 0) { + $linkOk = $true + foreach ($link in $linkNodes) { + $src = $link.SelectSingleNode("s:sourceDataSet", $ns) + $dst = $link.SelectSingleNode("s:destinationDataSet", $ns) + $srcExpr = $link.SelectSingleNode("s:sourceExpression", $ns) + $dstExpr = $link.SelectSingleNode("s:destinationExpression", $ns) + + if ($src -and $src.InnerText -and -not $dataSetNames.ContainsKey($src.InnerText)) { + Report-Error "DataSetLink: sourceDataSet '$($src.InnerText)' not found" + $linkOk = $false + } + if ($dst -and $dst.InnerText -and -not $dataSetNames.ContainsKey($dst.InnerText)) { + Report-Error "DataSetLink: destinationDataSet '$($dst.InnerText)' not found" + $linkOk = $false + } + if (-not $srcExpr -or -not $srcExpr.InnerText.Trim()) { + Report-Error "DataSetLink: empty sourceExpression" + $linkOk = $false + } + if (-not $dstExpr -or -not $dstExpr.InnerText.Trim()) { + Report-Error "DataSetLink: empty destinationExpression" + $linkOk = $false + } + } + if ($linkOk) { + Report-OK "$($linkNodes.Count) dataSetLink(s): references valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 9. CalculatedField checks --- + +if ($calcFieldNodes.Count -gt 0) { + $cfOk = $true + $cfSeen = @{} + # Collect totalField dataPaths — an empty calculatedField is legitimate if a + # totalField with the same dataPath provides the expression (real-world + # pattern in vendor ERP/БП reports for fields visible only in totals). + $tfPaths = @{} + foreach ($tf in $totalFieldNodes) { + $tfDp = $tf.SelectSingleNode("s:dataPath", $ns) + if ($tfDp -and $tfDp.InnerText) { + $tfPaths[$tfDp.InnerText] = $true + } + } + + foreach ($cf in $calcFieldNodes) { + $dp = $cf.SelectSingleNode("s:dataPath", $ns) + $expr = $cf.SelectSingleNode("s:expression", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "CalculatedField has empty dataPath" + $cfOk = $false + continue + } + + $path = $dp.InnerText + if ($cfSeen.ContainsKey($path)) { + Report-Error "Duplicate calculatedField dataPath: $path" + $cfOk = $false + } else { + $cfSeen[$path] = $true + } + + if (-not $expr -or -not $expr.InnerText.Trim()) { + # Empty expression is legitimate in several vendor patterns: + # - totalField with same dataPath provides the calculation + # - groupTemplate uses the field as group name (declarative only) + # - field is referenced only by settingsVariants for grouping + # Surface as warning, not error, to avoid false positives on real + # ERP/БП reports while still flagging the unusual shape. + if (-not $tfPaths.ContainsKey($path)) { + Report-Warn "CalculatedField '$path' has empty expression (declarative-only?)" + } + } + + # Warn if collides with a dataset field + if ($allFieldPaths.ContainsKey($path)) { + Report-Warn "CalculatedField '$path' shadows dataSet field in '$($allFieldPaths[$path])'" + } + } + + if ($cfOk) { + Report-OK "$($calcFieldNodes.Count) calculatedField(s): dataPath and expression valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 10. TotalField checks --- + +if ($totalFieldNodes.Count -gt 0) { + $tfOk = $true + foreach ($tf in $totalFieldNodes) { + $dp = $tf.SelectSingleNode("s:dataPath", $ns) + $expr = $tf.SelectSingleNode("s:expression", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "TotalField has empty dataPath" + $tfOk = $false + continue + } + + if (-not $expr -or -not $expr.InnerText.Trim()) { + Report-Error "TotalField '$($dp.InnerText)' has empty expression" + $tfOk = $false + } + } + + if ($tfOk) { + Report-OK "$($totalFieldNodes.Count) totalField(s): dataPath and expression present" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 11. Parameter checks --- + +if ($paramNodes.Count -gt 0) { + $paramOk = $true + $paramSeen = @{} + foreach ($p in $paramNodes) { + $nameNode = $p.SelectSingleNode("s:name", $ns) + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "Parameter has empty name" + $paramOk = $false + continue + } + $pName = $nameNode.InnerText + if ($paramSeen.ContainsKey($pName)) { + Report-Error "Duplicate parameter name: $pName" + $paramOk = $false + } else { + $paramSeen[$pName] = $true + } + } + if ($paramOk) { + Report-OK "$($paramNodes.Count) parameter(s): names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 12. Template checks --- + +if ($templateNodes.Count -gt 0) { + $tplOk = $true + $tplSeen = @{} + foreach ($t in $templateNodes) { + $nameNode = $t.SelectSingleNode("s:name", $ns) + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "Template has empty name" + $tplOk = $false + continue + } + $tName = $nameNode.InnerText + if ($tplSeen.ContainsKey($tName)) { + # Vendor configs (ERP/БП) ship templates with repeating names — the + # platform identifies them by position/context, not by <name>. Demote + # to warning so the check still surfaces the collision without failing. + Report-Warn "Duplicate template name: $tName (allowed by platform but ambiguous)" + } else { + $tplSeen[$tName] = $true + } + } + if ($tplOk) { + Report-OK "$($templateNodes.Count) template(s) found" + } +} + +# --- 13. GroupTemplate checks --- + +if ($groupTemplateNodes.Count -gt 0) { + $gtOk = $true + $validTplTypes = @("Header", "Footer", "Overall", "OverallHeader", "OverallFooter") + foreach ($gt in $groupTemplateNodes) { + $tplRef = $gt.SelectSingleNode("s:template", $ns) + $tplType = $gt.SelectSingleNode("s:templateType", $ns) + + if ($tplRef -and $tplRef.InnerText -and -not $templateNames.ContainsKey($tplRef.InnerText)) { + Report-Error "GroupTemplate references unknown template: $($tplRef.InnerText)" + $gtOk = $false + } + if ($tplType -and $validTplTypes -notcontains $tplType.InnerText) { + Report-Warn "GroupTemplate has unusual templateType: $($tplType.InnerText)" + } + } + if ($gtOk) { + Report-OK "$($groupTemplateNodes.Count) groupTemplate(s): references valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 14. Settings helper functions --- + +$validComparisonTypes = @( + "Equal","NotEqual","Greater","GreaterOrEqual","Less","LessOrEqual", + "InList","NotInList","InHierarchy","NotInHierarchy", + "InListByHierarchy","NotInListByHierarchy", + "Contains","NotContains","BeginsWith","NotBeginsWith", + "Filled","NotFilled" +) + +$validStructureTypes = @( + "dcsset:StructureItemGroup", + "dcsset:StructureItemTable", + "dcsset:StructureItemChart", + "dcsset:StructureItemNestedObject" +) + +function Check-FilterItems { + param($parentNode, [string]$variantName) + + $filterItems = $parentNode.SelectNodes("dcsset:filter/dcsset:item", $ns) + foreach ($fi in $filterItems) { + if ($script:stopped) { return } + $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:FilterItemComparison") { + $compType = $fi.SelectSingleNode("dcsset:comparisonType", $ns) + if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { + Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" + } + } elseif ($xsiType -eq "dcsset:FilterItemGroup") { + $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) + if ($groupType) { + $validGroupTypes = @("AndGroup","OrGroup","NotGroup") + if ($validGroupTypes -notcontains $groupType.InnerText) { + Report-Warn "Variant '$variantName' filter group: unusual groupType '$($groupType.InnerText)'" + } + } + # Recurse into nested items + $nestedItems = $fi.SelectNodes("dcsset:item", $ns) + foreach ($ni in $nestedItems) { + $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($niType -eq "dcsset:FilterItemComparison") { + $compType = $ni.SelectSingleNode("dcsset:comparisonType", $ns) + if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { + Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" + } + } + } + } + } +} + +function Check-StructureItem { + param($itemNode, [string]$variantName) + + if ($script:stopped) { return } + + $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if (-not $xsiType) { + Report-Error "Variant '$variantName': structure item missing xsi:type" + return + } + if ($validStructureTypes -notcontains $xsiType) { + Report-Warn "Variant '$variantName': unusual structure item type '$xsiType'" + } + + # Recurse into nested items (groups can contain groups) + $nestedItems = $itemNode.SelectNodes("dcsset:item", $ns) + foreach ($ni in $nestedItems) { + Check-StructureItem -itemNode $ni -variantName $variantName + } + + # Check column/row in tables + if ($xsiType -eq "dcsset:StructureItemTable") { + $columns = $itemNode.SelectNodes("dcsset:column", $ns) + $rows = $itemNode.SelectNodes("dcsset:row", $ns) + if ($columns.Count -eq 0) { + Report-Warn "Variant '$variantName': table has no columns" + } + if ($rows.Count -eq 0) { + Report-Warn "Variant '$variantName': table has no rows" + } + } +} + +function Check-Settings { + param($settingsNode, [string]$variantName) + + if ($script:stopped) { return } + + # Selection + $selItems = $settingsNode.SelectNodes("dcsset:selection/dcsset:item", $ns) + foreach ($si in $selItems) { + $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:SelectedItemField") { + $field = $si.SelectSingleNode("dcsset:field", $ns) + if ($field -and $field.InnerText -and $field.InnerText -ne "SystemFields.Number") { + $basePath = ($field.InnerText -split '\.')[0] + if (-not $knownFields.ContainsKey($field.InnerText) -and -not $knownFields.ContainsKey($basePath)) { + # Soft check — autoFillFields may add fields not listed explicitly + } + } + } + } + + # Filter + Check-FilterItems -parentNode $settingsNode -variantName $variantName + + # Order + $orderItems = $settingsNode.SelectNodes("dcsset:order/dcsset:item", $ns) + foreach ($oi in $orderItems) { + $xsiType = $oi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:OrderItemField") { + $orderType = $oi.SelectSingleNode("dcsset:orderType", $ns) + if ($orderType -and $orderType.InnerText -ne "Asc" -and $orderType.InnerText -ne "Desc") { + Report-Warn "Variant '$variantName' order: invalid orderType '$($orderType.InnerText)'" + } + } + } + + # Structure items + $structItems = $settingsNode.SelectNodes("dcsset:item", $ns) + foreach ($si in $structItems) { + Check-StructureItem -itemNode $si -variantName $variantName + } +} + +# --- 15. SettingsVariant checks --- + +if ($variantNodes.Count -eq 0) { + Report-Warn "No settingsVariant elements found" +} else { + $vOk = $true + $vIdx = 0 + foreach ($v in $variantNodes) { + $vIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns) + if (-not $vName -or -not $vName.InnerText) { + Report-Error "SettingsVariant #$vIdx has empty name" + $vOk = $false + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + if (-not $settings) { + Report-Error "SettingsVariant '$($vName.InnerText)' has no settings element" + $vOk = $false + continue + } + + # Check settings internals + Check-Settings -settingsNode $settings -variantName "$($vName.InnerText)" + } + + if ($vOk) { + Report-OK "$($variantNodes.Count) settingsVariant(s) found" + } +} + +# --- 16. valueType structural checks --- +# Catches broken XDTO that XML/structural checks miss (decimal without xs:, +# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens). + +$validTypeQualifier = @{ + 'xs:decimal' = 'v8:NumberQualifiers' + 'xs:string' = 'v8:StringQualifiers' + 'xs:dateTime' = 'v8:DateQualifiers' + 'xs:boolean' = '' + 'v8:StandardPeriod' = '' + 'v8:UUID' = '' + 'v8:Null' = '' + 'v8:Type' = '' + 'v8:ValueStorage' = '' +} +$validSign = @('Any', 'Nonnegative', 'Negative') +$validLength = @('Variable', 'Fixed') +$validFractions = @('Date', 'DateTime', 'Time') + +# DCS supports composite types: multiple <v8:Type> blocks may share a single +# trailing qualifier block (e.g. xs:string + CatalogRef.X + StringQualifiers). +# So we collect all types and qualifiers per valueType, then check consistency. +$qualifierProducers = @{ + 'v8:NumberQualifiers' = 'xs:decimal' + 'v8:StringQualifiers' = 'xs:string' + 'v8:DateQualifiers' = 'xs:dateTime' +} + +$valueTypeNodes = $root.SelectNodes("//s:valueType", $ns) +$vtChecked = 0 +$vtOk = $true +foreach ($vt in $valueTypeNodes) { + $vtChecked++ + $types = @() # list of short type strings; '' marks a ref type + $qualifiers = @() # list of @{ name = 'v8:XQualifiers'; node = $child } + + foreach ($child in $vt.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.NamespaceURI -ne 'http://v8.1c.ru/8.1/data/core') { continue } + $localName = $child.LocalName + + if ($localName -eq 'Type') { + $t = "$($child.InnerText)".Trim() + if (-not $t) { + Report-Error "valueType: <v8:Type> is empty" + $vtOk = $false + continue + } + if ($t -match '^([A-Za-z][A-Za-z0-9]*):(.+)$') { + $prefix = $Matches[1] + $localT = $Matches[2] + if ($prefix -eq 'xs' -or $prefix -eq 'v8') { + if (-not $validTypeQualifier.ContainsKey($t)) { + Report-Error "valueType: unknown type '$t' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or <prefix>:*Ref.X)" + $vtOk = $false + } else { + $types += $t + } + } else { + $prefixNs = $child.GetNamespaceOfPrefix($prefix) + if ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise/current-config') { + if (-not ($localT -match '^[A-Za-z]+(Ref)?\.')) { + Report-Error "valueType: ref type '$t' must look like '<prefix>:<Kind>.<Name>' (e.g. d5p1:CatalogRef.X)" + $vtOk = $false + } else { + $types += '' # ref — no qualifier needed + } + } elseif ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise') { + # System types: AccumulationRecordType etc. — no qualifiers + if (-not ($localT -match '^[A-Za-z][A-Za-z0-9]*$')) { + Report-Error "valueType: system type '$t' has unexpected local-name shape" + $vtOk = $false + } else { + $types += '' + } + } else { + Report-Error "valueType: type '$t' uses prefix '$prefix' bound to unexpected namespace '$prefixNs'" + $vtOk = $false + } + } + } else { + Report-Error "valueType: type '$t' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)" + $vtOk = $false + } + } elseif ($localName -match 'Qualifiers$') { + $qName = "v8:$localName" + $qualifiers += @{ name = $qName; node = $child } + # Validate qualifier internals + if ($qName -eq 'v8:NumberQualifiers') { + $digits = $child.SelectSingleNode("v8:Digits", $ns) + $frac = $child.SelectSingleNode("v8:FractionDigits", $ns) + $sign = $child.SelectSingleNode("v8:AllowedSign", $ns) + if (-not $digits -or -not ($digits.InnerText -match '^\d+$')) { + Report-Error "v8:NumberQualifiers: <v8:Digits> missing or not a non-negative integer" + $vtOk = $false + } + if (-not $frac -or -not ($frac.InnerText -match '^\d+$')) { + Report-Error "v8:NumberQualifiers: <v8:FractionDigits> missing or not a non-negative integer" + $vtOk = $false + } + if ($sign -and $sign.InnerText -and $sign.InnerText -notin $validSign) { + Report-Error "v8:NumberQualifiers: <v8:AllowedSign>$($sign.InnerText)</v8:AllowedSign> — must be one of: $($validSign -join ', ')" + $vtOk = $false + } + } elseif ($qName -eq 'v8:StringQualifiers') { + $len = $child.SelectSingleNode("v8:Length", $ns) + $al = $child.SelectSingleNode("v8:AllowedLength", $ns) + if (-not $len -or -not ($len.InnerText -match '^\d+$')) { + Report-Error "v8:StringQualifiers: <v8:Length> missing or not a non-negative integer" + $vtOk = $false + } + if ($al -and $al.InnerText -and $al.InnerText -notin $validLength) { + Report-Error "v8:StringQualifiers: <v8:AllowedLength>$($al.InnerText)</v8:AllowedLength> — must be one of: $($validLength -join ', ')" + $vtOk = $false + } + } elseif ($qName -eq 'v8:DateQualifiers') { + $df = $child.SelectSingleNode("v8:DateFractions", $ns) + if ($df -and $df.InnerText -and $df.InnerText -notin $validFractions) { + Report-Error "v8:DateQualifiers: <v8:DateFractions>$($df.InnerText)</v8:DateFractions> — must be one of: $($validFractions -join ', ')" + $vtOk = $false + } + } + } + } + + # Cross-check: every qualifier must have a matching scalar type in this valueType + foreach ($q in $qualifiers) { + $producer = $qualifierProducers[$q.name] + if (-not $producer) { continue } + if ($types -notcontains $producer) { + Report-Error "valueType: <$($q.name)> has no matching <v8:Type>$producer</v8:Type> in this valueType" + $vtOk = $false + } + } +} +if ($vtChecked -gt 0 -and $vtOk) { + Report-OK "$vtChecked valueType block(s): structure and qualifiers OK" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 17. value content checks --- +# Catches literal placeholders ("_") and empty strings in DesignTimeValue refs +# that XDTO would reject at db-load-xml. + +$valueNodes = @() +$valueNodes += @($root.SelectNodes("//s:value[@xsi:type]", $ns)) +$valueNodes += @($root.SelectNodes("//dcscor:value[@xsi:type]", $ns)) +$vChecked = 0 +$vOk = $true +foreach ($vn in $valueNodes) { + if (-not $vn) { continue } + $vChecked++ + $xsiType = $vn.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $text = $vn.InnerText + if ($xsiType -eq 'dcscor:DesignTimeValue') { + if (-not $text -or $text.Trim() -eq '' -or $text.Trim() -eq '_') { + Report-Error "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '$text'" + $vOk = $false + } elseif (-not ($text -match '^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+')) { + Report-Warn "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — doesn't look like a typical ref path" + } + } +} +if ($vChecked -gt 0 -and $vOk) { + Report-OK "$vChecked <value> element(s) with xsi:type: content OK" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/subsystem-compile/SKILL.md b/.claude/skills/subsystem-compile/SKILL.md index ab840aa9..baacd7c1 100644 --- a/.claude/skills/subsystem-compile/SKILL.md +++ b/.claude/skills/subsystem-compile/SKILL.md @@ -1,59 +1,59 @@ ---- -name: subsystem-compile -description: Создать подсистему 1С — XML-исходники из JSON-определения. Используй когда нужно добавить подсистему (раздел) в конфигурацию -argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputDir <ConfigDir> [-Parent <path>]" -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /subsystem-compile — генерация подсистемы из JSON - -Принимает JSON-определение подсистемы → генерирует XML + файловую структуру + регистрирует в родителе (Configuration.xml или родительская подсистема). - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `DefinitionFile` | Путь к JSON-файлу определения | -| `Value` | Инлайн JSON-строка (альтернатива DefinitionFile) | -| `OutputDir` | Корень выгрузки (где `Subsystems/`, `Configuration.xml`) | -| `Parent` | Путь к XML родительской подсистемы (для вложенных) | -| `NoValidate` | Пропустить авто-валидацию | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-compile.ps1" -Value '<json>' -OutputDir '<ConfigDir>' -``` - -## JSON-определение - -```json -{ - "name": "МояПодсистема", - "synonym": "Моя подсистема", - "comment": "", - "includeInCommandInterface": true, - "useOneCommand": false, - "explanation": "Описание раздела", - "picture": "CommonPicture.МояКартинка", - "content": ["Catalog.Товары", "Document.Заказ"] -} -``` - -Минимально: только `name`. Остальное — дефолты. - -## Примеры - -```powershell -# Минимальная подсистема -... -Value '{"name":"Тест"}' -OutputDir config/ - -# С составом и картинкой -... -Value '{"name":"Продажи","content":["Catalog.Товары","Report.Продажи"],"picture":"CommonPicture.Продажи"}' -OutputDir config/ - -# Вложенная подсистема -... -Value '{"name":"Дочерняя"}' -OutputDir config/ -Parent config/Subsystems/Продажи.xml -``` - +--- +name: subsystem-compile +description: Создать подсистему 1С — XML-исходники из JSON-определения. Используй когда нужно добавить подсистему (раздел) в конфигурацию +argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputDir <ConfigDir> [-Parent <path>]" +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /subsystem-compile — генерация подсистемы из JSON + +Принимает JSON-определение подсистемы → генерирует XML + файловую структуру + регистрирует в родителе (Configuration.xml или родительская подсистема). + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `DefinitionFile` | Путь к JSON-файлу определения | +| `Value` | Инлайн JSON-строка (альтернатива DefinitionFile) | +| `OutputDir` | Корень выгрузки (где `Subsystems/`, `Configuration.xml`) | +| `Parent` | Путь к XML родительской подсистемы (для вложенных) | +| `NoValidate` | Пропустить авто-валидацию | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-compile.ps1" -Value '<json>' -OutputDir '<ConfigDir>' +``` + +## JSON-определение + +```json +{ + "name": "МояПодсистема", + "synonym": "Моя подсистема", + "comment": "", + "includeInCommandInterface": true, + "useOneCommand": false, + "explanation": "Описание раздела", + "picture": "CommonPicture.МояКартинка", + "content": ["Catalog.Товары", "Document.Заказ"] +} +``` + +Минимально: только `name`. Остальное — дефолты. + +## Примеры + +```powershell +# Минимальная подсистема +... -Value '{"name":"Тест"}' -OutputDir config/ + +# С составом и картинкой +... -Value '{"name":"Продажи","content":["Catalog.Товары","Report.Продажи"],"picture":"CommonPicture.Продажи"}' -OutputDir config/ + +# Вложенная подсистема +... -Value '{"name":"Дочерняя"}' -OutputDir config/ -Parent config/Subsystems/Продажи.xml +``` + diff --git a/.claude/skills/subsystem-edit/SKILL.md b/.claude/skills/subsystem-edit/SKILL.md index 72d45ae1..a2fb12ed 100644 --- a/.claude/skills/subsystem-edit/SKILL.md +++ b/.claude/skills/subsystem-edit/SKILL.md @@ -1,57 +1,57 @@ ---- -name: subsystem-edit -description: Точечное редактирование подсистемы 1С. Используй когда нужно добавить или удалить объекты из подсистемы, управлять дочерними подсистемами или изменить свойства -argument-hint: -SubsystemPath <path> -Operation <op> -Value <value> -allowed-tools: - - Bash - - Read - - Write - - Glob ---- - -# /subsystem-edit — редактирование подсистемы 1С - -Точечное редактирование XML подсистемы: состав, дочерние подсистемы, свойства. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `SubsystemPath` | Путь к XML-файлу подсистемы | -| `DefinitionFile` | JSON-файл с массивом операций | -| `Operation` | Одна операция (альтернатива DefinitionFile) | -| `Value` | Значение для операции | -| `NoValidate` | Пропустить авто-валидацию | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-edit.ps1" -SubsystemPath '<path>' -Operation add-content -Value 'Catalog.Товары' -``` - -## Операции - -| Операция | Значение | Описание | -|----------|----------|----------| -| `add-content` | `"Catalog.X"` или `["Catalog.X","Document.Y"]` | Добавить объекты в Content | -| `remove-content` | `"Catalog.X"` или `["Catalog.X"]` | Удалить объекты из Content | -| `add-child` | `"ИмяПодсистемы"` | Добавить дочернюю подсистему в ChildObjects | -| `remove-child` | `"ИмяПодсистемы"` | Удалить дочернюю подсистему | -| `set-property` | `{"name":"prop","value":"val"}` | Изменить свойство (Synonym, IncludeInCommandInterface, UseOneCommand, etc.) | - -## Примеры - -```powershell -# Добавить объект в состав -... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value "Document.Заказ" - -# Добавить несколько объектов -... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value '["Catalog.Товары","Report.Продажи"]' - -# Удалить объект из состава -... -SubsystemPath Subsystems/Продажи.xml -Operation remove-content -Value "Report.Старый" - -# Добавить дочернюю подсистему -... -SubsystemPath Subsystems/Продажи.xml -Operation add-child -Value "НоваяДочерняя" - -# Изменить свойство -... -SubsystemPath Subsystems/Продажи.xml -Operation set-property -Value '{"name":"IncludeInCommandInterface","value":"false"}' -``` +--- +name: subsystem-edit +description: Точечное редактирование подсистемы 1С. Используй когда нужно добавить или удалить объекты из подсистемы, управлять дочерними подсистемами или изменить свойства +argument-hint: -SubsystemPath <path> -Operation <op> -Value <value> +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /subsystem-edit — редактирование подсистемы 1С + +Точечное редактирование XML подсистемы: состав, дочерние подсистемы, свойства. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `SubsystemPath` | Путь к XML-файлу подсистемы | +| `DefinitionFile` | JSON-файл с массивом операций | +| `Operation` | Одна операция (альтернатива DefinitionFile) | +| `Value` | Значение для операции | +| `NoValidate` | Пропустить авто-валидацию | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-edit.ps1" -SubsystemPath '<path>' -Operation add-content -Value 'Catalog.Товары' +``` + +## Операции + +| Операция | Значение | Описание | +|----------|----------|----------| +| `add-content` | `"Catalog.X"` или `["Catalog.X","Document.Y"]` | Добавить объекты в Content | +| `remove-content` | `"Catalog.X"` или `["Catalog.X"]` | Удалить объекты из Content | +| `add-child` | `"ИмяПодсистемы"` | Добавить дочернюю подсистему в ChildObjects | +| `remove-child` | `"ИмяПодсистемы"` | Удалить дочернюю подсистему | +| `set-property` | `{"name":"prop","value":"val"}` | Изменить свойство (Synonym, IncludeInCommandInterface, UseOneCommand, etc.) | + +## Примеры + +```powershell +# Добавить объект в состав +... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value "Document.Заказ" + +# Добавить несколько объектов +... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value '["Catalog.Товары","Report.Продажи"]' + +# Удалить объект из состава +... -SubsystemPath Subsystems/Продажи.xml -Operation remove-content -Value "Report.Старый" + +# Добавить дочернюю подсистему +... -SubsystemPath Subsystems/Продажи.xml -Operation add-child -Value "НоваяДочерняя" + +# Изменить свойство +... -SubsystemPath Subsystems/Продажи.xml -Operation set-property -Value '{"name":"IncludeInCommandInterface","value":"false"}' +``` diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 index da68b341..7cf74907 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 @@ -1,666 +1,666 @@ -# subsystem-edit v1.5 — Edit existing 1C subsystem XML -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, - [string]$DefinitionFile, - [ValidateSet("add-content","remove-content","add-child","remove-child","set-property")] - [string]$Operation, - [string]$Value, - [switch]$NoValidate -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Content type normalization (plural→singular, Russian→English) --- -$script:contentTypeMap = @{ - "Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "Constants"="Constant" - "Reports"="Report"; "DataProcessors"="DataProcessor" - "InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister" - "AccountingRegisters"="AccountingRegister"; "CalculationRegisters"="CalculationRegister" - "ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes" - "ChartsOfCalculationTypes"="ChartOfCalculationTypes" - "BusinessProcesses"="BusinessProcess"; "Tasks"="Task" - "ExchangePlans"="ExchangePlan"; "DocumentJournals"="DocumentJournal" - "CommonModules"="CommonModule"; "CommonCommands"="CommonCommand" - "CommonForms"="CommonForm"; "CommonPictures"="CommonPicture" - "CommonTemplates"="CommonTemplate"; "CommonAttributes"="CommonAttribute" - "CommandGroups"="CommandGroup"; "Roles"="Role" - "SessionParameters"="SessionParameter"; "FilterCriteria"="FilterCriterion" - "XDTOPackages"="XDTOPackage"; "WebServices"="WebService" - "HTTPServices"="HTTPService"; "WSReferences"="WSReference" - "EventSubscriptions"="EventSubscription"; "ScheduledJobs"="ScheduledJob" - "SettingsStorages"="SettingsStorage"; "FunctionalOptions"="FunctionalOption" - "FunctionalOptionsParameters"="FunctionalOptionsParameter" - "DefinedTypes"="DefinedType"; "DocumentNumerators"="DocumentNumerator" - "Sequences"="Sequence"; "Subsystems"="Subsystem" - "StyleItems"="StyleItem"; "IntegrationServices"="IntegrationService" - # Russian singular - "Справочник"="Catalog"; "Каталог"="Catalog"; "Документ"="Document" - "Перечисление"="Enum"; "Константа"="Constant" - "Отчёт"="Report"; "Отчет"="Report"; "Обработка"="DataProcessor" - "РегистрСведений"="InformationRegister"; "РегистрНакопления"="AccumulationRegister" - "РегистрБухгалтерии"="AccountingRegister" - "РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister" - "ПланСчетов"="ChartOfAccounts"; "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" - "ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes" - "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" - "ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal" - "ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand" - "ОбщаяФорма"="CommonForm"; "ОбщаяКартинка"="CommonPicture" - "ОбщийМакет"="CommonTemplate"; "ОбщийРеквизит"="CommonAttribute" - "ГруппаКоманд"="CommandGroup"; "Роль"="Role" - "ПараметрСеанса"="SessionParameter"; "КритерийОтбора"="FilterCriterion" - "ПакетXDTO"="XDTOPackage"; "ВебСервис"="WebService" - "HTTPСервис"="HTTPService"; "WSСсылка"="WSReference" - "ПодпискаНаСобытие"="EventSubscription"; "РегламентноеЗадание"="ScheduledJob" - "ХранилищеНастроек"="SettingsStorage"; "ФункциональнаяОпция"="FunctionalOption" - "ПараметрФункциональныхОпций"="FunctionalOptionsParameter" - "ОпределяемыйТип"="DefinedType"; "Подсистема"="Subsystem" - "ЭлементСтиля"="StyleItem"; "СервисИнтеграции"="IntegrationService" - # Russian plural - "Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum" - "Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report" - "Обработки"="DataProcessor"; "РегистрыСведений"="InformationRegister" - "РегистрыНакопления"="AccumulationRegister"; "РегистрыБухгалтерии"="AccountingRegister" - "РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister" - "ПланыСчетов"="ChartOfAccounts"; "ПланыВидовХарактеристик"="ChartOfCharacteristicTypes" - "ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes" - "БизнесПроцессы"="BusinessProcess"; "Задачи"="Task" - "ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal" - "ОбщиеМодули"="CommonModule"; "ОбщиеКоманды"="CommonCommand" - "ОбщиеФормы"="CommonForm"; "ОбщиеКартинки"="CommonPicture" - "ОбщиеМакеты"="CommonTemplate"; "ОбщиеРеквизиты"="CommonAttribute" - "ГруппыКоманд"="CommandGroup"; "Роли"="Role" - "ПараметрыСеанса"="SessionParameter"; "КритерииОтбора"="FilterCriterion" - "ПакетыXDTO"="XDTOPackage"; "ВебСервисы"="WebService" - "HTTPСервисы"="HTTPService"; "WSСсылки"="WSReference" - "ПодпискиНаСобытия"="EventSubscription"; "РегламентныеЗадания"="ScheduledJob" - "ХранилищаНастроек"="SettingsStorage"; "ФункциональныеОпции"="FunctionalOption" - "ОпределяемыеТипы"="DefinedType"; "Подсистемы"="Subsystem" - "ЭлементыСтиля"="StyleItem"; "СервисыИнтеграции"="IntegrationService" -} - -function Normalize-ContentRef([string]$ref) { - if (-not $ref -or -not $ref.Contains('.')) { return $ref } - $dotIdx = $ref.IndexOf('.') - $typePart = $ref.Substring(0, $dotIdx) - $namePart = $ref.Substring($dotIdx + 1) - if ($script:contentTypeMap.ContainsKey($typePart)) { - $typePart = $script:contentTypeMap[$typePart] - } - return "$typePart.$namePart" -} - -# --- Mode validation --- -if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } -if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { - $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath -} -if (Test-Path $SubsystemPath -PathType Container) { - $dirName = Split-Path $SubsystemPath -Leaf - $candidate = Join-Path $SubsystemPath "$dirName.xml" - $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" - if (Test-Path $candidate) { $SubsystemPath = $candidate } - elseif (Test-Path $sibling) { $SubsystemPath = $sibling } - else { Write-Error "No $dirName.xml found in directory or as sibling"; exit 1 } -} -# File not found — check Dir/Name/Name.xml → Dir/Name.xml -if (-not (Test-Path $SubsystemPath)) { - $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) - $pd = Split-Path $SubsystemPath - if ($fn -eq (Split-Path $pd -Leaf)) { - $c = Join-Path (Split-Path $pd) "$fn.xml" - if (Test-Path $c) { $SubsystemPath = $c } - } -} -if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPath"; exit 1 } -$resolvedPath = (Resolve-Path $SubsystemPath).Path -$script:resolvedPath = $resolvedPath - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -Assert-EditAllowed $resolvedPath 'editable' - -# --- Load XML with PreserveWhitespace --- -$script:xmlDoc = New-Object System.Xml.XmlDocument -$script:xmlDoc.PreserveWhitespace = $true -$script:xmlDoc.Load($resolvedPath) - -$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version") -if (-not $script:formatVersion) { $script:formatVersion = "2.17" } -$script:utf8Bom = New-Object System.Text.UTF8Encoding($true) - -$script:addCount = 0 -$script:removeCount = 0 -$script:modifyCount = 0 - -function Info([string]$msg) { Write-Host "[INFO] $msg" } -function Warn([string]$msg) { Write-Host "[WARN] $msg" } - -# --- Detect structure --- -$root = $script:xmlDoc.DocumentElement -$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" -$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" -$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" -$script:v8Ns = "http://v8.1c.ru/8.1/data/core" - -$script:sub = $null -foreach ($child in $root.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") { - $script:sub = $child; break - } -} -if (-not $script:sub) { Write-Error "No <Subsystem> element found"; exit 1 } - -$script:propsEl = $null -$script:childObjsEl = $null -foreach ($child in $script:sub.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -eq "Properties") { $script:propsEl = $child } - if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } -} - -$script:objName = "" -foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { - $script:objName = $child.InnerText.Trim(); break - } -} -Info "Subsystem: $($script:objName)" - -# --- XML manipulation helpers (from meta-edit pattern) --- -function Esc-Xml([string]$s) { - return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') -} - -function New-Guid-String { - return [System.Guid]::NewGuid().ToString() -} - -function Write-ChildSubsystemStub([string]$childPath, [string]$childName, [string]$formatVersion, [System.Text.Encoding]$utf8Bom) { - $childUuid = New-Guid-String - $sb = New-Object System.Text.StringBuilder 2048 - [void]$sb.AppendLine('<?xml version="1.0" encoding="UTF-8"?>') - [void]$sb.AppendLine("<MetaDataObject xmlns=`"http://v8.1c.ru/8.3/MDClasses`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:cmi=`"http://v8.1c.ru/8.2/managed-application/cmi`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xen=`"http://v8.1c.ru/8.3/xcf/enums`" xmlns:xpr=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$formatVersion`">") - [void]$sb.AppendLine("`t<Subsystem uuid=`"$childUuid`">") - [void]$sb.AppendLine("`t`t<Properties>") - [void]$sb.AppendLine("`t`t`t<Name>$(Esc-Xml $childName)</Name>") - [void]$sb.AppendLine("`t`t`t<Synonym/>") - [void]$sb.AppendLine("`t`t`t<Comment/>") - [void]$sb.AppendLine("`t`t`t<IncludeHelpInContents>true</IncludeHelpInContents>") - [void]$sb.AppendLine("`t`t`t<IncludeInCommandInterface>true</IncludeInCommandInterface>") - [void]$sb.AppendLine("`t`t`t<UseOneCommand>false</UseOneCommand>") - [void]$sb.AppendLine("`t`t`t<Explanation/>") - [void]$sb.AppendLine("`t`t`t<Picture/>") - [void]$sb.AppendLine("`t`t`t<Content/>") - [void]$sb.AppendLine("`t`t</Properties>") - [void]$sb.AppendLine("`t`t<ChildObjects/>") - [void]$sb.AppendLine("`t</Subsystem>") - [void]$sb.AppendLine('</MetaDataObject>') - [System.IO.File]::WriteAllText($childPath, $sb.ToString(), $utf8Bom) -} - -function Import-Fragment([string]$xmlString) { - $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString</_W>" - $frag = New-Object System.Xml.XmlDocument - $frag.PreserveWhitespace = $true - $frag.LoadXml($wrapper) - $nodes = @() - foreach ($child in $frag.DocumentElement.ChildNodes) { - if ($child.NodeType -eq 'Element') { - $nodes += $script:xmlDoc.ImportNode($child, $true) - } - } - return ,$nodes -} - -function Get-ChildIndent($container) { - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { - if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } - if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } - } - } - $depth = 0; $current = $container - while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } - return "`t" * ($depth + 1) -} - -function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") - if ($refNode) { - $container.InsertBefore($ws, $refNode) | Out-Null - $container.InsertBefore($newNode, $ws) | Out-Null - } else { - $trailing = $container.LastChild - if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { - $container.InsertBefore($ws, $trailing) | Out-Null - $container.InsertBefore($newNode, $trailing) | Out-Null - } else { - $container.AppendChild($ws) | Out-Null - $container.AppendChild($newNode) | Out-Null - $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } - } -} - -function Remove-NodeWithWhitespace($node) { - $parent = $node.ParentNode - $prev = $node.PreviousSibling - $next = $node.NextSibling - if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($prev) | Out-Null - } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { - $parent.RemoveChild($next) | Out-Null - } - $parent.RemoveChild($node) | Out-Null -} - -function Expand-SelfClosingElement($container, $parentIndent) { - # If the element is self-closing (empty), add whitespace for children - if (-not $container.HasChildNodes -or $container.IsEmpty) { - $childIndent = "$parentIndent`t" - # The element is self-closing; we need to add something to make it non-empty - # Adding a whitespace node will force opening+closing tags - $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") - $container.AppendChild($closeWs) | Out-Null - } -} - -# --- Parse value: string or JSON array --- -function Parse-ValueList([string]$val) { - $val = $val.Trim() - if ($val.StartsWith("[")) { - $arr = $val | ConvertFrom-Json - $result = @(); foreach ($item in $arr) { $result += "$item" } - return ,$result - } - return @($val) -} - -# --- Operations --- -function Do-AddContent([string[]]$items) { - $contentEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") { - $contentEl = $child; break - } - } - if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 } - - # Get existing items for dedup - $existing = @() - foreach ($child in $contentEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item") { - $existing += $child.InnerText.Trim() - } - } - - # Determine indentation - $propsIndent = Get-ChildIndent $script:propsEl - $contentIndent = "$propsIndent`t" - - # Expand self-closing if needed - if (-not $contentEl.HasChildNodes -or $contentEl.IsEmpty) { - Expand-SelfClosingElement $contentEl $propsIndent - $contentIndent = "$propsIndent`t" - } else { - $contentIndent = Get-ChildIndent $contentEl - } - - foreach ($rawItem in $items) { - $item = Normalize-ContentRef $rawItem - if ($item -ne $rawItem) { Write-Host "[NORM] Content: $rawItem -> $item" } - if ($item -in $existing) { - Warn "Content already contains: $item" - continue - } - $fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$item</xr:Item>" - $nodes = Import-Fragment $fragXml - if ($nodes.Count -gt 0) { - Insert-BeforeElement $contentEl $nodes[0] $null $contentIndent - $script:addCount++ - Info "Added content: $item" - } - } -} - -function Do-RemoveContent([string[]]$items) { - $contentEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") { - $contentEl = $child; break - } - } - if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 } - - foreach ($item in $items) { - $found = $false - foreach ($child in @($contentEl.ChildNodes)) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item" -and $child.InnerText.Trim() -eq $item) { - Remove-NodeWithWhitespace $child - $script:removeCount++ - Info "Removed content: $item" - $found = $true - break - } - } - if (-not $found) { Warn "Content item not found: $item" } - } -} - -function Do-AddChild([string]$childName) { - if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 } - - # Dedup check - foreach ($child in $script:childObjsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) { - Warn "ChildObjects already contains: $childName" - return - } - } - - $subIndent = Get-ChildIndent $script:sub - if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { - Expand-SelfClosingElement $script:childObjsEl $subIndent - } - $childIndent = Get-ChildIndent $script:childObjsEl - - $newEl = $script:xmlDoc.CreateElement("Subsystem", $script:mdNs) - $newEl.InnerText = $childName - Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent - $script:addCount++ - Info "Added child subsystem: $childName" - - # Write stub XML for the new child if it doesn't exist yet - $parentDir = [System.IO.Path]::GetDirectoryName($script:resolvedPath) - $parentBaseName = [System.IO.Path]::GetFileNameWithoutExtension($script:resolvedPath) - $childSubsDir = Join-Path (Join-Path $parentDir $parentBaseName) "Subsystems" - if (-not (Test-Path $childSubsDir)) { - New-Item -ItemType Directory -Path $childSubsDir -Force | Out-Null - Info "Created directory: $childSubsDir" - } - $childXml = Join-Path $childSubsDir "$childName.xml" - if (-not (Test-Path $childXml)) { - Write-ChildSubsystemStub $childXml $childName $script:formatVersion $script:utf8Bom - Info "Created stub: $childXml" - } -} - -function Do-RemoveChild([string]$childName) { - if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 } - - $found = $false - foreach ($child in @($script:childObjsEl.ChildNodes)) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) { - Remove-NodeWithWhitespace $child - $script:removeCount++ - Info "Removed child subsystem: $childName" - $found = $true - break - } - } - if (-not $found) { Warn "Child subsystem not found: $childName" } -} - -function Do-SetProperty([string]$jsonVal) { - $propDef = $jsonVal | ConvertFrom-Json - $propName = "$($propDef.name)" - $propValue = "$($propDef.value)" - - $propEl = $null - foreach ($child in $script:propsEl.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { - $propEl = $child; break - } - } - if (-not $propEl) { - Write-Error "Property '$propName' not found in Properties" - exit 1 - } - - $boolProps = @("IncludeInCommandInterface","UseOneCommand","IncludeHelpInContents") - if ($propName -in $boolProps) { - $propEl.InnerText = $propValue.ToLower() - $script:modifyCount++ - Info "Set $propName = $propValue" - return - } - - $mlProps = @("Synonym","Explanation") - if ($propName -in $mlProps) { - if (-not $propValue) { - # Clear - make self-closing - $propEl.InnerXml = "" - $script:modifyCount++ - Info "Cleared $propName" - } else { - $indent = Get-ChildIndent $script:propsEl - $mlXml = "`r`n$indent`t<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$([System.Security.SecurityElement]::Escape($propValue))</v8:content>`r`n$indent`t</v8:item>`r`n$indent" - $propEl.InnerXml = $mlXml - $script:modifyCount++ - Info "Set $propName = `"$propValue`"" - } - return - } - - if ($propName -eq "Comment") { - if (-not $propValue) { $propEl.InnerXml = "" } - else { $propEl.InnerText = $propValue } - $script:modifyCount++ - Info "Set Comment = `"$propValue`"" - return - } - - if ($propName -eq "Picture") { - if (-not $propValue) { - $propEl.InnerXml = "" - } else { - $indent = Get-ChildIndent $script:propsEl - $picXml = "`r`n$indent`t<xr:Ref>$propValue</xr:Ref>`r`n$indent`t<xr:LoadTransparent>false</xr:LoadTransparent>`r`n$indent" - $propEl.InnerXml = $picXml - } - $script:modifyCount++ - Info "Set Picture = `"$propValue`"" - return - } - - # Generic text property - $propEl.InnerText = $propValue - $script:modifyCount++ - Info "Set $propName = `"$propValue`"" -} - -# --- Execute operations --- -$operations = @() -if ($DefinitionFile) { - if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { - $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile - } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile - $ops = $jsonText | ConvertFrom-Json - if ($ops -is [System.Array]) { - foreach ($op in $ops) { $operations += $op } - } else { - $operations += $ops - } -} else { - $operations += @{ operation = $Operation; value = $Value } -} - -foreach ($op in $operations) { - $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } - $opValue = if ($op.value) { "$($op.value)" } else { "$Value" } - - switch ($opName) { - "add-content" { Do-AddContent (Parse-ValueList $opValue) } - "remove-content" { Do-RemoveContent (Parse-ValueList $opValue) } - "add-child" { Do-AddChild $opValue } - "remove-child" { Do-RemoveChild $opValue } - "set-property" { Do-SetProperty $opValue } - default { Write-Error "Unknown operation: $opName"; exit 1 } - } -} - -# --- Save --- -$settings = New-Object System.Xml.XmlWriterSettings -$settings.Encoding = New-Object System.Text.UTF8Encoding($true) -$settings.Indent = $false -$settings.NewLineHandling = [System.Xml.NewLineHandling]::None - -$memStream = New-Object System.IO.MemoryStream -$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) -$script:xmlDoc.Save($writer) -$writer.Flush(); $writer.Close() - -$bytes = $memStream.ToArray() -$memStream.Close() -$text = [System.Text.Encoding]::UTF8.GetString($bytes) -if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } -$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') - -$utf8Bom = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) -Info "Saved: $resolvedPath" - -# --- Auto-validate --- -if (-not $NoValidate) { - $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\subsystem-validate") "scripts\subsystem-validate.ps1" - $validateScript = [System.IO.Path]::GetFullPath($validateScript) - if (Test-Path $validateScript) { - Write-Host "" - Write-Host "--- Running subsystem-validate ---" - & powershell.exe -NoProfile -File $validateScript -SubsystemPath $resolvedPath - } -} - -# --- Summary --- -Write-Host "" -Write-Host "=== subsystem-edit summary ===" -Write-Host " Subsystem: $($script:objName)" -Write-Host " Added: $($script:addCount)" -Write-Host " Removed: $($script:removeCount)" -Write-Host " Modified: $($script:modifyCount)" -exit 0 +# subsystem-edit v1.5 — Edit existing 1C subsystem XML +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, + [string]$DefinitionFile, + [ValidateSet("add-content","remove-content","add-child","remove-child","set-property")] + [string]$Operation, + [string]$Value, + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Content type normalization (plural→singular, Russian→English) --- +$script:contentTypeMap = @{ + "Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "Constants"="Constant" + "Reports"="Report"; "DataProcessors"="DataProcessor" + "InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister" + "AccountingRegisters"="AccountingRegister"; "CalculationRegisters"="CalculationRegister" + "ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes" + "ChartsOfCalculationTypes"="ChartOfCalculationTypes" + "BusinessProcesses"="BusinessProcess"; "Tasks"="Task" + "ExchangePlans"="ExchangePlan"; "DocumentJournals"="DocumentJournal" + "CommonModules"="CommonModule"; "CommonCommands"="CommonCommand" + "CommonForms"="CommonForm"; "CommonPictures"="CommonPicture" + "CommonTemplates"="CommonTemplate"; "CommonAttributes"="CommonAttribute" + "CommandGroups"="CommandGroup"; "Roles"="Role" + "SessionParameters"="SessionParameter"; "FilterCriteria"="FilterCriterion" + "XDTOPackages"="XDTOPackage"; "WebServices"="WebService" + "HTTPServices"="HTTPService"; "WSReferences"="WSReference" + "EventSubscriptions"="EventSubscription"; "ScheduledJobs"="ScheduledJob" + "SettingsStorages"="SettingsStorage"; "FunctionalOptions"="FunctionalOption" + "FunctionalOptionsParameters"="FunctionalOptionsParameter" + "DefinedTypes"="DefinedType"; "DocumentNumerators"="DocumentNumerator" + "Sequences"="Sequence"; "Subsystems"="Subsystem" + "StyleItems"="StyleItem"; "IntegrationServices"="IntegrationService" + # Russian singular + "Справочник"="Catalog"; "Каталог"="Catalog"; "Документ"="Document" + "Перечисление"="Enum"; "Константа"="Constant" + "Отчёт"="Report"; "Отчет"="Report"; "Обработка"="DataProcessor" + "РегистрСведений"="InformationRegister"; "РегистрНакопления"="AccumulationRegister" + "РегистрБухгалтерии"="AccountingRegister" + "РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister" + "ПланСчетов"="ChartOfAccounts"; "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" + "ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes" + "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" + "ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal" + "ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand" + "ОбщаяФорма"="CommonForm"; "ОбщаяКартинка"="CommonPicture" + "ОбщийМакет"="CommonTemplate"; "ОбщийРеквизит"="CommonAttribute" + "ГруппаКоманд"="CommandGroup"; "Роль"="Role" + "ПараметрСеанса"="SessionParameter"; "КритерийОтбора"="FilterCriterion" + "ПакетXDTO"="XDTOPackage"; "ВебСервис"="WebService" + "HTTPСервис"="HTTPService"; "WSСсылка"="WSReference" + "ПодпискаНаСобытие"="EventSubscription"; "РегламентноеЗадание"="ScheduledJob" + "ХранилищеНастроек"="SettingsStorage"; "ФункциональнаяОпция"="FunctionalOption" + "ПараметрФункциональныхОпций"="FunctionalOptionsParameter" + "ОпределяемыйТип"="DefinedType"; "Подсистема"="Subsystem" + "ЭлементСтиля"="StyleItem"; "СервисИнтеграции"="IntegrationService" + # Russian plural + "Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum" + "Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report" + "Обработки"="DataProcessor"; "РегистрыСведений"="InformationRegister" + "РегистрыНакопления"="AccumulationRegister"; "РегистрыБухгалтерии"="AccountingRegister" + "РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister" + "ПланыСчетов"="ChartOfAccounts"; "ПланыВидовХарактеристик"="ChartOfCharacteristicTypes" + "ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes" + "БизнесПроцессы"="BusinessProcess"; "Задачи"="Task" + "ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal" + "ОбщиеМодули"="CommonModule"; "ОбщиеКоманды"="CommonCommand" + "ОбщиеФормы"="CommonForm"; "ОбщиеКартинки"="CommonPicture" + "ОбщиеМакеты"="CommonTemplate"; "ОбщиеРеквизиты"="CommonAttribute" + "ГруппыКоманд"="CommandGroup"; "Роли"="Role" + "ПараметрыСеанса"="SessionParameter"; "КритерииОтбора"="FilterCriterion" + "ПакетыXDTO"="XDTOPackage"; "ВебСервисы"="WebService" + "HTTPСервисы"="HTTPService"; "WSСсылки"="WSReference" + "ПодпискиНаСобытия"="EventSubscription"; "РегламентныеЗадания"="ScheduledJob" + "ХранилищаНастроек"="SettingsStorage"; "ФункциональныеОпции"="FunctionalOption" + "ОпределяемыеТипы"="DefinedType"; "Подсистемы"="Subsystem" + "ЭлементыСтиля"="StyleItem"; "СервисыИнтеграции"="IntegrationService" +} + +function Normalize-ContentRef([string]$ref) { + if (-not $ref -or -not $ref.Contains('.')) { return $ref } + $dotIdx = $ref.IndexOf('.') + $typePart = $ref.Substring(0, $dotIdx) + $namePart = $ref.Substring($dotIdx + 1) + if ($script:contentTypeMap.ContainsKey($typePart)) { + $typePart = $script:contentTypeMap[$typePart] + } + return "$typePart.$namePart" +} + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } +if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { + $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath +} +if (Test-Path $SubsystemPath -PathType Container) { + $dirName = Split-Path $SubsystemPath -Leaf + $candidate = Join-Path $SubsystemPath "$dirName.xml" + $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" + if (Test-Path $candidate) { $SubsystemPath = $candidate } + elseif (Test-Path $sibling) { $SubsystemPath = $sibling } + else { Write-Error "No $dirName.xml found in directory or as sibling"; exit 1 } +} +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $SubsystemPath)) { + $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) + $pd = Split-Path $SubsystemPath + if ($fn -eq (Split-Path $pd -Leaf)) { + $c = Join-Path (Split-Path $pd) "$fn.xml" + if (Test-Path $c) { $SubsystemPath = $c } + } +} +if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPath"; exit 1 } +$resolvedPath = (Resolve-Path $SubsystemPath).Path +$script:resolvedPath = $resolvedPath + +# --- Support guard (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / +# read-only configs unless allowed. Trigger = bin present; reaction from +# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never +# throws — guard errors degrade to allow. +function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null +} +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Get-EditMode([string]$cfgDir) { + try { + $pj = Find-V8Project (Get-Location).Path + if (-not $pj) { $pj = Find-V8Project $cfgDir } + if (-not $pj) { return 'deny' } + $proj = Get-Content -Raw $pj | ConvertFrom-Json + $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') + if ($proj.databases) { + foreach ($db in $proj.databases) { + if ($db.configSrc) { + $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') + if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { + if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } + } + } + } + } + if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } + return 'deny' + } catch { return 'deny' } +} +function Assert-EditAllowed([string]$targetPath, [string]$require) { + try { + $rp = $targetPath + try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} + $elemUuid = Get-RootUuid $rp + $cfgDir = $null; $binPath = $null + $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $cfgDir) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } + } + if ($elemUuid -and $cfgDir) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + # New object (no element file): fall back to config root uuid. + if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } + if (-not $binPath -or -not (Test-Path $binPath)) { return } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $hm.Success) { return } + $G = [int]$hm.Groups[1].Value + $K = [int]$hm.Groups[2].Value + if ($K -eq 0) { return } + $best = $null + if ($elemUuid) { + $u = [regex]::Escape($elemUuid.ToLower()) + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + } + $blocked = $false; $code = ""; $reason = "" + if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } + elseif ($require -eq 'removed') { + if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } + } + else { + if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } + } + if (-not $blocked) { return } + $mode = Get-EditMode $cfgDir + if ($mode -eq 'off') { return } + # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the + # latter throws and would be swallowed by this function's own catch. + if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } + $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." + $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." + $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." + if ($code -eq "capability-off") { + $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." + $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." + } elseif ($code -eq "not-removed") { + $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." + $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." + } else { + $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." + $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." + } + [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") + exit 1 + } catch { return } +} + +Assert-EditAllowed $resolvedPath 'editable' + +# --- Load XML with PreserveWhitespace --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version") +if (-not $script:formatVersion) { $script:formatVersion = "2.17" } +$script:utf8Bom = New-Object System.Text.UTF8Encoding($true) + +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- Detect structure --- +$root = $script:xmlDoc.DocumentElement +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$script:sub = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") { + $script:sub = $child; break + } +} +if (-not $script:sub) { Write-Error "No <Subsystem> element found"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:sub.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +$script:objName = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { + $script:objName = $child.InnerText.Trim(); break + } +} +Info "Subsystem: $($script:objName)" + +# --- XML manipulation helpers (from meta-edit pattern) --- +function Esc-Xml([string]$s) { + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function New-Guid-String { + return [System.Guid]::NewGuid().ToString() +} + +function Write-ChildSubsystemStub([string]$childPath, [string]$childName, [string]$formatVersion, [System.Text.Encoding]$utf8Bom) { + $childUuid = New-Guid-String + $sb = New-Object System.Text.StringBuilder 2048 + [void]$sb.AppendLine('<?xml version="1.0" encoding="UTF-8"?>') + [void]$sb.AppendLine("<MetaDataObject xmlns=`"http://v8.1c.ru/8.3/MDClasses`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:cmi=`"http://v8.1c.ru/8.2/managed-application/cmi`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xen=`"http://v8.1c.ru/8.3/xcf/enums`" xmlns:xpr=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$formatVersion`">") + [void]$sb.AppendLine("`t<Subsystem uuid=`"$childUuid`">") + [void]$sb.AppendLine("`t`t<Properties>") + [void]$sb.AppendLine("`t`t`t<Name>$(Esc-Xml $childName)</Name>") + [void]$sb.AppendLine("`t`t`t<Synonym/>") + [void]$sb.AppendLine("`t`t`t<Comment/>") + [void]$sb.AppendLine("`t`t`t<IncludeHelpInContents>true</IncludeHelpInContents>") + [void]$sb.AppendLine("`t`t`t<IncludeInCommandInterface>true</IncludeInCommandInterface>") + [void]$sb.AppendLine("`t`t`t<UseOneCommand>false</UseOneCommand>") + [void]$sb.AppendLine("`t`t`t<Explanation/>") + [void]$sb.AppendLine("`t`t`t<Picture/>") + [void]$sb.AppendLine("`t`t`t<Content/>") + [void]$sb.AppendLine("`t`t</Properties>") + [void]$sb.AppendLine("`t`t<ChildObjects/>") + [void]$sb.AppendLine("`t</Subsystem>") + [void]$sb.AppendLine('</MetaDataObject>') + [System.IO.File]::WriteAllText($childPath, $sb.ToString(), $utf8Bom) +} + +function Import-Fragment([string]$xmlString) { + $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString</_W>" + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Expand-SelfClosingElement($container, $parentIndent) { + # If the element is self-closing (empty), add whitespace for children + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $childIndent = "$parentIndent`t" + # The element is self-closing; we need to add something to make it non-empty + # Adding a whitespace node will force opening+closing tags + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# --- Parse value: string or JSON array --- +function Parse-ValueList([string]$val) { + $val = $val.Trim() + if ($val.StartsWith("[")) { + $arr = $val | ConvertFrom-Json + $result = @(); foreach ($item in $arr) { $result += "$item" } + return ,$result + } + return @($val) +} + +# --- Operations --- +function Do-AddContent([string[]]$items) { + $contentEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") { + $contentEl = $child; break + } + } + if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 } + + # Get existing items for dedup + $existing = @() + foreach ($child in $contentEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item") { + $existing += $child.InnerText.Trim() + } + } + + # Determine indentation + $propsIndent = Get-ChildIndent $script:propsEl + $contentIndent = "$propsIndent`t" + + # Expand self-closing if needed + if (-not $contentEl.HasChildNodes -or $contentEl.IsEmpty) { + Expand-SelfClosingElement $contentEl $propsIndent + $contentIndent = "$propsIndent`t" + } else { + $contentIndent = Get-ChildIndent $contentEl + } + + foreach ($rawItem in $items) { + $item = Normalize-ContentRef $rawItem + if ($item -ne $rawItem) { Write-Host "[NORM] Content: $rawItem -> $item" } + if ($item -in $existing) { + Warn "Content already contains: $item" + continue + } + $fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$item</xr:Item>" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $contentEl $nodes[0] $null $contentIndent + $script:addCount++ + Info "Added content: $item" + } + } +} + +function Do-RemoveContent([string[]]$items) { + $contentEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") { + $contentEl = $child; break + } + } + if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 } + + foreach ($item in $items) { + $found = $false + foreach ($child in @($contentEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item" -and $child.InnerText.Trim() -eq $item) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed content: $item" + $found = $true + break + } + } + if (-not $found) { Warn "Content item not found: $item" } + } +} + +function Do-AddChild([string]$childName) { + if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 } + + # Dedup check + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) { + Warn "ChildObjects already contains: $childName" + return + } + } + + $subIndent = Get-ChildIndent $script:sub + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $subIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + $newEl = $script:xmlDoc.CreateElement("Subsystem", $script:mdNs) + $newEl.InnerText = $childName + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + $script:addCount++ + Info "Added child subsystem: $childName" + + # Write stub XML for the new child if it doesn't exist yet + $parentDir = [System.IO.Path]::GetDirectoryName($script:resolvedPath) + $parentBaseName = [System.IO.Path]::GetFileNameWithoutExtension($script:resolvedPath) + $childSubsDir = Join-Path (Join-Path $parentDir $parentBaseName) "Subsystems" + if (-not (Test-Path $childSubsDir)) { + New-Item -ItemType Directory -Path $childSubsDir -Force | Out-Null + Info "Created directory: $childSubsDir" + } + $childXml = Join-Path $childSubsDir "$childName.xml" + if (-not (Test-Path $childXml)) { + Write-ChildSubsystemStub $childXml $childName $script:formatVersion $script:utf8Bom + Info "Created stub: $childXml" + } +} + +function Do-RemoveChild([string]$childName) { + if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 } + + $found = $false + foreach ($child in @($script:childObjsEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed child subsystem: $childName" + $found = $true + break + } + } + if (-not $found) { Warn "Child subsystem not found: $childName" } +} + +function Do-SetProperty([string]$jsonVal) { + $propDef = $jsonVal | ConvertFrom-Json + $propName = "$($propDef.name)" + $propValue = "$($propDef.value)" + + $propEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + $propEl = $child; break + } + } + if (-not $propEl) { + Write-Error "Property '$propName' not found in Properties" + exit 1 + } + + $boolProps = @("IncludeInCommandInterface","UseOneCommand","IncludeHelpInContents") + if ($propName -in $boolProps) { + $propEl.InnerText = $propValue.ToLower() + $script:modifyCount++ + Info "Set $propName = $propValue" + return + } + + $mlProps = @("Synonym","Explanation") + if ($propName -in $mlProps) { + if (-not $propValue) { + # Clear - make self-closing + $propEl.InnerXml = "" + $script:modifyCount++ + Info "Cleared $propName" + } else { + $indent = Get-ChildIndent $script:propsEl + $mlXml = "`r`n$indent`t<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$([System.Security.SecurityElement]::Escape($propValue))</v8:content>`r`n$indent`t</v8:item>`r`n$indent" + $propEl.InnerXml = $mlXml + $script:modifyCount++ + Info "Set $propName = `"$propValue`"" + } + return + } + + if ($propName -eq "Comment") { + if (-not $propValue) { $propEl.InnerXml = "" } + else { $propEl.InnerText = $propValue } + $script:modifyCount++ + Info "Set Comment = `"$propValue`"" + return + } + + if ($propName -eq "Picture") { + if (-not $propValue) { + $propEl.InnerXml = "" + } else { + $indent = Get-ChildIndent $script:propsEl + $picXml = "`r`n$indent`t<xr:Ref>$propValue</xr:Ref>`r`n$indent`t<xr:LoadTransparent>false</xr:LoadTransparent>`r`n$indent" + $propEl.InnerXml = $picXml + } + $script:modifyCount++ + Info "Set Picture = `"$propValue`"" + return + } + + # Generic text property + $propEl.InnerText = $propValue + $script:modifyCount++ + Info "Set $propName = `"$propValue`"" +} + +# --- Execute operations --- +$operations = @() +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $ops = $jsonText | ConvertFrom-Json + if ($ops -is [System.Array]) { + foreach ($op in $ops) { $operations += $op } + } else { + $operations += $ops + } +} else { + $operations += @{ operation = $Operation; value = $Value } +} + +foreach ($op in $operations) { + $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } + $opValue = if ($op.value) { "$($op.value)" } else { "$Value" } + + switch ($opName) { + "add-content" { Do-AddContent (Parse-ValueList $opValue) } + "remove-content" { Do-RemoveContent (Parse-ValueList $opValue) } + "add-child" { Do-AddChild $opValue } + "remove-child" { Do-RemoveChild $opValue } + "set-property" { Do-SetProperty $opValue } + default { Write-Error "Unknown operation: $opName"; exit 1 } + } +} + +# --- Save --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) +Info "Saved: $resolvedPath" + +# --- Auto-validate --- +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\subsystem-validate") "scripts\subsystem-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running subsystem-validate ---" + & powershell.exe -NoProfile -File $validateScript -SubsystemPath $resolvedPath + } +} + +# --- Summary --- +Write-Host "" +Write-Host "=== subsystem-edit summary ===" +Write-Host " Subsystem: $($script:objName)" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +exit 0 diff --git a/.claude/skills/subsystem-info/SKILL.md b/.claude/skills/subsystem-info/SKILL.md index 8b4198e1..030abdeb 100644 --- a/.claude/skills/subsystem-info/SKILL.md +++ b/.claude/skills/subsystem-info/SKILL.md @@ -1,62 +1,62 @@ ---- -name: subsystem-info -description: Анализ структуры подсистемы 1С из XML-выгрузки — состав, дочерние подсистемы, командный интерфейс, дерево иерархии. Используй для изучения структуры подсистем и навигации по конфигурации -argument-hint: <SubsystemPath> [-Mode overview|content|ci|tree|full] [-Name <элемент>] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /subsystem-info — Структура подсистемы 1С - -Читает XML подсистемы из выгрузки конфигурации 1С и выводит компактное описание структуры. - -## Параметры и команда - -| Параметр | Описание | -|----------|----------| -| `SubsystemPath` | Путь к XML-файлу подсистемы, каталогу подсистемы или каталогу `Subsystems/` (для tree) | -| `Mode` | Режим: `overview` (default), `content`, `ci`, `tree`, `full` | -| `Name` | Drill-down: тип объекта в content, секция в ci, имя подсистемы в tree | -| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | -| `OutFile` | Записать результат в файл (UTF-8 BOM) | - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-info.ps1" -SubsystemPath "<путь>" -``` - -## Пять режимов - -| Режим | Что показывает | -|---|---| -| `overview` *(default)* | Компактная сводка: свойства, состав (сгруппирован по типам), дочерние подсистемы, наличие CI | -| `content` | Список Content с группировкой по типу объекта. `-Name Catalog` — только каталоги | -| `ci` | Разбор CommandInterface.xml: видимость, размещение, порядок команд/подсистем/групп | -| `tree` | Рекурсивное дерево иерархии подсистем с маркерами [CI], [OneCmd], [Скрыт] | -| `full` | Полная сводка: overview + content + ci в одном вызове | - -## Примеры - -```powershell -# Обзор подсистемы -... -SubsystemPath Subsystems/Продажи.xml - -# Состав подсистемы -... -SubsystemPath Subsystems/Администрирование.xml -Mode content - -# Только документы в составе -... -SubsystemPath Subsystems/Продажи.xml -Mode content -Name Document - -# Командный интерфейс подсистемы -... -SubsystemPath Subsystems/Продажи.xml -Mode ci - -# Дерево подсистем от корня -... -SubsystemPath Subsystems -Mode tree - -# Дерево от конкретной подсистемы -... -SubsystemPath Subsystems/Администрирование.xml -Mode tree - -# Дерево только для одной подсистемы -... -SubsystemPath Subsystems -Mode tree -Name Администрирование -``` +--- +name: subsystem-info +description: Анализ структуры подсистемы 1С из XML-выгрузки — состав, дочерние подсистемы, командный интерфейс, дерево иерархии. Используй для изучения структуры подсистем и навигации по конфигурации +argument-hint: <SubsystemPath> [-Mode overview|content|ci|tree|full] [-Name <элемент>] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /subsystem-info — Структура подсистемы 1С + +Читает XML подсистемы из выгрузки конфигурации 1С и выводит компактное описание структуры. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `SubsystemPath` | Путь к XML-файлу подсистемы, каталогу подсистемы или каталогу `Subsystems/` (для tree) | +| `Mode` | Режим: `overview` (default), `content`, `ci`, `tree`, `full` | +| `Name` | Drill-down: тип объекта в content, секция в ci, имя подсистемы в tree | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-info.ps1" -SubsystemPath "<путь>" +``` + +## Пять режимов + +| Режим | Что показывает | +|---|---| +| `overview` *(default)* | Компактная сводка: свойства, состав (сгруппирован по типам), дочерние подсистемы, наличие CI | +| `content` | Список Content с группировкой по типу объекта. `-Name Catalog` — только каталоги | +| `ci` | Разбор CommandInterface.xml: видимость, размещение, порядок команд/подсистем/групп | +| `tree` | Рекурсивное дерево иерархии подсистем с маркерами [CI], [OneCmd], [Скрыт] | +| `full` | Полная сводка: overview + content + ci в одном вызове | + +## Примеры + +```powershell +# Обзор подсистемы +... -SubsystemPath Subsystems/Продажи.xml + +# Состав подсистемы +... -SubsystemPath Subsystems/Администрирование.xml -Mode content + +# Только документы в составе +... -SubsystemPath Subsystems/Продажи.xml -Mode content -Name Document + +# Командный интерфейс подсистемы +... -SubsystemPath Subsystems/Продажи.xml -Mode ci + +# Дерево подсистем от корня +... -SubsystemPath Subsystems -Mode tree + +# Дерево от конкретной подсистемы +... -SubsystemPath Subsystems/Администрирование.xml -Mode tree + +# Дерево только для одной подсистемы +... -SubsystemPath Subsystems -Mode tree -Name Администрирование +``` diff --git a/.claude/skills/subsystem-info/scripts/subsystem-info.ps1 b/.claude/skills/subsystem-info/scripts/subsystem-info.ps1 index 3c031849..8ad7fc63 100644 --- a/.claude/skills/subsystem-info/scripts/subsystem-info.ps1 +++ b/.claude/skills/subsystem-info/scripts/subsystem-info.ps1 @@ -1,574 +1,574 @@ -# subsystem-info v1.1 — Compact summary of 1C subsystem structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath, - [ValidateSet("overview","content","ci","tree","full")] - [string]$Mode = "overview", - [string]$Name, - [int]$Limit = 150, - [int]$Offset = 0, - [string]$OutFile -) - -$ErrorActionPreference = 'Stop' -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Output helper --- -$script:lines = @() -function Out([string]$text) { $script:lines += $text } - -function Get-SupportStatusForPath([string]$targetPath) { - try { - $rp = (Resolve-Path $targetPath).Path - $elemUuid = $null - $binPath = $null - # Reads the uuid of the first metadata element in an .xml file (or $null). - function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null - } - # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). - $elemUuid = Get-RootUuid $rp - $d = [System.IO.Path]::GetDirectoryName($rp) - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $binPath) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } - } - if ($elemUuid -and $binPath) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $h.Success) { return "не на поддержке" } - $G = [int]$h.Groups[1].Value - $K = [int]$h.Groups[2].Value - if ($K -eq 0) { return "снято с поддержки (правки свободны)" } - if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } - if (-not $elemUuid) { return "не на поддержке" } - $u = [regex]::Escape($elemUuid.ToLower()) - $best = $null - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - if ($null -eq $best) { return "не на поддержке" } - switch ($best) { - 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } - 1 { return "редактируется с сохранением поддержки" } - 2 { return "снято с поддержки (правки свободны)" } - } - return "не на поддержке" - } catch { return "не на поддержке" } -} - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { - $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath -} - -# --- Helper: get LocalString text --- -function Get-MLText($node) { - if (-not $node -or -not $node.HasChildNodes) { return "" } - foreach ($item in $node.ChildNodes) { - if ($item.NodeType -ne 'Element') { continue } - $lang = ""; $content = "" - foreach ($c in $item.ChildNodes) { - if ($c.NodeType -ne 'Element') { continue } - if ($c.LocalName -eq "lang") { $lang = $c.InnerText } - if ($c.LocalName -eq "content") { $content = $c.InnerText } - } - if ($lang -eq "ru" -and $content) { return $content } - } - # fallback: first item - foreach ($item in $node.ChildNodes) { - if ($item.NodeType -ne 'Element') { continue } - foreach ($c in $item.ChildNodes) { - if ($c.NodeType -ne 'Element') { continue } - if ($c.LocalName -eq "content" -and $c.InnerText) { return $c.InnerText } - } - } - return "" -} - -# --- Helper: load subsystem XML --- -function Load-SubsystemXml([string]$xmlPath) { - [xml]$doc = Get-Content -Path $xmlPath -Encoding UTF8 - $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) - $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - $sub = $doc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns) - if (-not $sub) { - Write-Host "[ERROR] Not a valid subsystem XML: $xmlPath" - exit 1 - } - return @{ Doc=$doc; Ns=$ns; Sub=$sub } -} - -# --- Helper: get content items --- -function Get-ContentItems($props, $ns) { - $items = @() - $contentNode = $props.SelectSingleNode("md:Content", $ns) - if (-not $contentNode -or -not $contentNode.HasChildNodes) { return $items } - foreach ($item in $contentNode.SelectNodes("xr:Item", $ns)) { - $items += $item.InnerText - } - return $items -} - -# --- Helper: get child subsystem names --- -function Get-ChildNames($sub, $ns) { - $names = @() - $co = $sub.SelectSingleNode("md:ChildObjects", $ns) - if (-not $co -or -not $co.HasChildNodes) { return $names } - foreach ($child in $co.ChildNodes) { - if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") { - $names += $child.InnerText - } - } - return $names -} - -# --- Helper: group content by type --- -function Group-ContentByType($items) { - $groups = [ordered]@{} - foreach ($item in $items) { - if ($item -match '^([^.]+)\.(.+)$') { - $type = $Matches[1] - $name = $Matches[2] - } elseif ($item -match '^[0-9a-fA-F]{8}-') { - $type = "[UUID]" - $name = $item - } else { - $type = "[Other]" - $name = $item - } - if (-not $groups.Contains($type)) { $groups[$type] = @() } - $groups[$type] += $name - } - return $groups -} - -# --- Helper: find subsystem dir from XML path --- -function Get-SubsystemDir([string]$xmlPath) { - $dir = [System.IO.Path]::GetDirectoryName($xmlPath) - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($xmlPath) - return Join-Path $dir $baseName -} - -# --- Show functions for full mode --- -function Show-Overview { - Out "Подсистема: $subName" - Out "Поддержка: $(Get-SupportStatusForPath $SubsystemPath)" - if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" } - if ($commentText) { Out "Комментарий: $commentText" } - Out "ВключатьВКомандныйИнтерфейс: $inclCI" - Out "ИспользоватьОднуКоманду: $useOneCmd" - if ($explanation) { Out "Пояснение: $explanation" } - if ($picText) { Out "Картинка: $picText" } - if ($contentItems.Count -gt 0) { - $parts = @() - foreach ($type in $groups.Keys) { - $parts += "$type`: $($groups[$type].Count)" - } - Out "Состав: $($contentItems.Count) объектов ($($parts -join ', '))" - } else { - Out "Состав: пусто" - } - if ($childNames.Count -gt 0) { - Out "Дочерние подсистемы ($($childNames.Count)): $($childNames -join ', ')" - } - if ($hasCI) { - Out "Командный интерфейс: есть" - } -} - -function Show-Content { - Out "Состав подсистемы $subName ($($contentItems.Count) объектов):" - Out "" - if ($Name) { - if ($groups.Contains($Name)) { - $filtered = $groups[$Name] - Out "$Name ($($filtered.Count)):" - foreach ($n in $filtered) { Out " $n" } - } else { - Out "[INFO] Тип '$Name' не найден в составе." - Out "Доступные типы: $($groups.Keys -join ', ')" - } - } else { - foreach ($type in $groups.Keys) { - Out "$type ($($groups[$type].Count)):" - foreach ($n in $groups[$type]) { Out " $n" } - Out "" - } - } -} - -function Show-CI { - $localSubDir = Get-SubsystemDir $SubsystemPath - $localCiPath = Join-Path (Join-Path $localSubDir "Ext") "CommandInterface.xml" - - if (-not (Test-Path $localCiPath)) { - Out "Командный интерфейс: $subName" - Out "" - Out "Файл CommandInterface.xml не найден." - Out "Путь: $localCiPath" - } else { - [xml]$ciDoc = Get-Content -Path $localCiPath -Encoding UTF8 - $ciNs = New-Object System.Xml.XmlNamespaceManager($ciDoc.NameTable) - $ciNs.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops") - $ciNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - - $ciRoot = $ciDoc.DocumentElement - - Out "Командный интерфейс: $subName" - Out "" - - # --- CommandsVisibility --- - $visSection = $ciRoot.SelectSingleNode("ci:CommandsVisibility", $ciNs) - if ($visSection) { - $hidden = @(); $shown = @() - foreach ($cmd in $visSection.SelectNodes("ci:Command", $ciNs)) { - $cmdName = $cmd.GetAttribute("name") - $vis = $cmd.SelectSingleNode("ci:Visibility/xr:Common", $ciNs) - if ($vis -and $vis.InnerText -eq "false") { $hidden += $cmdName } - else { $shown += $cmdName } - } - $total = $hidden.Count + $shown.Count - if (-not $Name -or $Name -eq "visibility") { - Out "Видимость ($total):" - if ($hidden.Count -gt 0) { - Out " СКРЫТО ($($hidden.Count)):" - foreach ($h in $hidden) { Out " $h" } - } - if ($shown.Count -gt 0) { - Out " ПОКАЗАНО ($($shown.Count)):" - foreach ($s in $shown) { Out " $s" } - } - Out "" - } - } - - # --- CommandsPlacement --- - $placeSection = $ciRoot.SelectSingleNode("ci:CommandsPlacement", $ciNs) - if ($placeSection) { - $placements = @() - foreach ($cmd in $placeSection.SelectNodes("ci:Command", $ciNs)) { - $cmdName = $cmd.GetAttribute("name") - $grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs) - $pl = $cmd.SelectSingleNode("ci:Placement", $ciNs) - $grpText = if ($grp) { $grp.InnerText } else { "?" } - $plText = if ($pl) { $pl.InnerText } else { "?" } - $placements += @{ Name=$cmdName; Group=$grpText; Placement=$plText } - } - if ((-not $Name -or $Name -eq "placement") -and $placements.Count -gt 0) { - Out "Размещение ($($placements.Count)):" - $arrow = [char]0x2192 - foreach ($p in $placements) { - Out " $($p.Name) $arrow $($p.Group) ($($p.Placement))" - } - Out "" - } - } - - # --- CommandsOrder --- - $orderSection = $ciRoot.SelectSingleNode("ci:CommandsOrder", $ciNs) - if ($orderSection) { - $orderGroups = [ordered]@{} - foreach ($cmd in $orderSection.SelectNodes("ci:Command", $ciNs)) { - $cmdName = $cmd.GetAttribute("name") - $grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs) - $grpText = if ($grp) { $grp.InnerText } else { "?" } - if (-not $orderGroups.Contains($grpText)) { $orderGroups[$grpText] = @() } - $orderGroups[$grpText] += $cmdName - } - $totalOrder = 0 - foreach ($k in $orderGroups.Keys) { $totalOrder += $orderGroups[$k].Count } - if ((-not $Name -or $Name -eq "order") -and $totalOrder -gt 0) { - Out "Порядок команд ($totalOrder):" - foreach ($grpName in $orderGroups.Keys) { - Out " [$grpName]:" - foreach ($c in $orderGroups[$grpName]) { Out " $c" } - } - Out "" - } - } - - # --- SubsystemsOrder --- - $subOrderSection = $ciRoot.SelectSingleNode("ci:SubsystemsOrder", $ciNs) - if ($subOrderSection) { - $subOrder = @() - foreach ($s in $subOrderSection.SelectNodes("ci:Subsystem", $ciNs)) { - $subOrder += $s.InnerText - } - if ((-not $Name -or $Name -eq "subsystems") -and $subOrder.Count -gt 0) { - Out "Порядок подсистем ($($subOrder.Count)):" - for ($i = 0; $i -lt $subOrder.Count; $i++) { - Out " $($i+1). $($subOrder[$i])" - } - Out "" - } - } - - # --- GroupsOrder --- - $grpOrderSection = $ciRoot.SelectSingleNode("ci:GroupsOrder", $ciNs) - if ($grpOrderSection) { - $grpOrder = @() - foreach ($g in $grpOrderSection.SelectNodes("ci:Group", $ciNs)) { - $grpOrder += $g.InnerText - } - if ((-not $Name -or $Name -eq "groups") -and $grpOrder.Count -gt 0) { - Out "Порядок групп ($($grpOrder.Count)):" - foreach ($g in $grpOrder) { Out " $g" } - } - } - } -} - -# ============================================================ -# Mode: tree -# ============================================================ -if ($Mode -eq "tree") { - $isDir = Test-Path $SubsystemPath -PathType Container - $rootDir = $null - $rootXml = $null - - if ($isDir) { - # Subsystems/ directory — show all top-level subsystems - $rootDir = $SubsystemPath - } else { - # Specific subsystem XML — show tree from this subsystem - if (-not (Test-Path $SubsystemPath)) { - Write-Host "[ERROR] File not found: $SubsystemPath" - exit 1 - } - $rootXml = $SubsystemPath - } - - # Box-drawing chars (PS 5.1 compatible) - $script:T_BRANCH = [char]0x251C + [char]0x2500 + [char]0x2500 + " " # ├── - $script:T_LAST = [char]0x2514 + [char]0x2500 + [char]0x2500 + " " # └── - $script:T_PIPE = [char]0x2502 + " " # │ - $script:T_ARROW = [char]0x2192 # → - - function Get-TreeLine([string]$xmlPath) { - $parsed = Load-SubsystemXml $xmlPath - $sub = $parsed.Sub; $ns = $parsed.Ns - $props = $sub.SelectSingleNode("md:Properties", $ns) - $name = $props.SelectSingleNode("md:Name", $ns).InnerText - - $markers = @() - $subDir = Get-SubsystemDir $xmlPath - $ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml" - if (Test-Path $ciPath) { $markers += "CI" } - $useOne = $props.SelectSingleNode("md:UseOneCommand", $ns) - if ($useOne -and $useOne.InnerText -eq "true") { $markers += "OneCmd" } - $inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns) - if ($inclCI -and $inclCI.InnerText -eq "false") { $markers += "Скрыт" } - $markerStr = if ($markers.Count -gt 0) { " [$($markers -join ', ')]" } else { "" } - - $contentItems = @(Get-ContentItems $props $ns) - $childNames = @(Get-ChildNames $sub $ns) - $childStr = if ($childNames.Count -gt 0) { ", $($childNames.Count) дочерних" } else { "" } - - return @{ - Label = "$name$markerStr ($($contentItems.Count) объектов$childStr)" - SubDir = $subDir - ChildNames = $childNames - } - } - - function Build-TreeEntry([string]$xmlPath, [string]$prefix, [bool]$isLast, [bool]$isRoot) { - $info = Get-TreeLine $xmlPath - - $connector = if ($isRoot) { "" } elseif ($isLast) { $script:T_LAST } else { $script:T_BRANCH } - Out "$prefix$connector$($info.Label)" - - if ($info.ChildNames.Count -gt 0) { - $childPrefix = if ($isRoot) { "" } elseif ($isLast) { "$prefix " } else { "$prefix$($script:T_PIPE)" } - $subsDir = Join-Path $info.SubDir "Subsystems" - for ($i = 0; $i -lt $info.ChildNames.Count; $i++) { - $childXml = Join-Path $subsDir "$($info.ChildNames[$i]).xml" - $childIsLast = ($i -eq $info.ChildNames.Count - 1) - if (Test-Path $childXml) { - Build-TreeEntry $childXml $childPrefix $childIsLast $false - } else { - $conn2 = if ($childIsLast) { $script:T_LAST } else { $script:T_BRANCH } - Out "$childPrefix$conn2$($info.ChildNames[$i]) [NOT FOUND]" - } - } - } - } - - if ($rootDir) { - $label = Split-Path $rootDir -Leaf - Out "Дерево подсистем от: $label/" - Out "" - $xmlFiles = @(Get-ChildItem $rootDir -Filter "*.xml" -File | Sort-Object Name) - if ($Name) { - $xmlFiles = @($xmlFiles | Where-Object { $_.BaseName -eq $Name }) - if ($xmlFiles.Count -eq 0) { - Write-Host "[ERROR] Subsystem '$Name' not found in $rootDir" - exit 1 - } - } - for ($i = 0; $i -lt $xmlFiles.Count; $i++) { - Build-TreeEntry $xmlFiles[$i].FullName "" ($i -eq $xmlFiles.Count - 1) $true - } - } else { - Build-TreeEntry $rootXml "" $true $true - } - -} elseif ($Mode -eq "ci") { -# ============================================================ -# Mode: ci — CommandInterface.xml -# ============================================================ - if (Test-Path $SubsystemPath -PathType Container) { - Write-Host "[ERROR] ci mode requires a subsystem .xml file, not a directory" - exit 1 - } - # File not found — check Dir/Name/Name.xml → Dir/Name.xml - if (-not (Test-Path $SubsystemPath)) { - $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) - $pd = Split-Path $SubsystemPath - if ($fn -eq (Split-Path $pd -Leaf)) { - $c = Join-Path (Split-Path $pd) "$fn.xml" - if (Test-Path $c) { $SubsystemPath = $c } - } - } - if (-not (Test-Path $SubsystemPath)) { - Write-Host "[ERROR] File not found: $SubsystemPath" - exit 1 - } - - $parsed = Load-SubsystemXml $SubsystemPath - $sub = $parsed.Sub; $ns = $parsed.Ns - $props = $sub.SelectSingleNode("md:Properties", $ns) - $subName = $props.SelectSingleNode("md:Name", $ns).InnerText - - Show-CI - -} else { -# ============================================================ -# Mode: overview / content — requires a subsystem XML file -# ============================================================ - if (Test-Path $SubsystemPath -PathType Container) { - $dirName = Split-Path $SubsystemPath -Leaf - $candidate = Join-Path $SubsystemPath "$dirName.xml" - $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" - if (Test-Path $candidate) { - $SubsystemPath = $candidate - } elseif (Test-Path $sibling) { - $SubsystemPath = $sibling - } else { - Write-Host "[ERROR] No $dirName.xml found in directory. Use -Mode tree for directory listing." - exit 1 - } - } - - # File not found — check Dir/Name/Name.xml → Dir/Name.xml - if (-not (Test-Path $SubsystemPath)) { - $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) - $pd = Split-Path $SubsystemPath - if ($fn -eq (Split-Path $pd -Leaf)) { - $c = Join-Path (Split-Path $pd) "$fn.xml" - if (Test-Path $c) { $SubsystemPath = $c } - } - } - if (-not (Test-Path $SubsystemPath)) { - Write-Host "[ERROR] File not found: $SubsystemPath" - exit 1 - } - - $parsed = Load-SubsystemXml $SubsystemPath - $sub = $parsed.Sub; $ns = $parsed.Ns - $props = $sub.SelectSingleNode("md:Properties", $ns) - - $subName = $props.SelectSingleNode("md:Name", $ns).InnerText - $synonym = Get-MLText $props.SelectSingleNode("md:Synonym", $ns) - $comment = $props.SelectSingleNode("md:Comment", $ns) - $commentText = if ($comment -and $comment.InnerText) { $comment.InnerText } else { "" } - $inclHelp = $props.SelectSingleNode("md:IncludeHelpInContents", $ns).InnerText - $inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns).InnerText - $useOneCmd = $props.SelectSingleNode("md:UseOneCommand", $ns).InnerText - $explanation = Get-MLText $props.SelectSingleNode("md:Explanation", $ns) - - # Picture - $picNode = $props.SelectSingleNode("md:Picture", $ns) - $picText = "" - if ($picNode -and $picNode.HasChildNodes) { - $picRef = $picNode.SelectSingleNode("xr:Ref", $ns) - if ($picRef -and $picRef.InnerText) { $picText = $picRef.InnerText } - } - - # Content - $contentItems = @(Get-ContentItems $props $ns) - $groups = Group-ContentByType $contentItems - - # Children - $childNames = @(Get-ChildNames $sub $ns) - - # CI presence - $subDir = Get-SubsystemDir $SubsystemPath - $ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml" - $hasCI = Test-Path $ciPath - - if ($Mode -eq "overview") { - Show-Overview - } elseif ($Mode -eq "content") { - Show-Content - } elseif ($Mode -eq "full") { - Show-Overview - Out ""; Out "--- content ---"; Out "" - Show-Content - Out ""; Out "--- ci ---"; Out "" - Show-CI - } -} - -# --- Pagination and output --- -$totalLines = $script:lines.Count -$outLines = $script:lines - -if ($Offset -gt 0) { - if ($Offset -ge $totalLines) { - Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." - exit 0 - } - $outLines = $outLines[$Offset..($totalLines - 1)] -} - -if ($Limit -gt 0 -and $outLines.Count -gt $Limit) { - $shown = $outLines[0..($Limit - 1)] - $remaining = $totalLines - $Offset - $Limit - $shown += "" - $shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения." - $outLines = $shown -} - -if ($OutFile) { - if (-not [System.IO.Path]::IsPathRooted($OutFile)) { - $OutFile = Join-Path (Get-Location).Path $OutFile - } - $utf8 = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8) - Write-Host "Output written to $OutFile" -} else { - foreach ($l in $outLines) { Write-Host $l } -} +# subsystem-info v1.1 — Compact summary of 1C subsystem structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath, + [ValidateSet("overview","content","ci","tree","full")] + [string]$Mode = "overview", + [string]$Name, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +function Get-SupportStatusForPath([string]$targetPath) { + try { + $rp = (Resolve-Path $targetPath).Path + $elemUuid = $null + $binPath = $null + # Reads the uuid of the first metadata element in an .xml file (or $null). + function Get-RootUuid([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $null } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } + } catch {} + return $null + } + # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). + $elemUuid = Get-RootUuid $rp + $d = [System.IO.Path]::GetDirectoryName($rp) + for ($i = 0; $i -lt 12 -and $d; $i++) { + if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } + if (-not $binPath) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand } + } + if ($elemUuid -and $binPath) { break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $elemUuid) { return "не на поддержке" } + $u = [regex]::Escape($elemUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { + $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath +} + +# --- Helper: get LocalString text --- +function Get-MLText($node) { + if (-not $node -or -not $node.HasChildNodes) { return "" } + foreach ($item in $node.ChildNodes) { + if ($item.NodeType -ne 'Element') { continue } + $lang = ""; $content = "" + foreach ($c in $item.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + if ($c.LocalName -eq "lang") { $lang = $c.InnerText } + if ($c.LocalName -eq "content") { $content = $c.InnerText } + } + if ($lang -eq "ru" -and $content) { return $content } + } + # fallback: first item + foreach ($item in $node.ChildNodes) { + if ($item.NodeType -ne 'Element') { continue } + foreach ($c in $item.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + if ($c.LocalName -eq "content" -and $c.InnerText) { return $c.InnerText } + } + } + return "" +} + +# --- Helper: load subsystem XML --- +function Load-SubsystemXml([string]$xmlPath) { + [xml]$doc = Get-Content -Path $xmlPath -Encoding UTF8 + $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + $sub = $doc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns) + if (-not $sub) { + Write-Host "[ERROR] Not a valid subsystem XML: $xmlPath" + exit 1 + } + return @{ Doc=$doc; Ns=$ns; Sub=$sub } +} + +# --- Helper: get content items --- +function Get-ContentItems($props, $ns) { + $items = @() + $contentNode = $props.SelectSingleNode("md:Content", $ns) + if (-not $contentNode -or -not $contentNode.HasChildNodes) { return $items } + foreach ($item in $contentNode.SelectNodes("xr:Item", $ns)) { + $items += $item.InnerText + } + return $items +} + +# --- Helper: get child subsystem names --- +function Get-ChildNames($sub, $ns) { + $names = @() + $co = $sub.SelectSingleNode("md:ChildObjects", $ns) + if (-not $co -or -not $co.HasChildNodes) { return $names } + foreach ($child in $co.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") { + $names += $child.InnerText + } + } + return $names +} + +# --- Helper: group content by type --- +function Group-ContentByType($items) { + $groups = [ordered]@{} + foreach ($item in $items) { + if ($item -match '^([^.]+)\.(.+)$') { + $type = $Matches[1] + $name = $Matches[2] + } elseif ($item -match '^[0-9a-fA-F]{8}-') { + $type = "[UUID]" + $name = $item + } else { + $type = "[Other]" + $name = $item + } + if (-not $groups.Contains($type)) { $groups[$type] = @() } + $groups[$type] += $name + } + return $groups +} + +# --- Helper: find subsystem dir from XML path --- +function Get-SubsystemDir([string]$xmlPath) { + $dir = [System.IO.Path]::GetDirectoryName($xmlPath) + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($xmlPath) + return Join-Path $dir $baseName +} + +# --- Show functions for full mode --- +function Show-Overview { + Out "Подсистема: $subName" + Out "Поддержка: $(Get-SupportStatusForPath $SubsystemPath)" + if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" } + if ($commentText) { Out "Комментарий: $commentText" } + Out "ВключатьВКомандныйИнтерфейс: $inclCI" + Out "ИспользоватьОднуКоманду: $useOneCmd" + if ($explanation) { Out "Пояснение: $explanation" } + if ($picText) { Out "Картинка: $picText" } + if ($contentItems.Count -gt 0) { + $parts = @() + foreach ($type in $groups.Keys) { + $parts += "$type`: $($groups[$type].Count)" + } + Out "Состав: $($contentItems.Count) объектов ($($parts -join ', '))" + } else { + Out "Состав: пусто" + } + if ($childNames.Count -gt 0) { + Out "Дочерние подсистемы ($($childNames.Count)): $($childNames -join ', ')" + } + if ($hasCI) { + Out "Командный интерфейс: есть" + } +} + +function Show-Content { + Out "Состав подсистемы $subName ($($contentItems.Count) объектов):" + Out "" + if ($Name) { + if ($groups.Contains($Name)) { + $filtered = $groups[$Name] + Out "$Name ($($filtered.Count)):" + foreach ($n in $filtered) { Out " $n" } + } else { + Out "[INFO] Тип '$Name' не найден в составе." + Out "Доступные типы: $($groups.Keys -join ', ')" + } + } else { + foreach ($type in $groups.Keys) { + Out "$type ($($groups[$type].Count)):" + foreach ($n in $groups[$type]) { Out " $n" } + Out "" + } + } +} + +function Show-CI { + $localSubDir = Get-SubsystemDir $SubsystemPath + $localCiPath = Join-Path (Join-Path $localSubDir "Ext") "CommandInterface.xml" + + if (-not (Test-Path $localCiPath)) { + Out "Командный интерфейс: $subName" + Out "" + Out "Файл CommandInterface.xml не найден." + Out "Путь: $localCiPath" + } else { + [xml]$ciDoc = Get-Content -Path $localCiPath -Encoding UTF8 + $ciNs = New-Object System.Xml.XmlNamespaceManager($ciDoc.NameTable) + $ciNs.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops") + $ciNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + $ciRoot = $ciDoc.DocumentElement + + Out "Командный интерфейс: $subName" + Out "" + + # --- CommandsVisibility --- + $visSection = $ciRoot.SelectSingleNode("ci:CommandsVisibility", $ciNs) + if ($visSection) { + $hidden = @(); $shown = @() + foreach ($cmd in $visSection.SelectNodes("ci:Command", $ciNs)) { + $cmdName = $cmd.GetAttribute("name") + $vis = $cmd.SelectSingleNode("ci:Visibility/xr:Common", $ciNs) + if ($vis -and $vis.InnerText -eq "false") { $hidden += $cmdName } + else { $shown += $cmdName } + } + $total = $hidden.Count + $shown.Count + if (-not $Name -or $Name -eq "visibility") { + Out "Видимость ($total):" + if ($hidden.Count -gt 0) { + Out " СКРЫТО ($($hidden.Count)):" + foreach ($h in $hidden) { Out " $h" } + } + if ($shown.Count -gt 0) { + Out " ПОКАЗАНО ($($shown.Count)):" + foreach ($s in $shown) { Out " $s" } + } + Out "" + } + } + + # --- CommandsPlacement --- + $placeSection = $ciRoot.SelectSingleNode("ci:CommandsPlacement", $ciNs) + if ($placeSection) { + $placements = @() + foreach ($cmd in $placeSection.SelectNodes("ci:Command", $ciNs)) { + $cmdName = $cmd.GetAttribute("name") + $grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs) + $pl = $cmd.SelectSingleNode("ci:Placement", $ciNs) + $grpText = if ($grp) { $grp.InnerText } else { "?" } + $plText = if ($pl) { $pl.InnerText } else { "?" } + $placements += @{ Name=$cmdName; Group=$grpText; Placement=$plText } + } + if ((-not $Name -or $Name -eq "placement") -and $placements.Count -gt 0) { + Out "Размещение ($($placements.Count)):" + $arrow = [char]0x2192 + foreach ($p in $placements) { + Out " $($p.Name) $arrow $($p.Group) ($($p.Placement))" + } + Out "" + } + } + + # --- CommandsOrder --- + $orderSection = $ciRoot.SelectSingleNode("ci:CommandsOrder", $ciNs) + if ($orderSection) { + $orderGroups = [ordered]@{} + foreach ($cmd in $orderSection.SelectNodes("ci:Command", $ciNs)) { + $cmdName = $cmd.GetAttribute("name") + $grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs) + $grpText = if ($grp) { $grp.InnerText } else { "?" } + if (-not $orderGroups.Contains($grpText)) { $orderGroups[$grpText] = @() } + $orderGroups[$grpText] += $cmdName + } + $totalOrder = 0 + foreach ($k in $orderGroups.Keys) { $totalOrder += $orderGroups[$k].Count } + if ((-not $Name -or $Name -eq "order") -and $totalOrder -gt 0) { + Out "Порядок команд ($totalOrder):" + foreach ($grpName in $orderGroups.Keys) { + Out " [$grpName]:" + foreach ($c in $orderGroups[$grpName]) { Out " $c" } + } + Out "" + } + } + + # --- SubsystemsOrder --- + $subOrderSection = $ciRoot.SelectSingleNode("ci:SubsystemsOrder", $ciNs) + if ($subOrderSection) { + $subOrder = @() + foreach ($s in $subOrderSection.SelectNodes("ci:Subsystem", $ciNs)) { + $subOrder += $s.InnerText + } + if ((-not $Name -or $Name -eq "subsystems") -and $subOrder.Count -gt 0) { + Out "Порядок подсистем ($($subOrder.Count)):" + for ($i = 0; $i -lt $subOrder.Count; $i++) { + Out " $($i+1). $($subOrder[$i])" + } + Out "" + } + } + + # --- GroupsOrder --- + $grpOrderSection = $ciRoot.SelectSingleNode("ci:GroupsOrder", $ciNs) + if ($grpOrderSection) { + $grpOrder = @() + foreach ($g in $grpOrderSection.SelectNodes("ci:Group", $ciNs)) { + $grpOrder += $g.InnerText + } + if ((-not $Name -or $Name -eq "groups") -and $grpOrder.Count -gt 0) { + Out "Порядок групп ($($grpOrder.Count)):" + foreach ($g in $grpOrder) { Out " $g" } + } + } + } +} + +# ============================================================ +# Mode: tree +# ============================================================ +if ($Mode -eq "tree") { + $isDir = Test-Path $SubsystemPath -PathType Container + $rootDir = $null + $rootXml = $null + + if ($isDir) { + # Subsystems/ directory — show all top-level subsystems + $rootDir = $SubsystemPath + } else { + # Specific subsystem XML — show tree from this subsystem + if (-not (Test-Path $SubsystemPath)) { + Write-Host "[ERROR] File not found: $SubsystemPath" + exit 1 + } + $rootXml = $SubsystemPath + } + + # Box-drawing chars (PS 5.1 compatible) + $script:T_BRANCH = [char]0x251C + [char]0x2500 + [char]0x2500 + " " # ├── + $script:T_LAST = [char]0x2514 + [char]0x2500 + [char]0x2500 + " " # └── + $script:T_PIPE = [char]0x2502 + " " # │ + $script:T_ARROW = [char]0x2192 # → + + function Get-TreeLine([string]$xmlPath) { + $parsed = Load-SubsystemXml $xmlPath + $sub = $parsed.Sub; $ns = $parsed.Ns + $props = $sub.SelectSingleNode("md:Properties", $ns) + $name = $props.SelectSingleNode("md:Name", $ns).InnerText + + $markers = @() + $subDir = Get-SubsystemDir $xmlPath + $ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml" + if (Test-Path $ciPath) { $markers += "CI" } + $useOne = $props.SelectSingleNode("md:UseOneCommand", $ns) + if ($useOne -and $useOne.InnerText -eq "true") { $markers += "OneCmd" } + $inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns) + if ($inclCI -and $inclCI.InnerText -eq "false") { $markers += "Скрыт" } + $markerStr = if ($markers.Count -gt 0) { " [$($markers -join ', ')]" } else { "" } + + $contentItems = @(Get-ContentItems $props $ns) + $childNames = @(Get-ChildNames $sub $ns) + $childStr = if ($childNames.Count -gt 0) { ", $($childNames.Count) дочерних" } else { "" } + + return @{ + Label = "$name$markerStr ($($contentItems.Count) объектов$childStr)" + SubDir = $subDir + ChildNames = $childNames + } + } + + function Build-TreeEntry([string]$xmlPath, [string]$prefix, [bool]$isLast, [bool]$isRoot) { + $info = Get-TreeLine $xmlPath + + $connector = if ($isRoot) { "" } elseif ($isLast) { $script:T_LAST } else { $script:T_BRANCH } + Out "$prefix$connector$($info.Label)" + + if ($info.ChildNames.Count -gt 0) { + $childPrefix = if ($isRoot) { "" } elseif ($isLast) { "$prefix " } else { "$prefix$($script:T_PIPE)" } + $subsDir = Join-Path $info.SubDir "Subsystems" + for ($i = 0; $i -lt $info.ChildNames.Count; $i++) { + $childXml = Join-Path $subsDir "$($info.ChildNames[$i]).xml" + $childIsLast = ($i -eq $info.ChildNames.Count - 1) + if (Test-Path $childXml) { + Build-TreeEntry $childXml $childPrefix $childIsLast $false + } else { + $conn2 = if ($childIsLast) { $script:T_LAST } else { $script:T_BRANCH } + Out "$childPrefix$conn2$($info.ChildNames[$i]) [NOT FOUND]" + } + } + } + } + + if ($rootDir) { + $label = Split-Path $rootDir -Leaf + Out "Дерево подсистем от: $label/" + Out "" + $xmlFiles = @(Get-ChildItem $rootDir -Filter "*.xml" -File | Sort-Object Name) + if ($Name) { + $xmlFiles = @($xmlFiles | Where-Object { $_.BaseName -eq $Name }) + if ($xmlFiles.Count -eq 0) { + Write-Host "[ERROR] Subsystem '$Name' not found in $rootDir" + exit 1 + } + } + for ($i = 0; $i -lt $xmlFiles.Count; $i++) { + Build-TreeEntry $xmlFiles[$i].FullName "" ($i -eq $xmlFiles.Count - 1) $true + } + } else { + Build-TreeEntry $rootXml "" $true $true + } + +} elseif ($Mode -eq "ci") { +# ============================================================ +# Mode: ci — CommandInterface.xml +# ============================================================ + if (Test-Path $SubsystemPath -PathType Container) { + Write-Host "[ERROR] ci mode requires a subsystem .xml file, not a directory" + exit 1 + } + # File not found — check Dir/Name/Name.xml → Dir/Name.xml + if (-not (Test-Path $SubsystemPath)) { + $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) + $pd = Split-Path $SubsystemPath + if ($fn -eq (Split-Path $pd -Leaf)) { + $c = Join-Path (Split-Path $pd) "$fn.xml" + if (Test-Path $c) { $SubsystemPath = $c } + } + } + if (-not (Test-Path $SubsystemPath)) { + Write-Host "[ERROR] File not found: $SubsystemPath" + exit 1 + } + + $parsed = Load-SubsystemXml $SubsystemPath + $sub = $parsed.Sub; $ns = $parsed.Ns + $props = $sub.SelectSingleNode("md:Properties", $ns) + $subName = $props.SelectSingleNode("md:Name", $ns).InnerText + + Show-CI + +} else { +# ============================================================ +# Mode: overview / content — requires a subsystem XML file +# ============================================================ + if (Test-Path $SubsystemPath -PathType Container) { + $dirName = Split-Path $SubsystemPath -Leaf + $candidate = Join-Path $SubsystemPath "$dirName.xml" + $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" + if (Test-Path $candidate) { + $SubsystemPath = $candidate + } elseif (Test-Path $sibling) { + $SubsystemPath = $sibling + } else { + Write-Host "[ERROR] No $dirName.xml found in directory. Use -Mode tree for directory listing." + exit 1 + } + } + + # File not found — check Dir/Name/Name.xml → Dir/Name.xml + if (-not (Test-Path $SubsystemPath)) { + $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) + $pd = Split-Path $SubsystemPath + if ($fn -eq (Split-Path $pd -Leaf)) { + $c = Join-Path (Split-Path $pd) "$fn.xml" + if (Test-Path $c) { $SubsystemPath = $c } + } + } + if (-not (Test-Path $SubsystemPath)) { + Write-Host "[ERROR] File not found: $SubsystemPath" + exit 1 + } + + $parsed = Load-SubsystemXml $SubsystemPath + $sub = $parsed.Sub; $ns = $parsed.Ns + $props = $sub.SelectSingleNode("md:Properties", $ns) + + $subName = $props.SelectSingleNode("md:Name", $ns).InnerText + $synonym = Get-MLText $props.SelectSingleNode("md:Synonym", $ns) + $comment = $props.SelectSingleNode("md:Comment", $ns) + $commentText = if ($comment -and $comment.InnerText) { $comment.InnerText } else { "" } + $inclHelp = $props.SelectSingleNode("md:IncludeHelpInContents", $ns).InnerText + $inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns).InnerText + $useOneCmd = $props.SelectSingleNode("md:UseOneCommand", $ns).InnerText + $explanation = Get-MLText $props.SelectSingleNode("md:Explanation", $ns) + + # Picture + $picNode = $props.SelectSingleNode("md:Picture", $ns) + $picText = "" + if ($picNode -and $picNode.HasChildNodes) { + $picRef = $picNode.SelectSingleNode("xr:Ref", $ns) + if ($picRef -and $picRef.InnerText) { $picText = $picRef.InnerText } + } + + # Content + $contentItems = @(Get-ContentItems $props $ns) + $groups = Group-ContentByType $contentItems + + # Children + $childNames = @(Get-ChildNames $sub $ns) + + # CI presence + $subDir = Get-SubsystemDir $SubsystemPath + $ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml" + $hasCI = Test-Path $ciPath + + if ($Mode -eq "overview") { + Show-Overview + } elseif ($Mode -eq "content") { + Show-Content + } elseif ($Mode -eq "full") { + Show-Overview + Out ""; Out "--- content ---"; Out "" + Show-Content + Out ""; Out "--- ci ---"; Out "" + Show-CI + } +} + +# --- Pagination and output --- +$totalLines = $script:lines.Count +$outLines = $script:lines + +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $outLines = $outLines[$Offset..($totalLines - 1)] +} + +if ($Limit -gt 0 -and $outLines.Count -gt $Limit) { + $shown = $outLines[0..($Limit - 1)] + $remaining = $totalLines - $Offset - $Limit + $shown += "" + $shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения." + $outLines = $shown +} + +if ($OutFile) { + if (-not [System.IO.Path]::IsPathRooted($OutFile)) { + $OutFile = Join-Path (Get-Location).Path $OutFile + } + $utf8 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8) + Write-Host "Output written to $OutFile" +} else { + foreach ($l in $outLines) { Write-Host $l } +} diff --git a/.claude/skills/subsystem-validate/SKILL.md b/.claude/skills/subsystem-validate/SKILL.md index d47902d5..cba0f381 100644 --- a/.claude/skills/subsystem-validate/SKILL.md +++ b/.claude/skills/subsystem-validate/SKILL.md @@ -1,29 +1,29 @@ ---- -name: subsystem-validate -description: Валидация подсистемы 1С. Используй после создания или модификации подсистемы для проверки корректности -argument-hint: <SubsystemPath> [-Detailed] [-MaxErrors 30] -allowed-tools: - - Bash - - Read - - Glob ---- - -# /subsystem-validate — валидация подсистемы 1С - -Проверяет структурную корректность XML-файла подсистемы из выгрузки конфигурации. - -## Параметры - -| Параметр | Обяз. | Умолч. | Описание | -|---------------|:-----:|---------|--------------------------------------------| -| SubsystemPath | да | — | Путь к XML-файлу подсистемы | -| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | -| MaxErrors | нет | 30 | Остановиться после N ошибок | -| OutFile | нет | — | Записать результат в файл | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи" -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml" -``` +--- +name: subsystem-validate +description: Валидация подсистемы 1С. Используй после создания или модификации подсистемы для проверки корректности +argument-hint: <SubsystemPath> [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /subsystem-validate — валидация подсистемы 1С + +Проверяет структурную корректность XML-файла подсистемы из выгрузки конфигурации. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|---------------|:-----:|---------|--------------------------------------------| +| SubsystemPath | да | — | Путь к XML-файлу подсистемы | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи" +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml" +``` diff --git a/.claude/skills/subsystem-validate/scripts/subsystem-validate.ps1 b/.claude/skills/subsystem-validate/scripts/subsystem-validate.ps1 index fa7612fc..5b6bed4f 100644 --- a/.claude/skills/subsystem-validate/scripts/subsystem-validate.ps1 +++ b/.claude/skills/subsystem-validate/scripts/subsystem-validate.ps1 @@ -1,352 +1,352 @@ -# subsystem-validate v1.2 — Validate 1C subsystem XML structure -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, - [switch]$Detailed, - [int]$MaxErrors = 30, - [string]$OutFile -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- Resolve path --- -if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { - $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath -} -if (Test-Path $SubsystemPath -PathType Container) { - $dirName = Split-Path $SubsystemPath -Leaf - $candidate = Join-Path $SubsystemPath "$dirName.xml" - $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" - if (Test-Path $candidate) { $SubsystemPath = $candidate } - elseif (Test-Path $sibling) { $SubsystemPath = $sibling } - else { - Write-Host "[ERROR] No $dirName.xml found in directory: $SubsystemPath" - exit 1 - } -} -# File not found — check Dir/Name/Name.xml → Dir/Name.xml -if (-not (Test-Path $SubsystemPath)) { - $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) - $pd = Split-Path $SubsystemPath - if ($fn -eq (Split-Path $pd -Leaf)) { - $c = Join-Path (Split-Path $pd) "$fn.xml" - if (Test-Path $c) { $SubsystemPath = $c } - } -} -if (-not (Test-Path $SubsystemPath)) { - Write-Host "[ERROR] File not found: $SubsystemPath" - exit 1 -} -$resolvedPath = (Resolve-Path $SubsystemPath).Path - -# --- Output infrastructure --- -$script:errors = 0 -$script:warnings = 0 -$script:stopped = $false -$script:okCount = 0 -$script:output = New-Object System.Text.StringBuilder 8192 - -function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null } -function Report-OK([string]$msg) { - $script:okCount++ - if ($Detailed) { Out-Line "[OK] $msg" } -} -function Report-Error([string]$msg) { - $script:errors++ - Out-Line "[ERROR] $msg" - if ($script:errors -ge $MaxErrors) { $script:stopped = $true } -} -function Report-Warn([string]$msg) { - $script:warnings++ - Out-Line "[WARN] $msg" -} - -$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' - -# Known plural forms that are NOT valid in subsystem Content (platform expects singular) -$knownPluralTypes = @( - "Catalogs","Documents","Enums","Constants","Reports","DataProcessors" - "InformationRegisters","AccumulationRegisters","AccountingRegisters","CalculationRegisters" - "ChartsOfAccounts","ChartsOfCharacteristicTypes","ChartsOfCalculationTypes" - "BusinessProcesses","Tasks","ExchangePlans","DocumentJournals" - "CommonModules","CommonCommands","CommonForms","CommonPictures","CommonTemplates" - "CommonAttributes","CommandGroups","Roles","SessionParameters","FilterCriteria" - "XDTOPackages","WebServices","HTTPServices","WSReferences","EventSubscriptions" - "ScheduledJobs","SettingsStorages","FunctionalOptions","FunctionalOptionsParameters" - "DefinedTypes","DocumentNumerators","Sequences","Subsystems","StyleItems","IntegrationServices" -) - -# --- 1. XML well-formedness + root structure --- -$xmlDoc = $null -try { - [xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8 -} catch { - Report-Error "1. XML parse error: $($_.Exception.Message)" - $script:stopped = $true -} - -if (-not $script:stopped) { - $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) - $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") - - $root = $xmlDoc.DocumentElement - $sub = $xmlDoc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns) - $version = $root.GetAttribute("version") - - if (-not $sub) { - Report-Error "1. Root structure: expected MetaDataObject/Subsystem, not found" - $script:stopped = $true - } else { - $uuid = $sub.GetAttribute("uuid") - if ($uuid -and $uuid -match $guidPattern) { - Report-OK "1. Root structure: MetaDataObject/Subsystem, uuid=$uuid, version $version" - } else { - Report-Error "1. Root structure: invalid or missing uuid" - } - } -} - -# --- Properties checks --- -if (-not $script:stopped) { - $props = $sub.SelectSingleNode("md:Properties", $ns) - if (-not $props) { - Report-Error "2. Properties: <Properties> element not found" - $script:stopped = $true - } -} - -$subName = "" -if (-not $script:stopped) { - # --- 2. Required properties --- - $requiredProps = @("Name","Synonym","Comment","IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand","Explanation","Picture","Content") - $missing = @() - foreach ($p in $requiredProps) { - $el = $props.SelectSingleNode("md:$p", $ns) - if (-not $el) { $missing += $p } - } - if ($missing.Count -eq 0) { - Report-OK "2. Properties: all 9 required properties present" - } else { - Report-Error "2. Properties: missing: $($missing -join ', ')" - } - - # --- 3. Name --- - $nameEl = $props.SelectSingleNode("md:Name", $ns) - $subName = if ($nameEl) { $nameEl.InnerText.Trim() } else { "" } - # Re-insert header at position 0 - $headerLine = "=== Validation: Subsystem.$subName ===" - $script:output.Insert(0, "$headerLine`r`n`r`n") | Out-Null - - if ($subName -and $subName -match $identPattern) { - Report-OK "3. Name: `"$subName`" - valid identifier" - } elseif (-not $subName) { - Report-Error "3. Name: empty" - } else { - Report-Error "3. Name: `"$subName`" - invalid identifier" - } - - # --- 4. Synonym --- - $synEl = $props.SelectSingleNode("md:Synonym", $ns) - if ($synEl -and $synEl.HasChildNodes) { - $items = $synEl.SelectNodes("v8:item", $ns) - if ($items.Count -gt 0) { - $firstContent = "" - foreach ($item in $items) { - $c = $item.SelectSingleNode("v8:content", $ns) - if ($c -and $c.InnerText) { $firstContent = $c.InnerText; break } - } - Report-OK "4. Synonym: `"$firstContent`" ($($items.Count) lang(s))" - } else { - Report-Warn "4. Synonym: element exists but no v8:item children" - } - } else { - Report-Warn "4. Synonym: empty or missing" - } - - # --- 5. Boolean properties --- - $boolProps = @("IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand") - $boolOk = $true - $boolVals = @{} - foreach ($bp in $boolProps) { - $el = $props.SelectSingleNode("md:$bp", $ns) - if ($el) { - $val = $el.InnerText.Trim() - $boolVals[$bp] = $val - if ($val -ne "true" -and $val -ne "false") { - Report-Error "5. Boolean property $bp = `"$val`" (expected true/false)" - $boolOk = $false - } - } - } - if ($boolOk) { Report-OK "5. Boolean properties: valid" } - - # --- 6. Content items format --- - $contentEl = $props.SelectSingleNode("md:Content", $ns) - $contentItems = @() - if ($contentEl -and $contentEl.HasChildNodes) { - $xrItems = $contentEl.SelectNodes("xr:Item", $ns) - $contentOk = $true - foreach ($item in $xrItems) { - $typeAttr = $item.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") - $text = $item.InnerText.Trim() - $contentItems += $text - if ($typeAttr -ne "xr:MDObjectRef") { - Report-Error "6. Content item `"$text`": xsi:type=`"$typeAttr`" (expected xr:MDObjectRef)" - $contentOk = $false - } - if ($text -notmatch '^[A-Za-z]+\..+$' -and $text -notmatch $guidPattern) { - Report-Error "6. Content item `"$text`": invalid format (expected Type.Name or UUID)" - $contentOk = $false - } - if ($text -match '^([A-Za-z]+)\.') { - $typePart = $Matches[1] - if ($typePart -in $knownPluralTypes) { - Report-Error "6. Content item `"$text`": uses plural form `"$typePart`" (platform requires singular, e.g. Catalog not Catalogs)" - $contentOk = $false - } - } - } - if ($contentOk) { Report-OK "6. Content: $($xrItems.Count) items, all valid MDObjectRef format" } - } else { - Report-OK "6. Content: empty (no items)" - } - - # --- 7. Content duplicates --- - if ($contentItems.Count -gt 0) { - $dupes = $contentItems | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Warn "7. Content: duplicates found: $dupeNames" - } else { - Report-OK "7. Content: no duplicates" - } - } else { - Report-OK "7. Content: no duplicates (empty)" - } - - # --- 8. ChildObjects entries non-empty --- - $childObjs = $sub.SelectSingleNode("md:ChildObjects", $ns) - $childNames = @() - if ($childObjs -and $childObjs.HasChildNodes) { - $childOk = $true - foreach ($child in $childObjs.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - if ($child.LocalName -ne "Subsystem") { - Report-Error "8. ChildObjects: unexpected element <$($child.LocalName)>" - $childOk = $false - } elseif (-not $child.InnerText.Trim()) { - Report-Error "8. ChildObjects: empty <Subsystem> element" - $childOk = $false - } else { - $childNames += $child.InnerText.Trim() - } - } - if ($childOk) { Report-OK "8. ChildObjects: $($childNames.Count) entries, all non-empty" } - } else { - Report-OK "8. ChildObjects: empty (leaf subsystem)" - } - - # --- 9. ChildObjects duplicates --- - if ($childNames.Count -gt 0) { - $dupes = $childNames | Group-Object | Where-Object { $_.Count -gt 1 } - if ($dupes) { - $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " - Report-Error "9. ChildObjects: duplicates: $dupeNames" - } else { - Report-OK "9. ChildObjects: no duplicates" - } - } else { - Report-OK "9. ChildObjects: no duplicates (empty)" - } - - # --- 10. ChildObjects files exist --- - if ($childNames.Count -gt 0) { - $parentDir = [System.IO.Path]::GetDirectoryName($resolvedPath) - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) - $subsDir = Join-Path (Join-Path $parentDir $baseName) "Subsystems" - $missingFiles = @() - foreach ($cn in $childNames) { - $childXml = Join-Path $subsDir "$cn.xml" - if (-not (Test-Path $childXml)) { $missingFiles += $cn } - } - if ($missingFiles.Count -eq 0) { - Report-OK "10. ChildObjects files: all $($childNames.Count) files exist" - } else { - Report-Warn "10. ChildObjects files: missing: $($missingFiles -join ', ')" - } - } - - # --- 11. CommandInterface.xml --- - $parentDir2 = [System.IO.Path]::GetDirectoryName($resolvedPath) - $baseName2 = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) - $ciPath = Join-Path (Join-Path (Join-Path $parentDir2 $baseName2) "Ext") "CommandInterface.xml" - if (Test-Path $ciPath) { - try { - [xml]$ciDoc = Get-Content -Path $ciPath -Encoding UTF8 - Report-OK "11. CommandInterface: exists, well-formed" - } catch { - Report-Warn "11. CommandInterface: exists but NOT well-formed: $($_.Exception.Message)" - } - } else { - Report-OK "11. CommandInterface: not present" - } - - # --- 12. Picture format --- - $picEl = $props.SelectSingleNode("md:Picture", $ns) - if ($picEl -and $picEl.HasChildNodes) { - $picRef = $picEl.SelectSingleNode("xr:Ref", $ns) - if ($picRef -and $picRef.InnerText) { - $refText = $picRef.InnerText - if ($refText -match '^CommonPicture\.') { - Report-OK "12. Picture: $refText" - } else { - Report-Warn "12. Picture: `"$refText`" (expected CommonPicture.XXX)" - } - } else { - Report-Warn "12. Picture: has children but no xr:Ref content" - } - } else { - Report-OK "12. Picture: empty (not set)" - } - - # --- 13. UseOneCommand constraint --- - $useOne = $boolVals["UseOneCommand"] - if ($useOne -eq "true") { - if ($contentItems.Count -eq 1) { - Report-OK "13. UseOneCommand: true, Content has exactly 1 item" - } else { - Report-Warn "13. UseOneCommand: true but Content has $($contentItems.Count) items (expected 1)" - } - } else { - Report-OK "13. UseOneCommand: false (no constraint)" - } -} - -# --- Finalize --- -$checks = $script:okCount + $script:errors + $script:warnings - -if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { - $result = "=== Validation OK: Subsystem.$subName ($checks checks) ===" -} else { - Out-Line "" - Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" - $result = $script:output.ToString() -} - -Write-Host $result - -if ($OutFile) { - if (-not [System.IO.Path]::IsPathRooted($OutFile)) { - $OutFile = Join-Path (Get-Location).Path $OutFile - } - $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) - Write-Host "Written to: $OutFile" -} - -if ($script:errors -gt 0) { exit 1 } else { exit 0 } +# subsystem-validate v1.2 — Validate 1C subsystem XML structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, + [switch]$Detailed, + [int]$MaxErrors = 30, + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) { + $SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath +} +if (Test-Path $SubsystemPath -PathType Container) { + $dirName = Split-Path $SubsystemPath -Leaf + $candidate = Join-Path $SubsystemPath "$dirName.xml" + $sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml" + if (Test-Path $candidate) { $SubsystemPath = $candidate } + elseif (Test-Path $sibling) { $SubsystemPath = $sibling } + else { + Write-Host "[ERROR] No $dirName.xml found in directory: $SubsystemPath" + exit 1 + } +} +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $SubsystemPath)) { + $fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath) + $pd = Split-Path $SubsystemPath + if ($fn -eq (Split-Path $pd -Leaf)) { + $c = Join-Path (Split-Path $pd) "$fn.xml" + if (Test-Path $c) { $SubsystemPath = $c } + } +} +if (-not (Test-Path $SubsystemPath)) { + Write-Host "[ERROR] File not found: $SubsystemPath" + exit 1 +} +$resolvedPath = (Resolve-Path $SubsystemPath).Path + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:okCount = 0 +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null } +function Report-OK([string]$msg) { + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} +function Report-Error([string]$msg) { + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { $script:stopped = $true } +} +function Report-Warn([string]$msg) { + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# Known plural forms that are NOT valid in subsystem Content (platform expects singular) +$knownPluralTypes = @( + "Catalogs","Documents","Enums","Constants","Reports","DataProcessors" + "InformationRegisters","AccumulationRegisters","AccountingRegisters","CalculationRegisters" + "ChartsOfAccounts","ChartsOfCharacteristicTypes","ChartsOfCalculationTypes" + "BusinessProcesses","Tasks","ExchangePlans","DocumentJournals" + "CommonModules","CommonCommands","CommonForms","CommonPictures","CommonTemplates" + "CommonAttributes","CommandGroups","Roles","SessionParameters","FilterCriteria" + "XDTOPackages","WebServices","HTTPServices","WSReferences","EventSubscriptions" + "ScheduledJobs","SettingsStorages","FunctionalOptions","FunctionalOptionsParameters" + "DefinedTypes","DocumentNumerators","Sequences","Subsystems","StyleItems","IntegrationServices" +) + +# --- 1. XML well-formedness + root structure --- +$xmlDoc = $null +try { + [xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8 +} catch { + Report-Error "1. XML parse error: $($_.Exception.Message)" + $script:stopped = $true +} + +if (-not $script:stopped) { + $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") + + $root = $xmlDoc.DocumentElement + $sub = $xmlDoc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns) + $version = $root.GetAttribute("version") + + if (-not $sub) { + Report-Error "1. Root structure: expected MetaDataObject/Subsystem, not found" + $script:stopped = $true + } else { + $uuid = $sub.GetAttribute("uuid") + if ($uuid -and $uuid -match $guidPattern) { + Report-OK "1. Root structure: MetaDataObject/Subsystem, uuid=$uuid, version $version" + } else { + Report-Error "1. Root structure: invalid or missing uuid" + } + } +} + +# --- Properties checks --- +if (-not $script:stopped) { + $props = $sub.SelectSingleNode("md:Properties", $ns) + if (-not $props) { + Report-Error "2. Properties: <Properties> element not found" + $script:stopped = $true + } +} + +$subName = "" +if (-not $script:stopped) { + # --- 2. Required properties --- + $requiredProps = @("Name","Synonym","Comment","IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand","Explanation","Picture","Content") + $missing = @() + foreach ($p in $requiredProps) { + $el = $props.SelectSingleNode("md:$p", $ns) + if (-not $el) { $missing += $p } + } + if ($missing.Count -eq 0) { + Report-OK "2. Properties: all 9 required properties present" + } else { + Report-Error "2. Properties: missing: $($missing -join ', ')" + } + + # --- 3. Name --- + $nameEl = $props.SelectSingleNode("md:Name", $ns) + $subName = if ($nameEl) { $nameEl.InnerText.Trim() } else { "" } + # Re-insert header at position 0 + $headerLine = "=== Validation: Subsystem.$subName ===" + $script:output.Insert(0, "$headerLine`r`n`r`n") | Out-Null + + if ($subName -and $subName -match $identPattern) { + Report-OK "3. Name: `"$subName`" - valid identifier" + } elseif (-not $subName) { + Report-Error "3. Name: empty" + } else { + Report-Error "3. Name: `"$subName`" - invalid identifier" + } + + # --- 4. Synonym --- + $synEl = $props.SelectSingleNode("md:Synonym", $ns) + if ($synEl -and $synEl.HasChildNodes) { + $items = $synEl.SelectNodes("v8:item", $ns) + if ($items.Count -gt 0) { + $firstContent = "" + foreach ($item in $items) { + $c = $item.SelectSingleNode("v8:content", $ns) + if ($c -and $c.InnerText) { $firstContent = $c.InnerText; break } + } + Report-OK "4. Synonym: `"$firstContent`" ($($items.Count) lang(s))" + } else { + Report-Warn "4. Synonym: element exists but no v8:item children" + } + } else { + Report-Warn "4. Synonym: empty or missing" + } + + # --- 5. Boolean properties --- + $boolProps = @("IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand") + $boolOk = $true + $boolVals = @{} + foreach ($bp in $boolProps) { + $el = $props.SelectSingleNode("md:$bp", $ns) + if ($el) { + $val = $el.InnerText.Trim() + $boolVals[$bp] = $val + if ($val -ne "true" -and $val -ne "false") { + Report-Error "5. Boolean property $bp = `"$val`" (expected true/false)" + $boolOk = $false + } + } + } + if ($boolOk) { Report-OK "5. Boolean properties: valid" } + + # --- 6. Content items format --- + $contentEl = $props.SelectSingleNode("md:Content", $ns) + $contentItems = @() + if ($contentEl -and $contentEl.HasChildNodes) { + $xrItems = $contentEl.SelectNodes("xr:Item", $ns) + $contentOk = $true + foreach ($item in $xrItems) { + $typeAttr = $item.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $text = $item.InnerText.Trim() + $contentItems += $text + if ($typeAttr -ne "xr:MDObjectRef") { + Report-Error "6. Content item `"$text`": xsi:type=`"$typeAttr`" (expected xr:MDObjectRef)" + $contentOk = $false + } + if ($text -notmatch '^[A-Za-z]+\..+$' -and $text -notmatch $guidPattern) { + Report-Error "6. Content item `"$text`": invalid format (expected Type.Name or UUID)" + $contentOk = $false + } + if ($text -match '^([A-Za-z]+)\.') { + $typePart = $Matches[1] + if ($typePart -in $knownPluralTypes) { + Report-Error "6. Content item `"$text`": uses plural form `"$typePart`" (platform requires singular, e.g. Catalog not Catalogs)" + $contentOk = $false + } + } + } + if ($contentOk) { Report-OK "6. Content: $($xrItems.Count) items, all valid MDObjectRef format" } + } else { + Report-OK "6. Content: empty (no items)" + } + + # --- 7. Content duplicates --- + if ($contentItems.Count -gt 0) { + $dupes = $contentItems | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Warn "7. Content: duplicates found: $dupeNames" + } else { + Report-OK "7. Content: no duplicates" + } + } else { + Report-OK "7. Content: no duplicates (empty)" + } + + # --- 8. ChildObjects entries non-empty --- + $childObjs = $sub.SelectSingleNode("md:ChildObjects", $ns) + $childNames = @() + if ($childObjs -and $childObjs.HasChildNodes) { + $childOk = $true + foreach ($child in $childObjs.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -ne "Subsystem") { + Report-Error "8. ChildObjects: unexpected element <$($child.LocalName)>" + $childOk = $false + } elseif (-not $child.InnerText.Trim()) { + Report-Error "8. ChildObjects: empty <Subsystem> element" + $childOk = $false + } else { + $childNames += $child.InnerText.Trim() + } + } + if ($childOk) { Report-OK "8. ChildObjects: $($childNames.Count) entries, all non-empty" } + } else { + Report-OK "8. ChildObjects: empty (leaf subsystem)" + } + + # --- 9. ChildObjects duplicates --- + if ($childNames.Count -gt 0) { + $dupes = $childNames | Group-Object | Where-Object { $_.Count -gt 1 } + if ($dupes) { + $dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", " + Report-Error "9. ChildObjects: duplicates: $dupeNames" + } else { + Report-OK "9. ChildObjects: no duplicates" + } + } else { + Report-OK "9. ChildObjects: no duplicates (empty)" + } + + # --- 10. ChildObjects files exist --- + if ($childNames.Count -gt 0) { + $parentDir = [System.IO.Path]::GetDirectoryName($resolvedPath) + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) + $subsDir = Join-Path (Join-Path $parentDir $baseName) "Subsystems" + $missingFiles = @() + foreach ($cn in $childNames) { + $childXml = Join-Path $subsDir "$cn.xml" + if (-not (Test-Path $childXml)) { $missingFiles += $cn } + } + if ($missingFiles.Count -eq 0) { + Report-OK "10. ChildObjects files: all $($childNames.Count) files exist" + } else { + Report-Warn "10. ChildObjects files: missing: $($missingFiles -join ', ')" + } + } + + # --- 11. CommandInterface.xml --- + $parentDir2 = [System.IO.Path]::GetDirectoryName($resolvedPath) + $baseName2 = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) + $ciPath = Join-Path (Join-Path (Join-Path $parentDir2 $baseName2) "Ext") "CommandInterface.xml" + if (Test-Path $ciPath) { + try { + [xml]$ciDoc = Get-Content -Path $ciPath -Encoding UTF8 + Report-OK "11. CommandInterface: exists, well-formed" + } catch { + Report-Warn "11. CommandInterface: exists but NOT well-formed: $($_.Exception.Message)" + } + } else { + Report-OK "11. CommandInterface: not present" + } + + # --- 12. Picture format --- + $picEl = $props.SelectSingleNode("md:Picture", $ns) + if ($picEl -and $picEl.HasChildNodes) { + $picRef = $picEl.SelectSingleNode("xr:Ref", $ns) + if ($picRef -and $picRef.InnerText) { + $refText = $picRef.InnerText + if ($refText -match '^CommonPicture\.') { + Report-OK "12. Picture: $refText" + } else { + Report-Warn "12. Picture: `"$refText`" (expected CommonPicture.XXX)" + } + } else { + Report-Warn "12. Picture: has children but no xr:Ref content" + } + } else { + Report-OK "12. Picture: empty (not set)" + } + + # --- 13. UseOneCommand constraint --- + $useOne = $boolVals["UseOneCommand"] + if ($useOne -eq "true") { + if ($contentItems.Count -eq 1) { + Report-OK "13. UseOneCommand: true, Content has exactly 1 item" + } else { + Report-Warn "13. UseOneCommand: true but Content has $($contentItems.Count) items (expected 1)" + } + } else { + Report-OK "13. UseOneCommand: false (no constraint)" + } +} + +# --- Finalize --- +$checks = $script:okCount + $script:errors + $script:warnings + +if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Subsystem.$subName ($checks checks) ===" +} else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() +} + +Write-Host $result + +if ($OutFile) { + if (-not [System.IO.Path]::IsPathRooted($OutFile)) { + $OutFile = Join-Path (Get-Location).Path $OutFile + } + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" +} + +if ($script:errors -gt 0) { exit 1 } else { exit 0 } diff --git a/.claude/skills/template-add/SKILL.md b/.claude/skills/template-add/SKILL.md index 724506cd..a45179d6 100644 --- a/.claude/skills/template-add/SKILL.md +++ b/.claude/skills/template-add/SKILL.md @@ -1,89 +1,89 @@ ---- -name: template-add -description: Добавить пустой макет к объекту 1С. Используй когда нужно создать у объекта новый макет -argument-hint: <ObjectName> <TemplateName> <TemplateType> -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /template-add — Добавление макета - -Создаёт макет указанного типа и регистрирует его в корневом XML объекта. - -## Usage - -``` -/template-add <ObjectName> <TemplateName> <TemplateType> -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|-----------------|--------------------------------------------------| -| ObjectName | да | — | Имя объекта | -| TemplateName | да | — | Имя макета | -| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData, DataCompositionSchema | -| Synonym | нет | = TemplateName | Синоним макета | -| SrcDir | нет | `src` | Путь к папке типа объектов (`Reports`, `DataProcessors`, `Catalogs`, `Documents`...), внутри которой лежит `<ObjectName>.xml`. Дефолт `src` подходит для каталогов с внешними обработками/отчётами, лежащими рядом | -| -SetMainSKD | нет | — | Принудительно установить MainDataCompositionSchema | - -## Команда - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" -TemplateType "<TemplateType>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-SetMainSKD] -``` - -## Пример - -Добавить основную СКД к отчёту в расширении: - -```powershell -powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "ОтчётПродажи" -TemplateName "ОсновнаяСхемаКомпоновкиДанных" -TemplateType "DataCompositionSchema" -SrcDir "src/cfe/МоёРасширение/Reports" -``` - -## Маппинг типов - -Пользователь может указать тип в свободной форме. Определи нужный по контексту: - -| Пользователь пишет | TemplateType | Расширение | Содержимое | -|---------------------------------------------|---------------------|------------|-------------------------| -| HTML | HTMLDocument | `.html` | Пустой HTML-документ | -| Text, текстовый документ, текст | TextDocument | `.txt` | Пустой файл | -| SpreadsheetDocument, табличный документ, MXL | SpreadsheetDocument | `.xml` | Минимальный spreadsheet | -| BinaryData, двоичные данные | BinaryData | `.bin` | Пустой файл | -| DataCompositionSchema, СКД, схема компоновки | DataCompositionSchema | `.xml` | Минимальная DCS-схема | - -## Конвенция именования - -Для макетов **печатных форм** (тип SpreadsheetDocument) применяй префикс `ПФ_MXL_`: - -| Контекст | Формат имени | Пример | -|------------------------------------------------------------------|----------------------------|-------------------------| -| Печатная форма (дополнительная обработка вида ПечатнаяФорма, или пользователь явно говорит «печатная форма») | `ПФ_MXL_<КраткоеИмя>` | `ПФ_MXL_М11`, `ПФ_MXL_СчётФактура`, `ПФ_MXL_КонвертDL` | -| Прочие макеты (загрузка данных, служебные, настройки) | Без префикса | `МакетЗагрузки`, `НастройкиПечати` | - -Если пользователь указал имя макета без префикса, но контекст — печатная форма, **добавь префикс `ПФ_MXL_` автоматически** и сообщи об этом. - -## MainDataCompositionSchema (авто) - -При добавлении макета типа `DataCompositionSchema` к `ExternalReport` или `Report`: -- Если `MainDataCompositionSchema` пуст — автоматически заполняется ссылкой на макет -- Используй `--SetMainSKD` чтобы перезаписать существующее значение - -## Что создаётся - -``` -<SrcDir>/<ObjectName>/Templates/ -├── <TemplateName>.xml # Метаданные макета (1 UUID) -└── <TemplateName>/ - └── Ext/ - └── Template.<ext> # Содержимое макета -``` - -## Что модифицируется - -- `<SrcDir>/<ObjectName>.xml` — добавляется `<Template>` в конец `ChildObjects` -- Для ExternalReport/Report: может обновляться `MainDataCompositionSchema` +--- +name: template-add +description: Добавить пустой макет к объекту 1С. Используй когда нужно создать у объекта новый макет +argument-hint: <ObjectName> <TemplateName> <TemplateType> +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /template-add — Добавление макета + +Создаёт макет указанного типа и регистрирует его в корневом XML объекта. + +## Usage + +``` +/template-add <ObjectName> <TemplateName> <TemplateType> +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|-----------------|--------------------------------------------------| +| ObjectName | да | — | Имя объекта | +| TemplateName | да | — | Имя макета | +| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData, DataCompositionSchema | +| Synonym | нет | = TemplateName | Синоним макета | +| SrcDir | нет | `src` | Путь к папке типа объектов (`Reports`, `DataProcessors`, `Catalogs`, `Documents`...), внутри которой лежит `<ObjectName>.xml`. Дефолт `src` подходит для каталогов с внешними обработками/отчётами, лежащими рядом | +| -SetMainSKD | нет | — | Принудительно установить MainDataCompositionSchema | + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" -TemplateType "<TemplateType>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-SetMainSKD] +``` + +## Пример + +Добавить основную СКД к отчёту в расширении: + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "ОтчётПродажи" -TemplateName "ОсновнаяСхемаКомпоновкиДанных" -TemplateType "DataCompositionSchema" -SrcDir "src/cfe/МоёРасширение/Reports" +``` + +## Маппинг типов + +Пользователь может указать тип в свободной форме. Определи нужный по контексту: + +| Пользователь пишет | TemplateType | Расширение | Содержимое | +|---------------------------------------------|---------------------|------------|-------------------------| +| HTML | HTMLDocument | `.html` | Пустой HTML-документ | +| Text, текстовый документ, текст | TextDocument | `.txt` | Пустой файл | +| SpreadsheetDocument, табличный документ, MXL | SpreadsheetDocument | `.xml` | Минимальный spreadsheet | +| BinaryData, двоичные данные | BinaryData | `.bin` | Пустой файл | +| DataCompositionSchema, СКД, схема компоновки | DataCompositionSchema | `.xml` | Минимальная DCS-схема | + +## Конвенция именования + +Для макетов **печатных форм** (тип SpreadsheetDocument) применяй префикс `ПФ_MXL_`: + +| Контекст | Формат имени | Пример | +|------------------------------------------------------------------|----------------------------|-------------------------| +| Печатная форма (дополнительная обработка вида ПечатнаяФорма, или пользователь явно говорит «печатная форма») | `ПФ_MXL_<КраткоеИмя>` | `ПФ_MXL_М11`, `ПФ_MXL_СчётФактура`, `ПФ_MXL_КонвертDL` | +| Прочие макеты (загрузка данных, служебные, настройки) | Без префикса | `МакетЗагрузки`, `НастройкиПечати` | + +Если пользователь указал имя макета без префикса, но контекст — печатная форма, **добавь префикс `ПФ_MXL_` автоматически** и сообщи об этом. + +## MainDataCompositionSchema (авто) + +При добавлении макета типа `DataCompositionSchema` к `ExternalReport` или `Report`: +- Если `MainDataCompositionSchema` пуст — автоматически заполняется ссылкой на макет +- Используй `--SetMainSKD` чтобы перезаписать существующее значение + +## Что создаётся + +``` +<SrcDir>/<ObjectName>/Templates/ +├── <TemplateName>.xml # Метаданные макета (1 UUID) +└── <TemplateName>/ + └── Ext/ + └── Template.<ext> # Содержимое макета +``` + +## Что модифицируется + +- `<SrcDir>/<ObjectName>.xml` — добавляется `<Template>` в конец `ChildObjects` +- Для ExternalReport/Report: может обновляться `MainDataCompositionSchema` diff --git a/.claude/skills/template-add/scripts/add-template.ps1 b/.claude/skills/template-add/scripts/add-template.ps1 index 71b592db..e4b3cf6c 100644 --- a/.claude/skills/template-add/scripts/add-template.ps1 +++ b/.claude/skills/template-add/scripts/add-template.ps1 @@ -1,386 +1,386 @@ -# template-add v1.7 — Add template to 1C object -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [Parameter(Mandatory)] - [Alias("ProcessorName")] - [string]$ObjectName, - - [Parameter(Mandatory)] - [string]$TemplateName, - - [Parameter(Mandatory)] - [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")] - [string]$TemplateType, - - [string]$Synonym = $TemplateName, - - [string]$SrcDir = "src", - - [switch]$SetMainSKD -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::InputEncoding = [System.Text.Encoding]::UTF8 - -# --- Support guard (Ext/ParentConfigurations.bin) --- -# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / -# read-only configs unless allowed. Trigger = bin present; reaction from -# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never -# throws — guard errors degrade to allow. -function Get-RootUuid([string]$xmlPath) { - if (-not (Test-Path $xmlPath)) { return $null } - try { - [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 - $el = $mx.DocumentElement.FirstChild - while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } - if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } - } catch {} - return $null -} -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Get-EditMode([string]$cfgDir) { - try { - $pj = Find-V8Project (Get-Location).Path - if (-not $pj) { $pj = Find-V8Project $cfgDir } - if (-not $pj) { return 'deny' } - $proj = Get-Content -Raw $pj | ConvertFrom-Json - $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') - if ($proj.databases) { - foreach ($db in $proj.databases) { - if ($db.configSrc) { - $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') - if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { - if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } - } - } - } - } - if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } - return 'deny' - } catch { return 'deny' } -} -function Assert-EditAllowed([string]$targetPath, [string]$require) { - try { - $rp = $targetPath - try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} - $elemUuid = Get-RootUuid $rp - $cfgDir = $null; $binPath = $null - $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } - for ($i = 0; $i -lt 12 -and $d; $i++) { - if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } - if (-not $cfgDir) { - $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" - if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } - } - if ($elemUuid -and $cfgDir) { break } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - # New object (no element file): fall back to config root uuid. - if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } - if (-not $binPath -or -not (Test-Path $binPath)) { return } - $bytes = [System.IO.File]::ReadAllBytes($binPath) - if ($bytes.Length -le 32) { return } - $start = 0 - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } - $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) - $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') - if (-not $hm.Success) { return } - $G = [int]$hm.Groups[1].Value - $K = [int]$hm.Groups[2].Value - if ($K -eq 0) { return } - $best = $null - if ($elemUuid) { - $u = [regex]::Escape($elemUuid.ToLower()) - foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { - $f1 = [int]$m.Groups[1].Value - if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } - } - } - $blocked = $false; $code = ""; $reason = "" - if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } - elseif ($require -eq 'removed') { - if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } - } - else { - if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } - } - if (-not $blocked) { return } - $mode = Get-EditMode $cfgDir - if ($mode -eq 'off') { return } - # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the - # latter throws and would be swallowed by this function's own catch. - if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } - $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." - $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." - $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." - if ($code -eq "capability-off") { - $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." - $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." - } elseif ($code -eq "not-removed") { - $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." - $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." - } else { - $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." - $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." - } - [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") - exit 1 - } catch { return } -} - -# --- Маппинг типов --- - -$typeMap = @{ - "HTML" = @{ TemplateType = "HTMLDocument"; Ext = ".html" } - "Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" } - "SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" } - "BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" } - "DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" } -} - -$tmpl = $typeMap[$TemplateType] - -# --- Проверки --- - -$objectTypeFolders = @( - "Reports", "DataProcessors", "Documents", "Catalogs", - "InformationRegisters", "AccumulationRegisters", - "ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes", - "BusinessProcesses", "Tasks", "ExchangePlans" -) - -$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml" -if (-not (Test-Path $rootXmlPath)) { - $candidates = @() - foreach ($folder in $objectTypeFolders) { - $probe = Join-Path (Join-Path $SrcDir $folder) "$ObjectName.xml" - if (Test-Path $probe) { $candidates += (Join-Path $SrcDir $folder) } - } - if ($candidates.Count -eq 1) { - $SrcDir = $candidates[0] - $rootXmlPath = Join-Path $SrcDir "$ObjectName.xml" - Write-Host "[INFO] SrcDir расширен до: $SrcDir" - } elseif ($candidates.Count -gt 1) { - Write-Error "Объект '$ObjectName' найден в нескольких подпапках: $($candidates -join ', ')`nУкажи SrcDir явно" - exit 1 - } else { - Write-Error "Корневой файл объекта не найден: $rootXmlPath`nОжидается: <SrcDir>/<ObjectName>.xml`nПодсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации" - exit 1 - } -} - -$processorDir = Join-Path $SrcDir $ObjectName -$templatesDir = Join-Path $processorDir "Templates" -$templateMetaPath = Join-Path $templatesDir "$TemplateName.xml" - -if (Test-Path $templateMetaPath) { - Write-Error "Макет уже существует: $templateMetaPath" - exit 1 -} - -Assert-EditAllowed $rootXmlPath 'editable' - -# --- Создание каталогов --- - -$templateExtDir = Join-Path (Join-Path $templatesDir $TemplateName) "Ext" -New-Item -ItemType Directory -Path $templateExtDir -Force | Out-Null - -# --- Кодировка --- - -$encBom = New-Object System.Text.UTF8Encoding($true) - -# --- Detect format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path - -# --- 1. Метаданные макета (Templates/<TemplateName>.xml) --- - -$templateUuid = [guid]::NewGuid().ToString() - -$templateMetaXml = @" -<?xml version="1.0" encoding="UTF-8"?> -<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version=`"$formatVersion`"> - <Template uuid="$templateUuid"> - <Properties> - <Name>$TemplateName</Name> - <Synonym> - <v8:item> - <v8:lang>ru</v8:lang> - <v8:content>$Synonym</v8:content> - </v8:item> - </Synonym> - <Comment/> - <TemplateType>$($tmpl.TemplateType)</TemplateType> - </Properties> - </Template> -</MetaDataObject> -"@ - -[System.IO.File]::WriteAllText($templateMetaPath, $templateMetaXml, $encBom) - -# --- 2. Содержимое макета (Templates/<TemplateName>/Ext/Template.<ext>) --- - -$templateFilePath = Join-Path $templateExtDir "Template$($tmpl.Ext)" - -switch ($TemplateType) { - "HTML" { - $content = @" -<!DOCTYPE html> -<html> -<head> - <meta charset="UTF-8"> - <title> - - - - -"@ - [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) - } - "Text" { - [System.IO.File]::WriteAllText($templateFilePath, "", $encBom) - } - "SpreadsheetDocument" { - $content = @" - - - -"@ - [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) - } - "BinaryData" { - [System.IO.File]::WriteAllBytes($templateFilePath, @()) - } - "DataCompositionSchema" { - $content = @" - - - - ИсточникДанных1 - Local - - -"@ - [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) - } -} - -# --- 3. Модификация корневого XML --- - -$rootXmlFull = Resolve-Path $rootXmlPath -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $true -$xmlDoc.Load($rootXmlFull.Path) - -$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - -$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) -if (-not $childObjects) { - Write-Error "Не найден элемент ChildObjects в $rootXmlPath" - exit 1 -} - -# Добавить