From 3b0061c8a0481c1df64891a63eff5d07cfbcd24e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 4 Jun 2026 21:28:53 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-decompile,form-compile):=20=D0=BC?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D1=82=D0=B8=D1=8F=D0=B7=D1=8B=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=20(=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B5=D1=80=20I)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit БАГ: Emit-MLText стрингифицировал мультиязычный объект {ru,en} → @{ru=…; en=…} (мусор). ERP — двуязычная конфигурация, поэтому это доминирующий пробел раундтрипа (item/content/lang). - compiler PS1+PY: Emit-MLItems/emit_ml_items — по на язык; все вызывающие (Title/ToolTip/InputHint/реквизиты/колонки/команды/форма + Emit-Label) передают сырой объект вместо стрингификации. choice presentation уже был мультиязычен. - decompiler уже давал {ru,en}; убран мёртвый titleFormatted (компилятор выводит formatted из hyperlink). - docs/form-dsl-spec: title/tooltip/inputHint принимают объект {ru,en,…}. - tests: groups (title {ru,en}) сертифицирован в 1С. Эффект (220 форм 2.17): item 3909→1475, content 3193→737, lang 1861→635. Регресс 32/32 PS1+PY. Co-Authored-By: Claude Opus 4.8 --- .../form-compile/scripts/form-compile.ps1 | 54 +++++++++++-------- .../form-compile/scripts/form-compile.py | 43 +++++++++------ .../form-decompile/scripts/form-decompile.ps1 | 5 +- docs/form-dsl-spec.md | 2 +- tests/skills/cases/form-compile/groups.json | 2 +- .../СГруппами/Forms/Форма/Ext/Form.xml | 10 ++++ 6 files changed, 72 insertions(+), 44 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 29bc4043..fa62765b 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.30 — Compile 1C managed form from JSON or object metadata +# form-compile v1.31 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -1521,13 +1521,26 @@ function Esc-Xml { # --- 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, [string]$text, [string]$indent) + param([string]$tag, $text, [string]$indent) X "$indent<$tag>" - X "$indent`t" - X "$indent`t`tru" - X "$indent`t`t$(Esc-Xml $text)" - X "$indent`t" + Emit-MLItems -val $text -indent "$indent`t" X "$indent" } @@ -2045,9 +2058,9 @@ function Emit-Title { 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 } + 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 + Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent } } @@ -2215,7 +2228,7 @@ function Emit-Input { Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) if ($el.inputHint) { - Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner + Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner } # Companions @@ -2476,14 +2489,11 @@ function Emit-Label { $inner = "$indent`t" $hasTitleKey = $null -ne $el.PSObject.Properties['title'] - $labelTitle = if ($hasTitleKey) { "$($el.title)" } else { Title-FromName -name $name } + $labelTitle = if ($hasTitleKey) { $el.title } else { Title-FromName -name $name } if ($labelTitle) { $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 "$labelTitle")</v8:content>" - X "$inner`t</v8:item>" + Emit-MLItems -val $labelTitle -indent "$inner`t" X "$inner" } @@ -2937,9 +2947,9 @@ function Emit-Attributes { X "$indent`t" $inner = "$indent`t`t" - $attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' } + $attrTitle = if ($attr.title) { $attr.title } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' } if ($attrTitle) { - Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner + Emit-MLText -tag "Title" -text $attrTitle -indent $inner } # Type @@ -2970,7 +2980,7 @@ function Emit-Attributes { $colId = New-Id X "$inner`t" if ($col.title) { - Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" + Emit-MLText -tag "Title" -text $col.title -indent "$inner`t`t" } Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" X "$inner`t" @@ -3031,13 +3041,13 @@ function Emit-Commands { X "$indent`t" $inner = "$indent`t`t" - $cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" } + $cmdTitle = if ($cmd.title) { $cmd.title } else { Title-FromName -name "$($cmd.name)" } if ($cmdTitle) { - Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner + Emit-MLText -tag "Title" -text $cmdTitle -indent $inner } if ($cmd.tooltip) { - Emit-MLText -tag "ToolTip" -text "$($cmd.tooltip)" -indent $inner + Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner } if ($cmd.action) { @@ -3289,7 +3299,7 @@ function Compute-MainAcbAutofill { # Title if ($def.title) { - Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" + Emit-MLText -tag "Title" -text $def.title -indent "`t" } # Header @@ -3312,7 +3322,7 @@ if (-not $formTitle -and $def.properties -and $def.properties.title) { $formTitle = $def.properties.title } if ($formTitle) { - Emit-MLText -tag "Title" -text "$formTitle" -indent "`t" + Emit-MLText -tag "Title" -text $formTitle -indent "`t" } # 12b. Properties (skip 'title' — handled above as multilingual) diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 58c35536..df7adf63 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.30 — Compile 1C managed form from JSON or object metadata +# form-compile v1.31 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1254,15 +1254,27 @@ def esc_xml(s): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') +def emit_ml_items(lines, indent, val): + # строка → один ru-элемент; объект {lang: text} → по элементу на язык + if isinstance(val, dict): + for k, v in val.items(): + lines.append(f"{indent}") + lines.append(f"{indent}\t{k}") + lines.append(f"{indent}\t{esc_xml(str(v))}") + lines.append(f"{indent}") + else: + lines.append(f"{indent}") + lines.append(f"{indent}\tru") + lines.append(f"{indent}\t{esc_xml(str(val))}") + lines.append(f"{indent}") + + def emit_mltext(lines, indent, tag, text): if not text: lines.append(f"{indent}<{tag}/>") return lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t") - lines.append(f"{indent}\t\tru") - lines.append(f"{indent}\t\t{esc_xml(text)}") - lines.append(f"{indent}\t") + emit_ml_items(lines, f"{indent}\t", text) lines.append(f"{indent}") @@ -1630,7 +1642,7 @@ def emit_title(lines, el, name, indent, auto=False): # Явный title "" (или None) → подавить. Явный непустой → как есть. if 'title' in el: if el.get('title'): - emit_mltext(lines, indent, 'Title', str(el['title'])) + emit_mltext(lines, indent, 'Title', el['title']) elif auto and name: emit_mltext(lines, indent, 'Title', title_from_name(name)) @@ -2020,7 +2032,7 @@ def emit_input(lines, el, name, eid, indent): emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) if el.get('inputHint'): - emit_mltext(lines, inner, 'InputHint', str(el['inputHint'])) + emit_mltext(lines, inner, 'InputHint', el['inputHint']) # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) @@ -2127,14 +2139,11 @@ def emit_label(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' - label_title = str(el['title'] or '') if 'title' in el else title_from_name(name) + label_title = el['title'] if 'title' in el else title_from_name(name) if label_title: formatted = 'true' if el.get('hyperlink') is True else 'false' lines.append(f'{inner}') - lines.append(f'{inner}\t<v8:item>') - lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>') - lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>') - lines.append(f'{inner}\t</v8:item>') + emit_ml_items(lines, f'{inner}\t', label_title) lines.append(f'{inner}') emit_common_flags(lines, el, inner) @@ -2559,7 +2568,7 @@ def emit_attributes(lines, attrs, indent): if not attr_title and attr.get('main') is not True: attr_title = title_from_name(attr_name) if attr_title: - emit_mltext(lines, inner, 'Title', str(attr_title)) + emit_mltext(lines, inner, 'Title', attr_title) # Type if attr.get('type'): @@ -2585,7 +2594,7 @@ def emit_attributes(lines, attrs, indent): col_id = new_id() lines.append(f'{inner}\t') if col.get('title'): - emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title'])) + emit_mltext(lines, f'{inner}\t\t', 'Title', col['title']) emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') @@ -2641,10 +2650,10 @@ def emit_commands(lines, cmds, indent): cmd_title = cmd.get('title') or title_from_name(str(cmd['name'])) if cmd_title: - emit_mltext(lines, inner, 'Title', str(cmd_title)) + emit_mltext(lines, inner, 'Title', cmd_title) if cmd.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', str(cmd['tooltip'])) + emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) if cmd.get('action'): lines.append(f'{inner}{cmd["action"]}') @@ -3106,7 +3115,7 @@ def main(): if not form_title and defn.get('properties') and defn['properties'].get('title'): form_title = defn['properties']['title'] if form_title: - emit_mltext(lines, '\t', 'Title', str(form_title)) + emit_mltext(lines, '\t', 'Title', form_title) # Properties (skip 'title' — handled above) # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index f62e9b1e..324b0102 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.8 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.9 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -258,8 +258,7 @@ function Add-CommonProps { if ($titleNode) { $t = Get-LangText $titleNode if ($null -ne $t) { $obj['title'] = $t } - $fmt = $titleNode.GetAttribute("formatted") - if ($fmt -eq 'true') { $obj['titleFormatted'] = $true } elseif ($fmt -eq 'false') { $obj['titleFormatted'] = $false } + # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) } $ev = Get-Events $node $elName if ($ev) { diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 4e3e19d9..b0456dc8 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -110,7 +110,7 @@ | Свойство | Тип | Описание | |----------|-----|----------| | `name` | string | Имя элемента (по умолчанию — из значения ключа типа) | -| `title` | string | Заголовок. **Нет ключа** → авто-вывод из имени (для page/popup/label и непривязанных полей/кнопок). **`""`** → подавить (заголовок не выводится). Непустая строка → как есть | +| `title` | string/object | Заголовок. **Нет ключа** → авто-вывод из имени (для page/popup/label и непривязанных полей/кнопок). **`""`** → подавить (заголовок не выводится). Строка → ru. Объект `{ "ru": "…", "en": "…" }` → мультиязычный (по `` на язык). Так же `tooltip`/`inputHint`/`title` команд/реквизитов/колонок | | `hidden` | bool | `true` → `false` | | `disabled` | bool | `true` → `false` | | `readOnly` | bool | `true` → `true` | diff --git a/tests/skills/cases/form-compile/groups.json b/tests/skills/cases/form-compile/groups.json index 78581318..f8dacb58 100644 --- a/tests/skills/cases/form-compile/groups.json +++ b/tests/skills/cases/form-compile/groups.json @@ -22,7 +22,7 @@ { "input": "Поле2", "path": "Поле2", "title": "Поле 2" }, { "labelField": "Метка", "path": "Поле1", "groupVerticalAlign": "Center" } ]}, - { "group": "vertical", "name": "ГруппаПодвал", "children": [ + { "group": "vertical", "name": "ГруппаПодвал", "title": { "ru": "Подвал", "en": "Footer" }, "children": [ { "input": "Поле3", "path": "Поле3", "title": "Поле 3" } ]} ], diff --git a/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml index dc89ea9e..998e6a86 100644 --- a/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml @@ -59,6 +59,16 @@ + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Подвал</v8:content> + </v8:item> + <v8:item> + <v8:lang>en</v8:lang> + <v8:content>Footer</v8:content> + </v8:item> + Vertical