diff --git a/.claude/skills/form-add/SKILL.md b/.claude/skills/form-add/SKILL.md new file mode 100644 index 00000000..0de9be7f --- /dev/null +++ b/.claude/skills/form-add/SKILL.md @@ -0,0 +1,110 @@ +--- +name: form-add +description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С (Form.xml) +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /form-add — Добавление в форму + +Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий. + +## Использование + +``` +/form-add +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|-----------|:------------:|----------------------------------| +| FormPath | да | Путь к существующему Form.xml | +| JsonPath | да | Путь к JSON с описанием добавлений | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\form-add\scripts\form-add.ps1 -FormPath "<путь>" -JsonPath "<путь>" +``` + +## JSON формат + +```json +{ + "into": "ГруппаШапка", + "after": "Контрагент", + "elements": [ + { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } + ], + "attributes": [ + { "name": "СуммаИтого", "type": "decimal(15,2)" } + ], + "commands": [ + { "name": "Рассчитать", "action": "РассчитатьОбработка" } + ] +} +``` + +### Позиционирование элементов + +| Ключ | По умолчанию | Описание | +|------|-------------|----------| +| `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` для вложенных элементов. + +### Система типов (для attributes) + +`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). + +## Вывод + +``` +=== form-add: Форма === + +Added elements (into ГруппаШапка, after Контрагент): + + [Input] Склад -> Объект.Склад {OnChange} + +Added attributes: + + СуммаИтого: decimal(15,2) (id=12) + +--- +Total: 1 element(s) (+2 companions), 1 attribute(s) +Run /form-validate to verify. +``` + +## Когда использовать + +- **После `/form-compile`**: добавить элементы, которые не были в исходном JSON +- **Модификация существующих форм**: добавить поле, реквизит или команду в форму из конфигурации +- **Пакетное добавление**: один JSON может содержать элементы + реквизиты + команды + +## Workflow + +1. `/form-info` — посмотреть текущую структуру формы +2. Создать JSON с описанием добавлений +3. `/form-add` — добавить в форму +4. `/form-validate` — проверить корректность +5. `/form-info` — убедиться что добавилось правильно diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 new file mode 100644 index 00000000..7f73b800 --- /dev/null +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -0,0 +1,958 @@ +param( + [Parameter(Mandatory)] + [string]$FormPath, + + [Parameter(Mandatory)] + [string]$JsonPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# === 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 +$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-add: $formName ===" +Write-Host "" + +# === 4. Scan max IDs per pool === + +$script:nextElemId = 0 +$script:nextAttrId = 0 +$script:nextCmdId = 0 + +function Scan-ElementIds($node) { + foreach ($child in $node.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $id = $child.GetAttribute("id") + if ($id -and $id -ne "" -and $id -ne "-1") { + try { + $intId = [int]$id + if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } + } catch {} + } + $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ci) { Scan-ElementIds $ci } + } +} + +$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($rootCI) { Scan-ElementIds $rootCI } + +# Also scan AutoCommandBar children +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $acbCI = $acb.SelectSingleNode("f:ChildItems", $nsMgr) + if ($acbCI) { Scan-ElementIds $acbCI } +} + +# Scan attribute IDs +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 {} + } +} + +# 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++ + +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 --- + +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) + + 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)" +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent) + if (-not $el.on) { return } + X "$indent" + foreach ($evt in $el.on) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + 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.hidden -eq $true) { X "$indentfalse" } + if ($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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 } + + $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] Element '$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 + + # 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" } + + # 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" } + + # 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 } + if ($cmd.action) { 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)" } 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 + } +} + +# === 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 ($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 ($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/README.md b/README.md index 65c70c55..2cb9d208 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | -| Управляемые формы (Form) | 3 навыка `/form-*` | Анализ, генерация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | +| Управляемые формы (Form) | 4 навыка `/form-*` | Анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -60,6 +60,7 @@ ├── form-info/ # Анализ структуры управляемой формы ├── form-compile/ # Компиляция формы из JSON ├── form-validate/ # Валидация формы +├── form-add/ # Добавление элементов в форму └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки diff --git a/docs/form-guide.md b/docs/form-guide.md index f0d8087c..46d02958 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -9,6 +9,7 @@ | `/form-info` | `` | Компактная сводка: дерево элементов, реквизиты, команды, события | | `/form-compile` | ` ` | Генерация Form.xml из компактного JSON-определения | | `/form-validate` | `` | Валидация: уникальность ID, companions, DataPath, команды | +| `/form-add` | ` ` | Добавление элементов, реквизитов, команд в существующую форму | ## Сценарии использования @@ -51,15 +52,32 @@ Commands: Печать -> ПечатьДокумента [Ctrl+P] ``` -### Добавление элемента на форму +### Добавление элементов в существующую форму -Чтобы добавить поле на форму, нужно знать структуру групп и ориентацию (горизонтальная/вертикальная). `/form-info` показывает это в дереве элементов. +`/form-add` добавляет элементы, реквизиты и команды в существующий Form.xml. Автоматически назначает ID, генерирует companion-элементы и обработчики событий. ``` -> Добавь поле "Склад" в шапку формы документа +> Добавь поле "Склад" в шапку формы после "Контрагент" ``` -Claude вызовет `/form-info`, увидит `[Group:AH] ГруппаШапка` → `[Group:V] ГруппаШапкаЛевая`, поймёт куда вставить элемент в XML. +Claude вызовет `/form-info` для анализа структуры, создаст JSON и вызовет `/form-add`: + +```json +{ + "into": "ГруппаШапка", + "after": "Контрагент", + "elements": [ + { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } + ], + "attributes": [ + { "name": "Склад", "type": "CatalogRef.Склады" } + ] +} +``` + +Позиционирование: `into` указывает группу-контейнер, `after` — элемент, после которого вставлять. Оба опциональны (по умолчанию — в конец корневого ChildItems). + +Один вызов может содержать элементы, реквизиты и команды одновременно. Группы поддерживают `children` для вложенных элементов. ### Поиск обработчиков и привязок @@ -173,6 +191,7 @@ Claude создаст JSON-определение и вызовет `/form-compi > /form-info upload/acc_8.3.24/Documents/РеализацияТоваровУслуг/Forms/ФормаДокумента/Ext/Form.xml > /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml > /form-compile src/form.json src/МояОбработка/Forms/Форма/Ext/Form.xml +> /form-add src/МояОбработка/Forms/Форма/Ext/Form.xml src/additions.json > /form-validate src/МояОбработка/Forms/Форма/Ext/Form.xml ``` @@ -182,11 +201,12 @@ Claude создаст JSON-определение и вызовет `/form-compi 1. `/epf-add-form` — создать форму (каркас) 2. `/form-compile` — сгенерировать Form.xml из JSON-определения -3. `/form-validate` — проверить корректность -4. `/form-info` — проанализировать результат -5. `/epf-build` — собрать EPF +3. `/form-add` — добавить элементы/реквизиты/команды в существующую форму +4. `/form-validate` — проверить корректность +5. `/form-info` — проанализировать результат +6. `/epf-build` — собрать EPF ## Спецификации - [Управляемая форма](1c-form-spec.md) — Form.xml, элементы, команды, реквизиты, система типов -- [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` +- [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` и `/form-add`