From be9ebedf142f6c070cf879da324aa1c1652ce432 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 20:01:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(skd-compile):=20multilang=20appearance=20va?= =?UTF-8?q?lue=20(=D0=A4=D0=BE=D1=80=D0=BC=D0=B0=D1=82=3D{ru,en}=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D1=80.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emit-AppearanceValue / emit_appearance_value: hashtable/PSCustomObject/dict значение → LocalStringType независимо от ключа. Раньше для значения {ru: "ДЛФ=D", en: "DLF=D"} compile эмитил xs:string "@{ru=ДЛФ=D; en=DLF=D}" (строковое представление PS hashtable) — потеря структуры и неверный XML. Wrapper {use: false, value: ...} распознаётся точечно (требуются оба ключа, чтобы не путать с multilang dict без 'use'). Унификация field-level appearance: parse сохраняет значение как есть (а не str(v)), emit использует Emit-AppearanceValue вместо дублированной mini-логики. Side-effect: "true"/"false" в field appearance теперь эмитятся как xs:boolean (раньше — xs:string). Корректнее для 1С; обновлён один snapshot теста compile. Новый тест appearance-multilang-value (поле + conditionalAppearance с multilang Формат — round-trip bit-perfect). Versions: compile v1.33→v1.34. Закрывает п.2 из handoff («известный баг с multilang appearance values»). --- .../skd-compile/scripts/skd-compile.ps1 | 59 ++++++++------ .../skills/skd-compile/scripts/skd-compile.py | 57 +++++++------ .../Template.xml | 2 +- .../appearance-multilang-value.json | 42 ++++++++++ .../appearance-multilang-value/Template.xml | 79 +++++++++++++++++++ .../decompiled.json | 40 ++++++++++ 6 files changed, 230 insertions(+), 49 deletions(-) create mode 100644 tests/skills/cases/skd-decompile/appearance-multilang-value.json create mode 100644 tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/Template.xml create mode 100644 tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/decompiled.json diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 2104925a..2f8947b1 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.33 — Compile 1C DCS from JSON +# skd-compile v1.34 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -827,10 +827,10 @@ function Emit-Field { if ($fieldDef.restrict) { $f.restrict = @($fieldDef.restrict) } - # Parse appearance + # Parse appearance (сохраняем значение как есть — может быть string или multilang dict) if ($fieldDef.appearance) { foreach ($prop in $fieldDef.appearance.PSObject.Properties) { - $f.appearance[$prop.Name] = "$($prop.Value)" + $f.appearance[$prop.Name] = $prop.Value } } if ($fieldDef.presentationExpression) { @@ -935,14 +935,15 @@ function Emit-Field { X "$indent`t" foreach ($key in $f.appearance.Keys) { $val = $f.appearance[$key] - X "$indent`t`t" - X "$indent`t`t`t$(Esc-Xml $key)" - if ($key -eq "ГоризонтальноеПоложение") { - X "$indent`t`t`t$(Esc-Xml $val)" + # ГоризонтальноеПоложение требует специального 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" + X "$indent`t`t`t$(Esc-Xml $key)" + X "$indent`t`t`t$(Esc-Xml "$val")" + X "$indent`t`t" } else { - X "$indent`t`t`t$(Esc-Xml $val)" + Emit-AppearanceValue -key $key -val $val -indent "$indent`t`t" } - X "$indent`t`t" } X "$indent`t" } @@ -2025,24 +2026,34 @@ function Emit-AppearanceValue { param([string]$key, $val, [string]$indent) X "$indent" - if ($val -is [PSCustomObject] -and $val.use -ne $null -and $val.use -eq $false) { - X "$indent`tfalse" - X "$indent`t$(Esc-Xml $key)" - $actualVal = "$($val.value)" - } else { - X "$indent`t$(Esc-Xml $key)" - $actualVal = "$val" + + # Распознаём wrapper {use: false, value: ...} (необходимо отличать от multilang dict). + $useWrapper = $false + $innerVal = $val + if ($val -is [PSCustomObject] -and $val.PSObject.Properties['use'] -and $val.use -eq $false -and $val.PSObject.Properties['value']) { + $useWrapper = $true + $innerVal = $val.value } - # Auto-detect value type - if ($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 "Формат") { - Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" + if ($useWrapper) { X "$indent`tfalse" } + X "$indent`t$(Esc-Xml $key)" + + # Multilang dict ({"ru": "...", "en": "..."}) → LocalStringType независимо от ключа. + $isMultilang = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) + if ($isMultilang) { + Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" } else { - X "$indent`t$(Esc-Xml $actualVal)" + $actualVal = "$innerVal" + if ($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 "Формат") { + # Строковые ключи, традиционно эмитятся как LocalStringType (даже если только ru). + Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" + } else { + X "$indent`t$(Esc-Xml $actualVal)" + } } X "$indent" } diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 3df68cd6..524fc040 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.33 — Compile 1C DCS from JSON +# skd-compile v1.34 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -627,10 +627,10 @@ def emit_field(lines, field_def, indent): # Parse restrictions if field_def.get('restrict'): f['restrict'] = list(field_def['restrict']) - # Parse appearance + # Parse appearance (сохраняем значение как есть — может быть string или multilang dict) if field_def.get('appearance'): for k, v in field_def['appearance'].items(): - f['appearance'][k] = str(v) + f['appearance'][k] = v if field_def.get('presentationExpression'): f['presentationExpression'] = str(field_def['presentationExpression']) # attrRestrict @@ -714,13 +714,14 @@ def emit_field(lines, field_def, indent): if f.get('appearance') and len(f['appearance']) > 0: lines.append(f'{indent}\t') for key, val in f['appearance'].items(): - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(key)}') - if key == '\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435': - lines.append(f'{indent}\t\t\t{esc_xml(val)}') + # \u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e xsi:type, \u043d\u0435 \u0441\u0442\u0440\u043e\u043a\u0430 + if key == '\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435' and not isinstance(val, dict): + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(key)}') + lines.append(f'{indent}\t\t\t{esc_xml(str(val))}') + lines.append(f'{indent}\t\t') else: - lines.append(f'{indent}\t\t\t{esc_xml(val)}') - lines.append(f'{indent}\t\t') + emit_appearance_value(lines, key, val, f'{indent}\t\t') lines.append(f'{indent}\t') # PresentationExpression @@ -1686,23 +1687,31 @@ def emit_order(lines, items, indent, skip_auto=False): def emit_appearance_value(lines, key, val, indent): lines.append(f'{indent}') - if isinstance(val, dict) and val.get('use') is False: - lines.append(f'{indent}\tfalse') - lines.append(f'{indent}\t{esc_xml(key)}') - actual_val = str(val.get('value', '')) - else: - lines.append(f'{indent}\t{esc_xml(key)}') - actual_val = str(val) + # \u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0451\u043c wrapper {use: false, value: ...} \u2014 \u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c \u043e\u0431\u0430 \u043a\u043b\u044e\u0447\u0430. + use_wrapper = False + inner_val = val + if isinstance(val, dict) and 'use' in val and val['use'] is False and 'value' in val: + use_wrapper = True + inner_val = val['value'] - # Auto-detect value type - if re.match(r'^(style|web|win):', actual_val): - lines.append(f'{indent}\t{esc_xml(actual_val)}') - elif actual_val == 'true' or actual_val == 'false': - lines.append(f'{indent}\t{actual_val}') - elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'): - emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val) + if use_wrapper: + lines.append(f'{indent}\tfalse') + lines.append(f'{indent}\t{esc_xml(key)}') + + # Multilang dict ({"ru": "...", "en": "..."}) \u2192 LocalStringType \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u043a\u043b\u044e\u0447\u0430. + if isinstance(inner_val, dict): + emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val) else: - lines.append(f'{indent}\t{esc_xml(actual_val)}') + actual_val = str(inner_val) if inner_val is not None else '' + if re.match(r'^(style|web|win):', actual_val): + lines.append(f'{indent}\t{esc_xml(actual_val)}') + elif actual_val == 'true' or actual_val == 'false': + lines.append(f'{indent}\t{actual_val}') + elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'): + # \u0421\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0435 \u043a\u043b\u044e\u0447\u0438 \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u043e \u044d\u043c\u0438\u0442\u044f\u0442\u0441\u044f \u043a\u0430\u043a LocalStringType (\u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e ru). + emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val) + else: + lines.append(f'{indent}\t{esc_xml(actual_val)}') lines.append(f'{indent}') diff --git a/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml b/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml index 935cc8ca..4593b927 100644 --- a/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml +++ b/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml @@ -36,7 +36,7 @@ РастягиватьПоГоризонтали - true + true diff --git a/tests/skills/cases/skd-decompile/appearance-multilang-value.json b/tests/skills/cases/skd-decompile/appearance-multilang-value.json new file mode 100644 index 00000000..4cbaa1bf --- /dev/null +++ b/tests/skills/cases/skd-decompile/appearance-multilang-value.json @@ -0,0 +1,42 @@ +{ + "name": "Appearance с multilang значением (Формат={ru,en}) — round-trip", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "Тест", + "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", + "fields": [ + { + "field": "ДатаДокумента", + "type": "date", + "appearance": { + "Формат": { "ru": "ДЛФ=D", "en": "DLF=D" } + } + } + ] + }], + "settingsVariants": [ + { + "name": "Основной", + "settings": { + "conditionalAppearance": [ + { + "selection": ["ДатаДокумента"], + "appearance": { + "Формат": { "ru": "ДЛФ=DT", "en": "DLF=DT" } + } + } + ] + } + } + ] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" + } + ], + "params": { "templatePath": "Template.xml" }, + "outputPath": "decompiled.json" +} diff --git a/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/Template.xml b/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/Template.xml new file mode 100644 index 00000000..3ecb7672 --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/Template.xml @@ -0,0 +1,79 @@ + + + + ИсточникДанных1 + Local + + + Тест + + ДатаДокумента + ДатаДокумента + + xs:dateTime + + Date + + + + + Формат + + + ru + ДЛФ=D + + + en + DLF=D + + + + + + ИсточникДанных1 + ВЫБРАТЬ * ИЗ Справочник.Сотрудники + + + Основной + + + ru + Основной + + + + + + + + ДатаДокумента + + + + + Формат + + + ru + ДЛФ=DT + + + en + DLF=DT + + + + + + + + + diff --git a/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/decompiled.json b/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/decompiled.json new file mode 100644 index 00000000..47864f78 --- /dev/null +++ b/tests/skills/cases/skd-decompile/snapshots/appearance-multilang-value/decompiled.json @@ -0,0 +1,40 @@ +{ + "dataSets": [ + { + "name": "Тест", + "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", + "fields": [ + { + "field": "ДатаДокумента", + "type": "date", + "appearance": { + "Формат": { + "ru": "ДЛФ=D", + "en": "DLF=D" + } + } + } + ] + } + ], + "settingsVariants": [ + { + "name": "Основной", + "settings": { + "conditionalAppearance": [ + { + "selection": [ + "ДатаДокумента" + ], + "appearance": { + "Формат": { + "ru": "ДЛФ=DT", + "en": "DLF=DT" + } + } + } + ] + } + } + ] +} \ No newline at end of file