feat(skd-decompile): сохранение viewMode/itemsViewMode для round-trip

Decompile теперь читает viewMode/itemsViewMode из XML и сохраняет в JSON
точно как было — даже Normal-значения (платформа эмитит эти теги
контекстно, и для bit-perfect нам важно наличие, а не сам режим).

Чтение:
- item-level: selection item, order item (новая object form)
- block-level: selection/filter/order/conditionalAppearance →
  XViewMode на settings
- structure group: viewMode + itemsViewMode на самом item
- settings: itemsViewMode

Дополнительно:
- Убран shorthand @normal из filter/condApp/dataParam (Normal — default,
  шум в JSON)
- Структурный shorthand "A > B > details" не сворачивается если есть
  viewMode/itemsViewMode на элементе
- Selection/order на structure-item сохраняются даже = [Auto] —
  compile теперь не дефолтит, поэтому наличие важно для bit-perfect

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-22 15:38:42 +03:00
parent bf4005bf76
commit a46d5a166b
14 changed files with 89 additions and 27 deletions
@@ -1,4 +1,4 @@
# skd-decompile v0.21 — Decompile 1C DCS Template.xml to JSON DSL (draft)
# skd-decompile v0.22 — Decompile 1C DCS Template.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1287,8 +1287,8 @@ function Build-FilterItem {
if ($use -eq 'false') { $flags += '@off' }
if ($userId) { $flags += '@user' }
if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
elseif ($viewMode -eq 'Normal') { $flags += '@normal' }
elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
# Normal is the default — do not emit @normal
# nullity ops have no value
$noValueOps = @('filled','notFilled')
@@ -1299,7 +1299,7 @@ function Build-FilterItem {
if ($op -notin $noValueOps -and $null -ne $value) { $obj['value'] = $value }
if ($use -eq 'false') { $obj['use'] = $false }
if ($userId) { $obj['userSettingID'] = 'auto' }
if ($viewMode) { $obj['viewMode'] = $viewMode }
if ($viewMode -and $viewMode -ne 'Normal') { $obj['viewMode'] = $viewMode }
$obj['userSettingPresentation'] = Get-MLText $userPresNode
return $obj
}
@@ -1334,7 +1334,14 @@ function Build-SelectionItem {
$fName = Get-Text $item "dcsset:field"
$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
$title = Get-MLText $titleNode
if ($title) { return [ordered]@{ field = $fName; title = $title } }
$vm = Get-Text $item "dcsset:viewMode"
$hasVM = $vm -and $vm -ne 'Normal'
if ($title -or $hasVM) {
$obj = [ordered]@{ field = $fName }
if ($title) { $obj['title'] = $title }
if ($hasVM) { $obj['viewMode'] = $vm }
return $obj
}
return $fName
}
'SelectedItemFolder' {
@@ -1379,7 +1386,15 @@ function Build-Order {
'OrderItemField' {
$fn = Get-Text $it "dcsset:field"
$ot = Get-Text $it "dcsset:orderType"
if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
$vm = Get-Text $it "dcsset:viewMode"
if ($vm -and $vm -ne 'Normal') {
$obj = [ordered]@{ field = $fn }
if ($ot -eq 'Desc') { $obj['direction'] = 'desc' }
$obj['viewMode'] = $vm
$out += $obj
} else {
if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
}
}
default { $out += (New-Sentinel -kind "OrderItem:$xt" -loc $loc -detail 'Неизвестный тип сортировки') }
}
@@ -1437,7 +1452,7 @@ function Build-ConditionalAppearance {
$pres = Get-Text $it "dcsset:presentation"
if ($pres) { $entry['presentation'] = $pres }
$vm = Get-Text $it "dcsset:viewMode"
if ($vm) { $entry['viewMode'] = $vm }
if ($vm -and $vm -ne 'Normal') { $entry['viewMode'] = $vm }
$usid = Get-Text $it "dcsset:userSettingID"
if ($usid) { $entry['userSettingID'] = 'auto' }
$out += $entry
@@ -1666,17 +1681,17 @@ function Build-Structure {
$gFields = Get-GroupFields -parentNode $it -loc $loc
if ($gFields.Count -gt 0) { $entry['groupFields'] = $gFields }
# Local selection — only emit if not "[Auto]" default
# Local selection — preserve presence (even [Auto]) for bit-perfect round-trip
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0 -and -not ($selItems.Count -eq 1 -and $selItems[0] -eq 'Auto')) {
$entry['selection'] = $selItems
if ($selNode) {
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems }
}
# Local order
# Local order — same
$ordNode = $it.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0 -and -not ($ordItems.Count -eq 1 -and $ordItems[0] -eq 'Auto')) {
$entry['order'] = $ordItems
if ($ordNode) {
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems }
}
# Local filter
$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
@@ -1690,6 +1705,19 @@ function Build-Structure {
$children = Build-Structure -node $it -loc "$loc/children"
if ($children.Count -gt 0) { $entry['children'] = $children }
# viewMode / itemsViewMode on the group itself
# Read direct-child <dcsset:viewMode> (avoid grabbing item-level ones from selection/filter/order)
$gvm = $null; $givm = $null
foreach ($ch in $it.ChildNodes) {
if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne 'http://v8.1c.ru/8.1/data-composition-system/settings') { continue }
if ($ch.LocalName -eq 'viewMode' -and $null -eq $gvm) { $gvm = $ch.InnerText }
elseif ($ch.LocalName -eq 'itemsViewMode' -and $null -eq $givm) { $givm = $ch.InnerText }
}
# Preserve explicit values (even Normal) so compile bit-perfect roundtrip works:
# platform emits viewMode on some StructureItemGroup shapes but not others.
if ($null -ne $gvm) { $entry['viewMode'] = $gvm }
if ($null -ne $givm) { $entry['itemsViewMode'] = $givm }
$items += $entry
$idx++
}
@@ -1711,6 +1739,8 @@ function Try-StructureShorthand {
if ($cur.Contains('selection')) { return $null }
if ($cur.Contains('order')) { return $null }
if ($cur.Contains('filter')) { return $null }
if ($cur.Contains('viewMode')) { return $null }
if ($cur.Contains('itemsViewMode')) { return $null }
$gfs = $cur['groupFields']
if ($null -eq $gfs -or $gfs.Count -eq 0) {
# details level (terminal)
@@ -1953,10 +1983,24 @@ foreach ($sv in $svNodes) {
$settingsNode = $sv.SelectSingleNode("dcsset:settings", $ns)
$settings = [ordered]@{}
# Helper: read block-level <dcsset:viewMode> (direct child, not item-level)
function Get-BlockVM($node) {
if (-not $node) { return $null }
foreach ($child in $node.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'viewMode' -and $child.NamespaceURI -eq 'http://v8.1c.ru/8.1/data-composition-system/settings') {
return $child.InnerText
}
}
return $null
}
# selection (top-level)
$selTop = $settingsNode.SelectSingleNode("dcsset:selection", $ns)
$selItems = Build-Selection -selNode $selTop -loc "variant[$vi]/selection"
if ($selItems.Count -gt 0) { $settings['selection'] = $selItems }
# Block-level viewMode: preserve exact presence (even Normal) for bit-perfect round-trip
$svm = Get-BlockVM $selTop
if ($null -ne $svm) { $settings['selectionViewMode'] = $svm }
# filter
$fTop = $settingsNode.SelectSingleNode("dcsset:filter", $ns)
@@ -1965,11 +2009,15 @@ foreach ($sv in $svNodes) {
foreach ($fc in $fTop.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "variant[$vi]/filter") }
$settings['filter'] = $fa
}
$fvm = Get-BlockVM $fTop
if ($null -ne $fvm) { $settings['filterViewMode'] = $fvm }
# order
$ordTop = $settingsNode.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordTop -loc "variant[$vi]/order"
if ($ordItems.Count -gt 0) { $settings['order'] = $ordItems }
$ovm = Get-BlockVM $ordTop
if ($null -ne $ovm) { $settings['orderViewMode'] = $ovm }
# conditionalAppearance
$caTop = $settingsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
@@ -1977,6 +2025,8 @@ foreach ($sv in $svNodes) {
$ca = Build-ConditionalAppearance -caNode $caTop -loc "variant[$vi]/ca"
if ($ca.Count -gt 0) { $settings['conditionalAppearance'] = $ca }
}
$cavm = Get-BlockVM $caTop
if ($null -ne $cavm) { $settings['conditionalAppearanceViewMode'] = $cavm }
# outputParameters
$opTop = $settingsNode.SelectSingleNode("dcsset:outputParameters", $ns)
@@ -1996,6 +2046,10 @@ foreach ($sv in $svNodes) {
else { $settings['structure'] = $structItems }
}
# <dcsset:itemsViewMode> on settings — preserve presence (even Normal)
$sivmNode = $settingsNode.SelectSingleNode("dcsset:itemsViewMode", $ns)
if ($sivmNode) { $settings['itemsViewMode'] = $sivmNode.InnerText }
# Skip pure-default variants: settings contains only "details" structure (or nothing) +
# name=Основной + no distinctive title.
$nonStructKeys = @($settings.Keys | Where-Object { $_ -ne 'structure' })
@@ -8,5 +8,6 @@
"СписокДокументов: CatalogRef.Документы @valueList",
"СлужебныйПар: string @hidden",
{ "name": "ПорядокОкругления", "type": "EnumRef.Округления", "value": "Перечисление.Округления.Окр1", "availableValues": [{ "value": "Перечисление.Округления.Окр1_00", "presentation": "руб. коп" }, { "value": "Перечисление.Округления.Окр1", "presentation": "руб." }] }
]
],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -1 +1 @@
{ "dataSets": [{ "name": "ПродажиПоПериодам", "query": "@decompiled-ПродажиПоПериодам.sql", "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] }] }
{ "dataSets": [{ "name": "ПродажиПоПериодам", "query": "@decompiled-ПродажиПоПериодам.sql", "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -3,5 +3,6 @@
{ "name": "Запрос1", "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Имя ИЗ Справочник.Номенклатура КАК Номенклатура", "fields": ["Имя: string"] },
{ "name": "Журнал", "objectName": "ЖурналОшибок", "fields": ["Сообщение: string(150)", "Уровень: string"] },
{ "name": "Объединение", "items": [{ "name": "Часть1", "query": "ВЫБРАТЬ 1 КАК Поле", "fields": ["Поле: decimal(10,2)"] }, { "name": "Часть2", "query": "ВЫБРАТЬ 2 КАК Поле", "fields": ["Поле: decimal(10,2)"] }] }
]
],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -9,5 +9,6 @@
{ "field": "ПростоЕА", "type": "CatalogRef.Сотрудники", "inputParameters": [{ "parameter": "ПараметрыВыбора", "choiceParameters": [] }, { "parameter": "БыстрыйВыбор", "use": false, "value": true }] }
]
}
]
],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -1 +1 @@
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.ВидыРасчета", "fields": [{ "field": "ВидРасчета", "type": "CatalogRef.ВидыРасчета", "orderExpression": { "expression": "ЕстьNULL(ВидРасчета.Порядок, 10000)", "orderType": "Asc", "autoOrder": false } }] }] }
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.ВидыРасчета", "fields": [{ "field": "ВидРасчета", "type": "CatalogRef.ВидыРасчета", "orderExpression": { "expression": "ЕстьNULL(ВидРасчета.Порядок, 10000)", "orderType": "Asc", "autoOrder": false } }] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -1 +1,4 @@
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ РегистрНакопления.Остатки", "fields": ["Период: date @period", "Контрагент: CatalogRef.Контрагенты @dimension @required", "СуммаНач: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance", "СуммаКон: decimal(15,2) @balance balanceGroupName=Сумма balanceType=ClosingBalance"] }] }
{
"dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ РегистрНакопления.Остатки", "fields": ["Период: date @period", "Контрагент: CatalogRef.Контрагенты @dimension @required", "СуммаНач: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance", "СуммаКон: decimal(15,2) @balance balanceGroupName=Сумма balanceType=ClosingBalance"] }],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -17,5 +17,6 @@
{ "field": "СВыражениемПредставления", "type": "CatalogRef.Номенклатура", "presentationExpression": "Представление(СВыражениемПредставления)" }
]
}
]
],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -1 +1 @@
{ "dataSets": [{ "name": "НаборДанных1", "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", "fields": ["Наименование"] }] }
{ "dataSets": [{ "name": "НаборДанных1", "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", "fields": ["Наименование"] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -1 +1 @@
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле: string"] }], "templates": [{ "name": "Заголовок", "style": "myHeader", "rows": [["A"]] }] }
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле: string"] }], "templates": [{ "name": "Заголовок", "style": "myHeader", "rows": [["A"]] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -1 +1 @@
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле: string"] }], "templates": [{ "name": "СмешанныйМакет", "style": "data", "rows": [["A", { "value": "B", "style": "header" }, "C"]] }] }
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле: string"] }], "templates": [{ "name": "СмешанныйМакет", "style": "data", "rows": [["A", { "value": "B", "style": "header" }, "C"]] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -1 +1 @@
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле1: string", "Поле2: string"] }], "templates": [{ "name": "БезСтиля", "style": "none", "widths": ["7", "7"], "rows": [["A", "B"]] }] }
{ "dataSets": [{ "name": "Тест", "query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники", "fields": ["Поле1: string", "Поле2: string"] }], "templates": [{ "name": "БезСтиля", "style": "none", "widths": ["7", "7"], "rows": [["A", "B"]] }], "settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }] }
@@ -3,5 +3,6 @@
"templates": [
{ "name": "Шапка", "style": "header", "widths": ["20", "30", "25", "25"], "rows": [["Имя", "Сумма", "Поступление", ">"], ["|", "|", "из произв.", "со сч.40"], ["К1", "К2", "К3", "К4"]] },
{ "name": "Данные", "style": "data", "widths": ["20", "30", "25", "25"], "rows": [["{Имя}", "{Сумма}", "{Поступление}", "{СчетПрочее}"]], "parameters": [{ "name": "Имя", "expression": "Имя" }, { "name": "Сумма", "expression": "Сумма" }, { "name": "Поступление", "expression": "СуммаПоступления", "drilldown": "СуммаПоступления" }, { "name": "СчетПрочее", "expression": "СчетПрочее" }] }
]
],
"settingsVariants": [{ "name": "Основной", "settings": { "structure": [{ "selection": ["Auto"], "order": ["Auto"] }] } }]
}
@@ -11,7 +11,7 @@
"selection": ["Организация", { "folder": "Объёмы", "items": ["Сумма", "Количество"] }],
"filter": ["Организация = _ @off @user", { "group": "Or", "items": ["Статус = Активен", "Сумма > 1000"] }],
"order": ["Сумма desc"],
"conditionalAppearance": [{ "filter": ["Сумма > 10000"], "appearance": { "ЦветТекста": "style:НегативныйТекстЦвет" }, "presentation": "Большие суммы", "viewMode": "Normal", "userSettingID": "auto" }],
"conditionalAppearance": [{ "filter": ["Сумма > 10000"], "appearance": { "ЦветТекста": "style:НегативныйТекстЦвет" }, "presentation": "Большие суммы", "userSettingID": "auto" }],
"outputParameters": { "Заголовок": "Сводка по организациям" },
"dataParameters": "auto",
"structure": "Организация > Номенклатура > details"