feat(form-decompile,form-compile): userSettingPresentation — плоская строка xs:string vs мультиязычный LocalStringType

Раундтрип ломал кастомную подпись пользовательской настройки в элементах настроек компоновки
(filter/order/conditionalAppearance/dataParameters): <dcsset:userSettingPresentation xsi:type="xs:string">
эмитился как мультиязычный <v8:item> блок (без нужного xsi:type) → 182 строки diff на 13 формах.

Корпус (acc+erp 8.3.24): 26 xs:string (плоская строка) vs 7 v8:LocalStringType (мультиязычный).
Компилятор всегда звал Emit-MLText (мультиязычная форма без xsi:type) — ломал ОБА случая.

compile (ps1+py): выделенный Emit-USPresentation/emit_us_presentation — строка → xsi:type="xs:string",
объект {ru,en} → xsi:type="v8:LocalStringType". Заменены 4 call-site (filter item/CA/dataParameters).
decompile: Get-PresText (строка ИЛИ объект) уже стоял в filter/group/order; добавлен в dataParameters
(был Get-MLText, ронял xs:string).

Верификация: таргет-раундтрип 13 форм с xs:string-подписью → match (182→0); регресс form-compile
43/43 (ps1+py); 1С-cert dynamic-list-form (оба типа подписи — xs:string и LocalStringType — грузятся). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-12 15:42:27 +03:00
parent 8542b45719
commit 01e5de8acf
6 changed files with 64 additions and 18 deletions
@@ -1,4 +1,4 @@
# form-compile v1.135 — Compile 1C managed form from JSON or object metadata
# form-compile v1.136 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -1577,6 +1577,19 @@ function Emit-MLText {
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 (иначе гибрид-раундтрип поедет).
@@ -1689,7 +1702,7 @@ function Emit-FilterItem {
$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" }
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>"
return
}
@@ -1770,7 +1783,7 @@ function Emit-FilterItem {
$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" }
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>"
}
@@ -1971,7 +1984,7 @@ function Emit-ConditionalAppearance {
$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" }
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 = @{}
@@ -5175,7 +5188,7 @@ function Emit-DataParameters {
}
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" }
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>" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.135 — Compile 1C managed form from JSON or object metadata
# form-compile v1.136 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1310,6 +1310,16 @@ def emit_mltext(lines, indent, tag, text, xsi_type=None):
lines.append(f"{indent}</{tag}>")
def emit_us_presentation(lines, indent, tag, val):
# <dcsset:userSettingPresentation>: плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType
if val is None:
return
if isinstance(val, str):
lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}</{tag}>')
else:
emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType')
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
@@ -1446,7 +1456,7 @@ def emit_filter_item(lines, item, indent):
guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(guid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
lines.append(f'{indent}</dcsset:item>')
return
@@ -1506,7 +1516,7 @@ def emit_filter_item(lines, item, indent):
uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
lines.append(f'{indent}</dcsset:item>')
@@ -1721,7 +1731,7 @@ def emit_conditional_appearance(lines, items, indent, block_view_mode=None, bloc
uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID'])
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if ca.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation'])
emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation'])
if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0:
use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header',
'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader',
@@ -4922,7 +4932,7 @@ def emit_data_parameters(lines, items, indent, block_view_mode=None):
uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID'])
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if dp.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation'])
emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation'])
lines.append(f'{indent}\t</dcscor:item>')
if block_view_mode is not None:
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
@@ -1,4 +1,4 @@
# form-decompile v0.108 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.109 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -1259,7 +1259,7 @@ function Build-FormDataParameters {
if ($use -eq 'false') { $obj['use'] = $false }
if ($usidN) { $obj['userSettingID'] = 'auto' }
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
if ($uspN) { $usp = Get-MLText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } }
if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } }
$entries += $obj
} else {
$s = $pn; if ($use -eq 'false') { $s += ' @off' }
+1 -1
View File
@@ -992,7 +992,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и
```
- **order** — строка `"Поле"` (asc) / `"Поле desc"` (синонимы `убыв`/`desc`, `возр`/`asc`) / `"Auto"`, либо объект `{ field, direction?, use?, viewMode? }`.
- **filter** — shorthand `"Поле оператор значение @флаги"` (`@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`; `_` = пусто) или объект `{ field, op, value?, use?, userSettingID? }` или группа `{ group: "And"|"Or"|"Not", items: [...] }`.
- **filter** — shorthand `"Поле оператор значение @флаги"` (`@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`; `_` = пусто) или объект `{ field, op, value?, use?, userSettingID?, userSettingPresentation? }` или группа `{ group: "And"|"Or"|"Not", items: [...] }`. `userSettingPresentation` (кастомная подпись настройки) ведётся **по форме значения**: голая строка → `xsi:type="xs:string"`; объект `{ru,en}``v8:LocalStringType` (так же у `order`/`conditionalAppearance`/`dataParameters`).
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in`/`notIn`, `inHierarchy`/`inListByHierarchy`, `contains`/`notContains`, `beginsWith`/`notBeginsWith`, `like`/`notLike` (подобно; `%`-шаблон в значении, напр. `"КодВалют like %/ %"`), `filled`/`notFilled`. Регистр оператора не важен; у `like`/`notLike` есть рус. синоним `подобно`/`неподобно`.
- **Дата в фильтре = `StandardBeginningDate`** (так платформа хранит дату-значение почти всегда — корпус 268 vs 2 `xs:dateTime`). Формы значения (от компактной к полной):
- **голая ISO-дата** `"2020-01-01T00:00:00"` (без `valueType`) → `Custom` + эта дата. Работает и в shorthand: `"ДатаЗаказа > 2020-01-01T00:00:00"`. Это дефолт даты в фильтре.
@@ -21,7 +21,9 @@
"mainTable": "Catalog.Товары", "dynamicDataRead": true, "autoSaveUserSettings": false,
"parameters": [ { "name": "ВыборТовара", "type": "CatalogRef.Товары", "valueListAllowed": true, "value": null } ],
"order": [ "Description", "Code desc" ],
"filter": [ "Артикул = _ @off @user", "Артикул like %тест%", "Description подобно %abc%" ],
"filter": [ "Артикул = _ @off @user", "Артикул like %тест%", "Description подобно %abc%",
{ "field": "Code", "op": "=", "userSettingID": "auto", "userSettingPresentation": "Код товара" },
{ "field": "Description", "op": "=", "userSettingID": "auto", "userSettingPresentation": { "ru": "Наименование", "en": "Name" } } ],
"dataParameters": [ "ВыборТовара @off" ],
"conditionalAppearance": [ { "filter": ["Артикул = _"], "appearance": { "ЦветТекста": "web:Red" } } ]
} }
@@ -135,8 +135,29 @@
<dcsset:comparisonType>Like</dcsset:comparisonType>
<dcsset:right xsi:type="xs:string">%abc%</dcsset:right>
</dcsset:item>
<dcsset:item xsi:type="dcsset:FilterItemComparison">
<dcsset:left xsi:type="dcscor:Field">Code</dcsset:left>
<dcsset:comparisonType>Equal</dcsset:comparisonType>
<dcsset:userSettingID>UUID-003</dcsset:userSettingID>
<dcsset:userSettingPresentation xsi:type="xs:string">Код товара</dcsset:userSettingPresentation>
</dcsset:item>
<dcsset:item xsi:type="dcsset:FilterItemComparison">
<dcsset:left xsi:type="dcscor:Field">Description</dcsset:left>
<dcsset:comparisonType>Equal</dcsset:comparisonType>
<dcsset:userSettingID>UUID-004</dcsset:userSettingID>
<dcsset:userSettingPresentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Наименование</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Name</v8:content>
</v8:item>
</dcsset:userSettingPresentation>
</dcsset:item>
<dcsset:viewMode>Normal</dcsset:viewMode>
<dcsset:userSettingID>UUID-003</dcsset:userSettingID>
<dcsset:userSettingID>UUID-005</dcsset:userSettingID>
</dcsset:filter>
<dcsset:dataParameters>
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
@@ -154,7 +175,7 @@
<dcsset:orderType>Desc</dcsset:orderType>
</dcsset:item>
<dcsset:viewMode>Normal</dcsset:viewMode>
<dcsset:userSettingID>UUID-004</dcsset:userSettingID>
<dcsset:userSettingID>UUID-006</dcsset:userSettingID>
</dcsset:order>
<dcsset:conditionalAppearance>
<dcsset:item>
@@ -173,10 +194,10 @@
</dcsset:appearance>
</dcsset:item>
<dcsset:viewMode>Normal</dcsset:viewMode>
<dcsset:userSettingID>UUID-005</dcsset:userSettingID>
<dcsset:userSettingID>UUID-007</dcsset:userSettingID>
</dcsset:conditionalAppearance>
<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>
<dcsset:itemsUserSettingID>UUID-006</dcsset:itemsUserSettingID>
<dcsset:itemsUserSettingID>UUID-008</dcsset:itemsUserSettingID>
</ListSettings>
</Settings>
</Attribute>