diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index b3004d89..6ea04904 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.50 — Compile 1C managed form from JSON or object metadata +# form-compile v1.51 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -2753,6 +2753,8 @@ function Emit-Input { Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner } + Emit-ChoiceList -el $el -indent $inner + # Companions Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip @@ -2927,6 +2929,62 @@ function Emit-ChoicePresentation { X "$indent" } +# 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 } + } + + $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$(Esc-Xml $norm.Text)" + X "$valIndent" + X "$itemIndent" + } + X "$indent" +} + function Emit-Radio { param($el, [string]$name, [int]$id, [string]$indent) @@ -2954,60 +3012,7 @@ function Emit-Radio { X "$inner$($el.columnsCount)" } - # ChoiceList - if ($el.choiceList -and $el.choiceList.Count -gt 0) { - X "$inner" - $itemIndent = "$inner`t" - foreach ($item in $el.choiceList) { - # Pull value (and tolerate Russian synonym "значение") - $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."значение" } - } - - # Pull presentation (presentation OR title synonym) - $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 } - } - - $norm = Normalize-ChoiceValue -value $valRaw - - # Auto-derive presentation if missing - if (-not $hasPres) { - if ($norm.XsiType -eq "xr:DesignTimeRef") { - $tail = ($norm.Text -split '\.')[-1] - $presRaw = Title-FromName -name $tail - } elseif ($norm.XsiType -eq "xs:string") { - $presRaw = $norm.Text - } 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$(Esc-Xml $norm.Text)" - X "$valIndent" - X "$itemIndent" - } - X "$inner" - } + Emit-ChoiceList -el $el -indent $inner Emit-Layout -el $el -indent $inner diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 62827f53..faa9ceb7 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.50 — Compile 1C managed form from JSON or object metadata +# form-compile v1.51 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1959,6 +1959,41 @@ def emit_choice_presentation(lines, pres, indent): lines.append(f"{indent}") +def emit_choice_list(lines, el, indent): + # — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. + choice_list = el.get('choiceList') or [] + if not choice_list: + return + lines.append(f'{indent}') + item_indent = f'{indent}\t' + for item in choice_list: + if not isinstance(item, dict): + continue + val_raw = item.get('value', item.get('значение')) + has_pres = any(k in item for k in ('presentation', 'представление', 'title')) + pres_raw = item.get('presentation', item.get('представление', item.get('title'))) + + norm = normalize_choice_value(val_raw) + + if not has_pres: + if norm['xsi_type'] == 'xr:DesignTimeRef': + tail = norm['text'].split('.')[-1] + pres_raw = title_from_name(tail) + else: + pres_raw = norm['text'] + + lines.append(f'{item_indent}') + val_indent = f'{item_indent}\t' + lines.append(f'{val_indent}') + lines.append(f'{val_indent}0') + lines.append(f'{val_indent}') + emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') + lines.append(f'{val_indent}\t{esc_xml(norm["text"])}') + lines.append(f'{val_indent}') + lines.append(f'{item_indent}') + lines.append(f'{indent}') + + def normalize_radio_button_type(raw): if not raw: return "Auto" @@ -2624,6 +2659,8 @@ def emit_input(lines, el, name, eid, indent): if el.get('inputHint'): emit_mltext(lines, inner, 'InputHint', el['inputHint']) + emit_choice_list(lines, el, inner) + # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) @@ -2684,36 +2721,7 @@ def emit_radio_button_field(lines, el, name, eid, indent): if el.get('columnsCount') is not None: lines.append(f'{inner}{el["columnsCount"]}') - choice_list = el.get('choiceList') or [] - if choice_list: - lines.append(f'{inner}') - item_indent = f'{inner}\t' - for item in choice_list: - if not isinstance(item, dict): - continue - val_raw = item.get('value', item.get('значение')) - has_pres = any(k in item for k in ('presentation', 'представление', 'title')) - pres_raw = item.get('presentation', item.get('представление', item.get('title'))) - - norm = normalize_choice_value(val_raw) - - if not has_pres: - if norm['xsi_type'] == 'xr:DesignTimeRef': - tail = norm['text'].split('.')[-1] - pres_raw = title_from_name(tail) - else: - pres_raw = norm['text'] - - lines.append(f'{item_indent}') - val_indent = f'{item_indent}\t' - lines.append(f'{val_indent}') - lines.append(f'{val_indent}0') - lines.append(f'{val_indent}') - emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') - lines.append(f'{val_indent}\t{esc_xml(norm["text"])}') - lines.append(f'{val_indent}') - lines.append(f'{item_indent}') - lines.append(f'{inner}') + emit_choice_list(lines, el, inner) emit_layout(lines, el, inner) diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 5f4dae89..133956df 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.32 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.33 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -909,6 +909,28 @@ function Decompile-CompanionPanel { return $o } +# Инверсия Emit-ChoiceList: … → [ { 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) { + $xsiType = $valNode.GetAttribute("type", $NS_XSI) + $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType + } + if ($presNode) { $p = Get-LangText $presNode; if ($p) { $ci['presentation'] = $p } } + [void]$items.Add($ci) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + function Decompile-Element { param($node) $tag = $node.LocalName @@ -961,6 +983,7 @@ function Decompile-Element { foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton')) { $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } } 'CheckBoxField' { $obj[$key] = $name @@ -980,22 +1003,7 @@ function Decompile-Element { Add-TitleLocation $obj $node 'None' $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } - $cl = $node.SelectSingleNode("lf:ChoiceList", $ns) - if ($cl) { - $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) { - $xsiType = $valNode.GetAttribute("type", $NS_XSI) - $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType - } - if ($presNode) { $p = Get-LangText $presNode; if ($p) { $ci['presentation'] = $p } } - [void]$items.Add($ci) - } - if ($items.Count -gt 0) { $obj['choiceList'] = @($items) } - } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } } 'LabelDecoration' { $obj[$key] = $name diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index e1575940..14462b5b 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -298,6 +298,7 @@ companion-панели с собственным контентом. Оба не | `editMode` | string | Режим редактирования: `EnterOnInput`, `Directly` | | `skipOnInput` | bool | Пропускать при вводе | | `inputHint` | string | Подсказка ввода (placeholder) | +| `choiceList` | array | Список выбора: массив `{ value, presentation?/title? }` — та же грамматика, что у `radio` (см. ниже) | | `width` | int | Ширина | | `height` | int | Высота | | `horizontalStretch` | bool | Растягивание по горизонтали | diff --git a/tests/skills/cases/form-compile/input-fields.json b/tests/skills/cases/form-compile/input-fields.json index 38d51228..be87a43f 100644 --- a/tests/skills/cases/form-compile/input-fields.json +++ b/tests/skills/cases/form-compile/input-fields.json @@ -21,6 +21,10 @@ { "input": "МногострочноеПоле", "path": "МногострочноеПоле", "multiLine": true, "height": 5, "title": "Комментарий" }, { "input": "ПолеПароля", "path": "ПолеПароля", "passwordMode": true, "title": "Пароль" }, { "input": "ПолеСКнопками", "path": "ПолеСКнопками", "choiceButton": true, "clearButton": true, "title": "Выбор" }, + { "input": "ПолеСписокВыбора", "path": "ПолеСписокВыбора", "title": "Список выбора", "choiceList": [ + { "value": "Первый" }, + { "value": "Второй", "presentation": "Второй вариант" } + ]}, { "input": "ПолеПодсказка", "path": "ПолеПодсказка", "inputHint": "Введите значение...", "title": "Подсказка" }, { "check": "Флаг", "path": "Флаг", "title": "Включено" }, { "check": "ФлагПлатформенный", "path": "ФлагПлатформенный", "title": "Слева", "titleLocation": "" }, @@ -33,6 +37,7 @@ { "name": "МногострочноеПоле", "type": "string" }, { "name": "ПолеПароля", "type": "string(50)" }, { "name": "ПолеСКнопками", "type": "string" }, + { "name": "ПолеСписокВыбора", "type": "string" }, { "name": "ПолеПодсказка", "type": "string" }, { "name": "Флаг", "type": "boolean" }, { "name": "ФлагПлатформенный", "type": "boolean" }, diff --git a/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml index 64b87d90..f1754f32 100644 --- a/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml @@ -73,7 +73,46 @@ - + + ПолеСписокВыбора + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Список выбора</v8:content> + </v8:item> + + + + + 0 + + + + ru + Первый + + + Первый + + + + + 0 + + + + ru + Второй вариант + + + Второй + + + + + + + ПолеПодсказка <v8:item> @@ -87,10 +126,10 @@ <v8:content>Введите значение...</v8:content> </v8:item> </InputHint> - <ContextMenu name="ПолеПодсказкаКонтекстноеМеню" id="17"/> - <ExtendedTooltip name="ПолеПодсказкаРасширеннаяПодсказка" id="18"/> + <ContextMenu name="ПолеПодсказкаКонтекстноеМеню" id="20"/> + <ExtendedTooltip name="ПолеПодсказкаРасширеннаяПодсказка" id="21"/> </InputField> - <CheckBoxField name="Флаг" id="19"> + <CheckBoxField name="Флаг" id="22"> <DataPath>Флаг</DataPath> <Title> <v8:item> @@ -100,10 +139,10 @@ Auto Right - - + + - + ФлагПлатформенный <v8:item> @@ -112,10 +151,10 @@ </v8:item> Auto - - + + - + ФлагЯвный <v8:item> @@ -125,10 +164,10 @@ Auto Top - - + + - + ФлагТумблер <v8:item> @@ -138,18 +177,18 @@ Switcher Right - - + + - + cfg:DataProcessorObject.ПоляВвода true - + <v8:item> <v8:lang>ru</v8:lang> @@ -164,7 +203,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="МногострочноеПоле" id="33"> + <Attribute name="МногострочноеПоле" id="36"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -179,7 +218,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеПароля" id="34"> + <Attribute name="ПолеПароля" id="37"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -194,7 +233,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеСКнопками" id="35"> + <Attribute name="ПолеСКнопками" id="38"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -209,7 +248,22 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеПодсказка" id="36"> + <Attribute name="ПолеСписокВыбора" id="39"> + <Title> + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Поле список выбора</v8:content> + </v8:item> + + + xs:string + + 0 + Variable + + + + <v8:item> <v8:lang>ru</v8:lang> @@ -224,7 +278,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="Флаг" id="37"> + <Attribute name="Флаг" id="41"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -235,7 +289,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагПлатформенный" id="38"> + <Attribute name="ФлагПлатформенный" id="42"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -246,7 +300,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагЯвный" id="39"> + <Attribute name="ФлагЯвный" id="43"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -257,7 +311,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагТумблер" id="40"> + <Attribute name="ФлагТумблер" id="44"> <Title> <v8:item> <v8:lang>ru</v8:lang>