mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-02 01:37:45 +03:00
feat(form-decompile,form-compile): настройки динамического списка — источник, ListSettings, контент filter/order/conditionalAppearance (кластер DynamicList Settings)
Декомпилятор (v0.21): парс <Settings xsi:type="DynamicList"> — mainTable/query/ dynamicDataRead(дефолт true→omit)/fields(только при наличии); вынос query в <basename>-<имяСписка>.sql рядом с JSON (зеркало skd-decompile); захват контента ListSettings (filter/order/conditionalAppearance) в skd-грамматику. Пустой/ каноничный скелет опускается (компилятор регенерит). Компилятор (ps1+py v1.39): query→ManualQuery=true+QueryText (+@file-резолвер); порядок платформы ManualQuery→DynamicDataRead→QueryText→Field*→MainTable→ListSettings; прощающий ввод (Справочник.X→Catalog.X через refRootSynonyms; убыв→desc/возр→asc); каноничный ListSettings-скелет с константными GUID контейнеров (~90% форм бит-в-бит); эмиттеры filter/order/conditionalAppearance скопированы из skd-compile (навыки автономны). Валидация: раундтрип CLEAN (бит-в-бит, GUID-норм.) на order/filter/condApp/пустом; сертификация в 1С PASS (пустой скелет + контент грузятся в базу); py==ps1 идентичны; регресс 33/33 ps+py; harness 12381→9634 (−22%), 0 fail, LOST контента=0. Тест-кейс dynamic-list-form дополнен контентом; spec — раздел settings динсписка. Хвост (минимальный/частичный ListSettings) — в BACKLOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6857ad5060
commit
15883a7e7c
@@ -1,4 +1,4 @@
|
||||
# form-compile v1.38 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.39 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$JsonPath,
|
||||
@@ -1496,6 +1496,22 @@ if ($FromObject) {
|
||||
$def = $json | ConvertFrom-Json
|
||||
}
|
||||
|
||||
# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
|
||||
$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path }
|
||||
function Resolve-QueryValue {
|
||||
param([string]$val, [string]$baseDir)
|
||||
if (-not $val.StartsWith("@")) { return $val }
|
||||
$filePath = $val.Substring(1)
|
||||
if ([System.IO.Path]::IsPathRooted($filePath)) {
|
||||
$candidates = @($filePath)
|
||||
} else {
|
||||
$candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath))
|
||||
}
|
||||
foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } }
|
||||
Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 2. ID allocator ---
|
||||
|
||||
$script:nextId = 1
|
||||
@@ -1547,6 +1563,347 @@ function Emit-MLText {
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
|
||||
# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип
|
||||
# (harness нормализует GUID для хвоста с иными идентификаторами).
|
||||
$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
|
||||
$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
|
||||
$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
|
||||
$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Настройки компоновщика ListSettings: filter/order/conditionalAppearance.
|
||||
# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
function New-Guid-String { return [System.Guid]::NewGuid().ToString() }
|
||||
|
||||
$script:comparisonTypes = @{
|
||||
"=" = "Equal"; "<>" = "NotEqual"
|
||||
">" = "Greater"; ">=" = "GreaterOrEqual"
|
||||
"<" = "Less"; "<=" = "LessOrEqual"
|
||||
"in" = "InList"; "notIn" = "NotInList"
|
||||
"inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
|
||||
"contains" = "Contains"; "notContains" = "NotContains"
|
||||
"beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
|
||||
"filled" = "Filled"; "notFilled" = "NotFilled"
|
||||
}
|
||||
|
||||
function Parse-FilterShorthand {
|
||||
param([string]$s)
|
||||
$result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null }
|
||||
if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' }
|
||||
if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' }
|
||||
if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' }
|
||||
if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' }
|
||||
if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' }
|
||||
$s = $s.Trim()
|
||||
$opPatterns = @('<>', '>=', '<=', '=', '>', '<',
|
||||
'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b',
|
||||
'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b',
|
||||
'notFilled\b', 'filled\b')
|
||||
$opJoined = $opPatterns -join '|'
|
||||
if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") {
|
||||
$result.field = $Matches[1].Trim()
|
||||
$result.op = $Matches[2].Trim()
|
||||
$valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" }
|
||||
if ($valPart -and $valPart -ne "_") {
|
||||
if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" }
|
||||
elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart; $result["valueType"] = "xs:dateTime" }
|
||||
elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" }
|
||||
elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" }
|
||||
else { $result.value = $valPart; $result["valueType"] = "xs:string" }
|
||||
}
|
||||
} else { $result.field = $s }
|
||||
return $result
|
||||
}
|
||||
|
||||
function Emit-FilterItem {
|
||||
param($item, [string]$indent)
|
||||
if ($item.group) {
|
||||
$groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } }
|
||||
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">"
|
||||
X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>"
|
||||
if ($item.items) {
|
||||
foreach ($sub in $item.items) {
|
||||
if ($sub -is [string]) {
|
||||
$parsed = Parse-FilterShorthand $sub
|
||||
$obj = @{ field = $parsed.field; op = $parsed.op }
|
||||
if ($parsed.use -eq $false) { $obj.use = $false }
|
||||
if ($null -ne $parsed.value) { $obj.value = $parsed.value }
|
||||
if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
|
||||
if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
|
||||
if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
|
||||
$sub = [pscustomobject]$obj
|
||||
}
|
||||
Emit-FilterItem -item $sub -indent "$indent`t"
|
||||
}
|
||||
}
|
||||
if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" }
|
||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
||||
if ($item.userSettingID) {
|
||||
$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" }
|
||||
X "$indent</dcsset:item>"
|
||||
return
|
||||
}
|
||||
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
|
||||
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
|
||||
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>"
|
||||
$compType = $script:comparisonTypes["$($item.op)"]
|
||||
if (-not $compType) { $compType = "$($item.op)" }
|
||||
X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>"
|
||||
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
|
||||
if ($valIsArray) {
|
||||
if (@($item.value).Count -eq 0) {
|
||||
X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">"
|
||||
X "$indent`t`t<v8:valueType/>"
|
||||
X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>"
|
||||
X "$indent`t</dcsset:right>"
|
||||
} else {
|
||||
foreach ($v in $item.value) {
|
||||
$vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
|
||||
if (-not $vt) {
|
||||
if ($v -is [bool]) { $vt = 'xs:boolean' }
|
||||
elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' }
|
||||
elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' }
|
||||
elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' }
|
||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
|
||||
else { $vt = 'xs:string' }
|
||||
}
|
||||
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" }
|
||||
X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||
}
|
||||
}
|
||||
} elseif ($null -ne $item.value) {
|
||||
$vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
|
||||
if (-not $vt) {
|
||||
$v = $item.value
|
||||
if ($v -is [bool]) { $vt = "xs:boolean" }
|
||||
elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" }
|
||||
elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" }
|
||||
elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" }
|
||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
|
||||
else { $vt = "xs:string" }
|
||||
}
|
||||
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
|
||||
X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||
}
|
||||
if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" }
|
||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
||||
if ($item.userSettingID) {
|
||||
$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" }
|
||||
X "$indent</dcsset:item>"
|
||||
}
|
||||
|
||||
function Emit-Filter {
|
||||
param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null)
|
||||
$hasItems = $items -and $items.Count -gt 0
|
||||
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
|
||||
if (-not $hasItems -and -not $hasBlockMeta) { return }
|
||||
X "$indent<dcsset:filter>"
|
||||
foreach ($item in $items) {
|
||||
if ($item -is [string]) {
|
||||
$parsed = Parse-FilterShorthand $item
|
||||
$obj = @{ field = $parsed.field; op = $parsed.op }
|
||||
if ($parsed.use -eq $false) { $obj.use = $false }
|
||||
if ($null -ne $parsed.value) { $obj.value = $parsed.value }
|
||||
if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
|
||||
if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
|
||||
if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
|
||||
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
|
||||
} else { Emit-FilterItem -item $item -indent "$indent`t" }
|
||||
}
|
||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
||||
if ($null -ne $blockUserSettingID) {
|
||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
||||
}
|
||||
X "$indent</dcsset:filter>"
|
||||
}
|
||||
|
||||
function Emit-Order {
|
||||
param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null)
|
||||
$hasItems = $items -and $items.Count -gt 0
|
||||
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
|
||||
if (-not $hasItems -and -not $hasBlockMeta) { return }
|
||||
X "$indent<dcsset:order>"
|
||||
foreach ($item in $items) {
|
||||
if ($item -is [string]) {
|
||||
if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" } }
|
||||
else {
|
||||
$parts = $item -split '\s+'
|
||||
$field = $parts[0]
|
||||
$dir = "Asc"
|
||||
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
|
||||
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||
X "$indent`t</dcsset:item>"
|
||||
}
|
||||
} else {
|
||||
if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" }; continue }
|
||||
$dir = if ($item.direction) { "$($item.direction)" } else { "Asc" }
|
||||
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
||||
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
|
||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
||||
X "$indent`t</dcsset:item>"
|
||||
}
|
||||
}
|
||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
||||
if ($null -ne $blockUserSettingID) {
|
||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
||||
}
|
||||
X "$indent</dcsset:order>"
|
||||
}
|
||||
|
||||
function Emit-AppearanceValue {
|
||||
param([string]$key, $val, [string]$indent)
|
||||
X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
|
||||
function _HasKey { param($o, [string]$k)
|
||||
if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] }
|
||||
if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) }
|
||||
return $false
|
||||
}
|
||||
function _Get { param($o, [string]$k)
|
||||
if ($o -is [PSCustomObject]) { return $o.$k }
|
||||
if ($o -is [System.Collections.IDictionary]) { return $o[$k] }
|
||||
return $null
|
||||
}
|
||||
$isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line')
|
||||
$useWrapper = $false
|
||||
$innerVal = $val
|
||||
$nestedItems = $null
|
||||
if ($isTopLevelLine) {
|
||||
if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
|
||||
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
||||
} elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) {
|
||||
$innerVal = (_Get $val 'value')
|
||||
if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
|
||||
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
||||
}
|
||||
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
|
||||
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
|
||||
$isFontDict = $false
|
||||
if ($innerVal -is [PSCustomObject]) {
|
||||
$tProp = $innerVal.PSObject.Properties['@type']
|
||||
if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true }
|
||||
} elseif ($innerVal -is [System.Collections.IDictionary]) {
|
||||
if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true }
|
||||
}
|
||||
$isLineDict = $false
|
||||
if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') }
|
||||
$isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject])
|
||||
if ($isLineDict) {
|
||||
$lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 }
|
||||
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
|
||||
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
|
||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
|
||||
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>"
|
||||
X "$indent`t</dcscor:value>"
|
||||
} elseif ($isFontDict) {
|
||||
$attrParts = @()
|
||||
foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
|
||||
$av = $null
|
||||
if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } }
|
||||
else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } }
|
||||
if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" }
|
||||
}
|
||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
|
||||
} elseif ($isDict) {
|
||||
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t"
|
||||
} else {
|
||||
$actualVal = "$innerVal"
|
||||
$keyTypeMap = @{
|
||||
'Размещение' = 'dcscor:DataCompositionTextPlacementType'
|
||||
'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign'
|
||||
'ВертикальноеПоложение' = 'v8ui:VerticalAlign'
|
||||
'ОриентацияТекста' = 'xs:decimal'
|
||||
'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement'
|
||||
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
|
||||
}
|
||||
$keyType = $keyTypeMap[$key]
|
||||
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
||||
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
||||
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
|
||||
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" }
|
||||
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
|
||||
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
||||
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
||||
}
|
||||
if ($nestedItems) {
|
||||
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
|
||||
if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } }
|
||||
elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } }
|
||||
}
|
||||
X "$indent</dcscor:item>"
|
||||
}
|
||||
|
||||
function Emit-ConditionalAppearance {
|
||||
param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null)
|
||||
$hasItems = $items -and $items.Count -gt 0
|
||||
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
|
||||
if (-not $hasItems -and -not $hasBlockMeta) { return }
|
||||
X "$indent<dcsset:conditionalAppearance>"
|
||||
foreach ($ca in $items) {
|
||||
X "$indent`t<dcsset:item>"
|
||||
if ($ca.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
||||
if ($ca.selection -and $ca.selection.Count -gt 0) {
|
||||
X "$indent`t`t<dcsset:selection>"
|
||||
foreach ($sel in $ca.selection) {
|
||||
X "$indent`t`t`t<dcsset:item>"
|
||||
X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>"
|
||||
X "$indent`t`t`t</dcsset:item>"
|
||||
}
|
||||
X "$indent`t`t</dcsset:selection>"
|
||||
} else { X "$indent`t`t<dcsset:selection/>" }
|
||||
if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" }
|
||||
else { X "$indent`t`t<dcsset:filter/>" }
|
||||
if ($ca.appearance) {
|
||||
X "$indent`t`t<dcsset:appearance>"
|
||||
foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" }
|
||||
X "$indent`t`t</dcsset:appearance>"
|
||||
}
|
||||
if ($ca.presentation) {
|
||||
if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { Emit-MLText -tag "dcsset:presentation" -text $ca.presentation -indent "$indent`t`t" }
|
||||
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" }
|
||||
}
|
||||
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" }
|
||||
if ($ca.userSettingID) {
|
||||
$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.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
|
||||
$useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader')
|
||||
$set = @{}
|
||||
foreach ($n in $ca.useInDontUse) { $set["$n"] = $true }
|
||||
foreach ($n in $useInOrder) {
|
||||
if ($set.ContainsKey($n)) {
|
||||
$tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1))
|
||||
X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>"
|
||||
}
|
||||
}
|
||||
}
|
||||
X "$indent`t</dcsset:item>"
|
||||
}
|
||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
||||
if ($null -ne $blockUserSettingID) {
|
||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
||||
}
|
||||
X "$indent</dcsset:conditionalAppearance>"
|
||||
}
|
||||
|
||||
# --- 5. Type emitter ---
|
||||
|
||||
$script:formTypeSynonyms = New-Object System.Collections.Hashtable
|
||||
@@ -2327,9 +2684,25 @@ $script:refRootSynonyms = @{
|
||||
"РегистрБухгалтерии" = "AccountingRegister"
|
||||
"РегистрРасчета" = "CalculationRegister"
|
||||
"РегистрРасчёта" = "CalculationRegister"
|
||||
"ЖурналДокументов" = "DocumentJournal"
|
||||
"КритерийОтбора" = "FilterCriterion"
|
||||
}
|
||||
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
|
||||
|
||||
# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты".
|
||||
# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений.
|
||||
function Normalize-MetaTypeRef {
|
||||
param([string]$ref)
|
||||
if ([string]::IsNullOrEmpty($ref)) { return $ref }
|
||||
$dot = $ref.IndexOf('.')
|
||||
if ($dot -lt 1) { return $ref }
|
||||
$root = $ref.Substring(0, $dot)
|
||||
if ($script:refRootSynonyms.ContainsKey($root)) {
|
||||
return $script:refRootSynonyms[$root] + $ref.Substring($dot)
|
||||
}
|
||||
return $ref
|
||||
}
|
||||
|
||||
# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
|
||||
function Normalize-ChoiceValue {
|
||||
param($value)
|
||||
@@ -3056,15 +3429,48 @@ function Emit-Attributes {
|
||||
X "$inner</Columns>"
|
||||
}
|
||||
|
||||
# Settings (for DynamicList)
|
||||
# Settings (динамический список)
|
||||
if ($attr.settings) {
|
||||
$st = $attr.settings
|
||||
X "$inner<Settings xsi:type=`"DynamicList`">"
|
||||
$si = "$inner`t"
|
||||
if ($attr.settings.mainTable) { X "$si<MainTable>$($attr.settings.mainTable)</MainTable>" }
|
||||
$mq = if ($attr.settings.manualQuery -eq $true) { "true" } else { "false" }
|
||||
# Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
$hasQuery = $st.query -and "$($st.query)".Trim()
|
||||
$mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" }
|
||||
X "$si<ManualQuery>$mq</ManualQuery>"
|
||||
$ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" }
|
||||
# DynamicDataRead: дефолт true; false только при явном отключении
|
||||
$ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" }
|
||||
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
|
||||
if ($hasQuery) {
|
||||
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
|
||||
X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>"
|
||||
}
|
||||
# Явные поля набора (редко): override title/dataPath
|
||||
if ($st.fields) {
|
||||
foreach ($fld in $st.fields) {
|
||||
X "$si<Field xsi:type=`"dcssch:DataSetFieldField`">"
|
||||
$dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field }
|
||||
X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>"
|
||||
X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>"
|
||||
if ($fld.title) {
|
||||
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
|
||||
Emit-MLItems -val $fld.title -indent "$si`t`t"
|
||||
X "$si`t</dcssch:title>"
|
||||
}
|
||||
X "$si</Field>"
|
||||
}
|
||||
}
|
||||
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
|
||||
# ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
|
||||
# Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
|
||||
$lsi = "$si`t"
|
||||
X "$si<ListSettings>"
|
||||
Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID
|
||||
Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID
|
||||
Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID
|
||||
X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>"
|
||||
X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>"
|
||||
X "$si</ListSettings>"
|
||||
X "$inner</Settings>"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-compile v1.38 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.39 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import copy
|
||||
@@ -1256,6 +1256,26 @@ def esc_xml(s):
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
# Базовая директория для @file-ссылок в query динсписка (устанавливается в main)
|
||||
QUERY_BASE_DIR = None
|
||||
|
||||
|
||||
def resolve_query_value(val, base_dir):
|
||||
if not val.startswith('@'):
|
||||
return val
|
||||
file_path = val[1:]
|
||||
if os.path.isabs(file_path):
|
||||
candidates = [file_path]
|
||||
else:
|
||||
candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)]
|
||||
for c in candidates:
|
||||
if os.path.exists(c):
|
||||
with open(c, 'r', encoding='utf-8-sig') as f:
|
||||
return f.read().rstrip()
|
||||
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def emit_ml_items(lines, indent, val):
|
||||
# строка → один ru-элемент; объект {lang: text} → по элементу на язык
|
||||
if isinstance(val, dict):
|
||||
@@ -1280,10 +1300,395 @@ def emit_mltext(lines, indent, tag, text):
|
||||
lines.append(f"{indent}</{tag}>")
|
||||
|
||||
|
||||
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
|
||||
CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
|
||||
CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
|
||||
CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
|
||||
CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
|
||||
|
||||
|
||||
def new_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Настройки компоновщика ListSettings: filter/order/conditionalAppearance.
|
||||
# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
COMPARISON_TYPES = {
|
||||
'=': 'Equal', '<>': 'NotEqual',
|
||||
'>': 'Greater', '>=': 'GreaterOrEqual',
|
||||
'<': 'Less', '<=': 'LessOrEqual',
|
||||
'in': 'InList', 'notIn': 'NotInList',
|
||||
'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy',
|
||||
'contains': 'Contains', 'notContains': 'NotContains',
|
||||
'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith',
|
||||
'filled': 'Filled', 'notFilled': 'NotFilled',
|
||||
}
|
||||
|
||||
_REF_TYPE_RE = re.compile(
|
||||
r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|'
|
||||
r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|'
|
||||
r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|'
|
||||
r'InformationRegister|ExchangePlan)\.')
|
||||
|
||||
|
||||
def parse_filter_shorthand(s):
|
||||
result = {'field': '', 'op': 'Equal', 'value': None, 'use': True,
|
||||
'userSettingID': None, 'viewMode': None, 'presentation': None}
|
||||
if re.search(r'@user', s):
|
||||
result['userSettingID'] = 'auto'
|
||||
s = re.sub(r'\s*@user', '', s)
|
||||
if re.search(r'@off', s):
|
||||
result['use'] = False
|
||||
s = re.sub(r'\s*@off', '', s)
|
||||
if re.search(r'@quickAccess', s):
|
||||
result['viewMode'] = 'QuickAccess'
|
||||
s = re.sub(r'\s*@quickAccess', '', s)
|
||||
if re.search(r'@normal', s):
|
||||
result['viewMode'] = 'Normal'
|
||||
s = re.sub(r'\s*@normal', '', s)
|
||||
if re.search(r'@inaccessible', s):
|
||||
result['viewMode'] = 'Inaccessible'
|
||||
s = re.sub(r'\s*@inaccessible', '', s)
|
||||
s = s.strip()
|
||||
op_patterns = ['<>', '>=', '<=', '=', '>', '<',
|
||||
r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b',
|
||||
r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b',
|
||||
r'notFilled\b', r'filled\b']
|
||||
op_joined = '|'.join(op_patterns)
|
||||
m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s)
|
||||
if m:
|
||||
result['field'] = m.group(1).strip()
|
||||
result['op'] = m.group(2).strip()
|
||||
val_part = m.group(3).strip() if m.group(3) else ''
|
||||
if val_part and val_part != '_':
|
||||
if val_part == 'true' or val_part == 'false':
|
||||
result['value'] = (val_part == 'true')
|
||||
result['valueType'] = 'xs:boolean'
|
||||
elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part):
|
||||
result['value'] = val_part
|
||||
result['valueType'] = 'xs:dateTime'
|
||||
elif re.match(r'^\d+(\.\d+)?$', val_part):
|
||||
result['value'] = val_part
|
||||
result['valueType'] = 'xs:decimal'
|
||||
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
|
||||
result['value'] = val_part
|
||||
result['valueType'] = 'dcscor:DesignTimeValue'
|
||||
else:
|
||||
result['value'] = val_part
|
||||
result['valueType'] = 'xs:string'
|
||||
else:
|
||||
result['field'] = s
|
||||
return result
|
||||
|
||||
|
||||
def _value_type_for(v, explicit=None):
|
||||
if explicit:
|
||||
return explicit
|
||||
if isinstance(v, bool):
|
||||
return 'xs:boolean'
|
||||
if isinstance(v, (int, float)):
|
||||
return 'xs:decimal'
|
||||
vs = str(v)
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}T', vs):
|
||||
return 'xs:dateTime'
|
||||
if re.match(r'^-?\d+(\.\d+)?$', vs):
|
||||
return 'xs:decimal'
|
||||
if _REF_TYPE_RE.match(vs):
|
||||
return 'dcscor:DesignTimeValue'
|
||||
return 'xs:string'
|
||||
|
||||
|
||||
def emit_filter_item(lines, item, indent):
|
||||
if item.get('group'):
|
||||
g = str(item['group'])
|
||||
group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group')
|
||||
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemGroup">')
|
||||
lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>')
|
||||
if item.get('items'):
|
||||
for sub in item['items']:
|
||||
if isinstance(sub, str):
|
||||
parsed = parse_filter_shorthand(sub)
|
||||
obj = {'field': parsed['field'], 'op': parsed['op']}
|
||||
if parsed['use'] is False:
|
||||
obj['use'] = False
|
||||
if parsed['value'] is not None:
|
||||
obj['value'] = parsed['value']
|
||||
if parsed.get('valueType'):
|
||||
obj['valueType'] = parsed['valueType']
|
||||
if parsed.get('userSettingID'):
|
||||
obj['userSettingID'] = parsed['userSettingID']
|
||||
if parsed.get('viewMode'):
|
||||
obj['viewMode'] = parsed['viewMode']
|
||||
sub = obj
|
||||
emit_filter_item(lines, sub, f'{indent}\t')
|
||||
if item.get('presentation'):
|
||||
emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation'])
|
||||
if item.get('viewMode'):
|
||||
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
|
||||
if item.get('userSettingID'):
|
||||
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'])
|
||||
lines.append(f'{indent}</dcsset:item>')
|
||||
return
|
||||
|
||||
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">')
|
||||
if item.get('use') is False:
|
||||
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
|
||||
lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(str(item.get("field", "")))}</dcsset:left>')
|
||||
comp_type = COMPARISON_TYPES.get(str(item.get('op')))
|
||||
if not comp_type:
|
||||
comp_type = str(item.get('op'))
|
||||
lines.append(f'{indent}\t<dcsset:comparisonType>{esc_xml(comp_type)}</dcsset:comparisonType>')
|
||||
val = item.get('value')
|
||||
if isinstance(val, list):
|
||||
if len(val) == 0:
|
||||
lines.append(f'{indent}\t<dcsset:right xsi:type="v8:ValueListType">')
|
||||
lines.append(f'{indent}\t\t<v8:valueType/>')
|
||||
lines.append(f'{indent}\t\t<v8:lastId xsi:type="xs:decimal">-1</v8:lastId>')
|
||||
lines.append(f'{indent}\t</dcsset:right>')
|
||||
else:
|
||||
for v in val:
|
||||
vt = _value_type_for(v, item.get('valueType'))
|
||||
v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v))
|
||||
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>')
|
||||
elif val is not None:
|
||||
vt = _value_type_for(val, item.get('valueType'))
|
||||
v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val))
|
||||
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>')
|
||||
if item.get('presentation'):
|
||||
emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation'])
|
||||
if item.get('viewMode'):
|
||||
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
|
||||
if item.get('userSettingID'):
|
||||
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'])
|
||||
lines.append(f'{indent}</dcsset:item>')
|
||||
|
||||
|
||||
def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
|
||||
has_items = bool(items) and len(items) > 0
|
||||
has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
|
||||
if not has_items and not has_block_meta:
|
||||
return
|
||||
lines.append(f'{indent}<dcsset:filter>')
|
||||
for item in (items or []):
|
||||
if isinstance(item, str):
|
||||
parsed = parse_filter_shorthand(item)
|
||||
obj = {'field': parsed['field'], 'op': parsed['op']}
|
||||
if parsed['use'] is False:
|
||||
obj['use'] = False
|
||||
if parsed['value'] is not None:
|
||||
obj['value'] = parsed['value']
|
||||
if parsed.get('valueType'):
|
||||
obj['valueType'] = parsed['valueType']
|
||||
if parsed.get('userSettingID'):
|
||||
obj['userSettingID'] = parsed['userSettingID']
|
||||
if parsed.get('viewMode'):
|
||||
obj['viewMode'] = parsed['viewMode']
|
||||
emit_filter_item(lines, obj, f'{indent}\t')
|
||||
else:
|
||||
emit_filter_item(lines, item, f'{indent}\t')
|
||||
if block_view_mode is not None:
|
||||
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
|
||||
if block_user_setting_id is not None:
|
||||
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
|
||||
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
|
||||
lines.append(f'{indent}</dcsset:filter>')
|
||||
|
||||
|
||||
def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None):
|
||||
has_items = bool(items) and len(items) > 0
|
||||
has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
|
||||
if not has_items and not has_block_meta:
|
||||
return
|
||||
lines.append(f'{indent}<dcsset:order>')
|
||||
for item in (items or []):
|
||||
if isinstance(item, str):
|
||||
if item == 'Auto':
|
||||
if not skip_auto:
|
||||
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
|
||||
else:
|
||||
parts = re.split(r'\s+', item)
|
||||
field = parts[0]
|
||||
direction = 'Asc'
|
||||
if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]):
|
||||
direction = 'Desc'
|
||||
elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]):
|
||||
direction = 'Asc'
|
||||
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">')
|
||||
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>')
|
||||
lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>')
|
||||
lines.append(f'{indent}\t</dcsset:item>')
|
||||
else:
|
||||
if item.get('field') == 'Auto' or item.get('type') == 'auto':
|
||||
if not skip_auto:
|
||||
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
|
||||
continue
|
||||
direction = str(item['direction']) if item.get('direction') else 'Asc'
|
||||
if re.match(r'(?i)^(desc|убыв)', direction):
|
||||
direction = 'Desc'
|
||||
elif re.match(r'(?i)^(asc|возр)', direction):
|
||||
direction = 'Asc'
|
||||
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">')
|
||||
if item.get('use') is False:
|
||||
lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>')
|
||||
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item.get("field", "")))}</dcsset:field>')
|
||||
lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>')
|
||||
if item.get('viewMode'):
|
||||
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
|
||||
lines.append(f'{indent}\t</dcsset:item>')
|
||||
if block_view_mode is not None:
|
||||
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
|
||||
if block_user_setting_id is not None:
|
||||
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
|
||||
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
|
||||
lines.append(f'{indent}</dcsset:order>')
|
||||
|
||||
|
||||
def emit_appearance_value(lines, key, val, indent):
|
||||
lines.append(f'{indent}<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
|
||||
|
||||
def _has_key(o, k):
|
||||
return isinstance(o, dict) and (k in o)
|
||||
|
||||
def _get(o, k):
|
||||
return o.get(k) if isinstance(o, dict) else None
|
||||
|
||||
is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line')
|
||||
use_wrapper = False
|
||||
inner_val = val
|
||||
nested_items = None
|
||||
if is_top_level_line:
|
||||
if _has_key(val, 'use') and (_get(val, 'use') is False):
|
||||
use_wrapper = True
|
||||
if _has_key(val, 'items'):
|
||||
nested_items = _get(val, 'items')
|
||||
elif _has_key(val, 'value') and isinstance(val, dict):
|
||||
inner_val = _get(val, 'value')
|
||||
if _has_key(val, 'use') and (_get(val, 'use') is False):
|
||||
use_wrapper = True
|
||||
if _has_key(val, 'items'):
|
||||
nested_items = _get(val, 'items')
|
||||
if use_wrapper:
|
||||
lines.append(f'{indent}\t<dcscor:use>false</dcscor:use>')
|
||||
lines.append(f'{indent}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>')
|
||||
|
||||
is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font'
|
||||
is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line')
|
||||
is_dict = isinstance(inner_val, dict)
|
||||
if is_line_dict:
|
||||
lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0
|
||||
lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false'
|
||||
ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None'
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Line" width="{lw}" gap="{lg}">')
|
||||
lines.append(f'{indent}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{esc_xml(ls)}</v8ui:style>')
|
||||
lines.append(f'{indent}\t</dcscor:value>')
|
||||
elif is_font_dict:
|
||||
attr_parts = []
|
||||
for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'):
|
||||
if attr_name in inner_val:
|
||||
av = inner_val[attr_name]
|
||||
if av is not None:
|
||||
attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"')
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>')
|
||||
elif is_dict:
|
||||
emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val)
|
||||
else:
|
||||
actual_val = str(inner_val)
|
||||
key_type_map = {
|
||||
'Размещение': 'dcscor:DataCompositionTextPlacementType',
|
||||
'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign',
|
||||
'ВертикальноеПоложение': 'v8ui:VerticalAlign',
|
||||
'ОриентацияТекста': 'xs:decimal',
|
||||
'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement',
|
||||
'ТипМакета': 'dcsset:DataCompositionGroupTemplateType',
|
||||
}
|
||||
key_type = key_type_map.get(key)
|
||||
if key_type:
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="{key_type}">{esc_xml(actual_val)}</dcscor:value>')
|
||||
elif re.match(r'^(style|web|win):', actual_val):
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>')
|
||||
elif actual_val == 'true' or actual_val == 'false':
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>')
|
||||
elif key == 'Текст' or key == 'Заголовок' or key == 'Формат':
|
||||
emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val)
|
||||
elif re.match(r'^-?\d+(\.\d+)?$', actual_val):
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:decimal">{actual_val}</dcscor:value>')
|
||||
elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы':
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>')
|
||||
else:
|
||||
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>')
|
||||
if nested_items:
|
||||
if isinstance(nested_items, dict):
|
||||
for nk, nv in nested_items.items():
|
||||
emit_appearance_value(lines, nk, nv, f'{indent}\t')
|
||||
lines.append(f'{indent}</dcscor:item>')
|
||||
|
||||
|
||||
def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
|
||||
has_items = bool(items) and len(items) > 0
|
||||
has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
|
||||
if not has_items and not has_block_meta:
|
||||
return
|
||||
lines.append(f'{indent}<dcsset:conditionalAppearance>')
|
||||
for ca in (items or []):
|
||||
lines.append(f'{indent}\t<dcsset:item>')
|
||||
if ca.get('use') is False:
|
||||
lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>')
|
||||
if ca.get('selection') and len(ca['selection']) > 0:
|
||||
lines.append(f'{indent}\t\t<dcsset:selection>')
|
||||
for sel in ca['selection']:
|
||||
lines.append(f'{indent}\t\t\t<dcsset:item>')
|
||||
lines.append(f'{indent}\t\t\t\t<dcsset:field>{esc_xml(str(sel))}</dcsset:field>')
|
||||
lines.append(f'{indent}\t\t\t</dcsset:item>')
|
||||
lines.append(f'{indent}\t\t</dcsset:selection>')
|
||||
else:
|
||||
lines.append(f'{indent}\t\t<dcsset:selection/>')
|
||||
if ca.get('filter') and len(ca['filter']) > 0:
|
||||
emit_filter(lines, ca['filter'], f'{indent}\t\t')
|
||||
else:
|
||||
lines.append(f'{indent}\t\t<dcsset:filter/>')
|
||||
if ca.get('appearance'):
|
||||
lines.append(f'{indent}\t\t<dcsset:appearance>')
|
||||
for k, v in ca['appearance'].items():
|
||||
emit_appearance_value(lines, k, v, f'{indent}\t\t\t')
|
||||
lines.append(f'{indent}\t\t</dcsset:appearance>')
|
||||
if ca.get('presentation'):
|
||||
if isinstance(ca['presentation'], dict):
|
||||
emit_mltext(lines, f'{indent}\t\t', 'dcsset:presentation', ca['presentation'])
|
||||
else:
|
||||
lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="xs:string">{esc_xml(str(ca["presentation"]))}</dcsset:presentation>')
|
||||
if ca.get('viewMode'):
|
||||
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(ca["viewMode"]))}</dcsset:viewMode>')
|
||||
if ca.get('userSettingID'):
|
||||
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'])
|
||||
if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0:
|
||||
use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header',
|
||||
'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader',
|
||||
'overallResourceFieldsHeader']
|
||||
sset = {str(n): True for n in ca['useInDontUse']}
|
||||
for n in use_in_order:
|
||||
if n in sset:
|
||||
tag = 'useIn' + n[0].upper() + n[1:]
|
||||
lines.append(f'{indent}\t\t<dcsset:{tag}>DontUse</dcsset:{tag}>')
|
||||
lines.append(f'{indent}\t</dcsset:item>')
|
||||
if block_view_mode is not None:
|
||||
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
|
||||
if block_user_setting_id is not None:
|
||||
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
|
||||
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
|
||||
lines.append(f'{indent}</dcsset:conditionalAppearance>')
|
||||
|
||||
|
||||
def write_utf8_bom(path, content):
|
||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(content)
|
||||
@@ -1440,10 +1845,25 @@ REF_ROOT_SYNONYMS = {
|
||||
"РегистрБухгалтерии": "AccountingRegister",
|
||||
"РегистрРасчета": "CalculationRegister",
|
||||
"РегистрРасчёта": "CalculationRegister",
|
||||
"ЖурналДокументов": "DocumentJournal",
|
||||
"КритерийОтбора": "FilterCriterion",
|
||||
}
|
||||
ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"}
|
||||
|
||||
|
||||
def normalize_meta_type_ref(ref):
|
||||
# "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений
|
||||
if not ref:
|
||||
return ref
|
||||
dot = ref.find('.')
|
||||
if dot < 1:
|
||||
return ref
|
||||
root = ref[:dot]
|
||||
if root in REF_ROOT_SYNONYMS:
|
||||
return REF_ROOT_SYNONYMS[root] + ref[dot:]
|
||||
return ref
|
||||
|
||||
|
||||
def normalize_choice_value(value):
|
||||
"""Returns dict {xsi_type, text} for a choiceList item value."""
|
||||
if isinstance(value, bool):
|
||||
@@ -2669,17 +3089,45 @@ def emit_attributes(lines, attrs, indent):
|
||||
lines.append(f'{inner}\t</Column>')
|
||||
lines.append(f'{inner}</Columns>')
|
||||
|
||||
# Settings (for DynamicList)
|
||||
# Settings (динамический список)
|
||||
if attr.get('settings'):
|
||||
s = attr['settings']
|
||||
lines.append(f'{inner}<Settings xsi:type="DynamicList">')
|
||||
si = f'{inner}\t'
|
||||
if s.get('mainTable'):
|
||||
lines.append(f'{si}<MainTable>{s["mainTable"]}</MainTable>')
|
||||
mq = 'true' if s.get('manualQuery') else 'false'
|
||||
# Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
has_query = bool(s.get('query') and str(s['query']).strip())
|
||||
mq = 'true' if (has_query or s.get('manualQuery')) else 'false'
|
||||
lines.append(f'{si}<ManualQuery>{mq}</ManualQuery>')
|
||||
ddr = 'true' if s.get('dynamicDataRead') else 'false'
|
||||
# DynamicDataRead: дефолт true; false только при явном отключении
|
||||
ddr = 'false' if s.get('dynamicDataRead') is False else 'true'
|
||||
lines.append(f'{si}<DynamicDataRead>{ddr}</DynamicDataRead>')
|
||||
if has_query:
|
||||
qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR)
|
||||
lines.append(f'{si}<QueryText>{esc_xml(qtext)}</QueryText>')
|
||||
# Явные поля набора (редко): override title/dataPath
|
||||
if s.get('fields'):
|
||||
for fld in s['fields']:
|
||||
lines.append(f'{si}<Field xsi:type="dcssch:DataSetFieldField">')
|
||||
dp = fld.get('dataPath') or fld.get('field')
|
||||
lines.append(f'{si}\t<dcssch:dataPath>{esc_xml(str(dp))}</dcssch:dataPath>')
|
||||
lines.append(f'{si}\t<dcssch:field>{esc_xml(str(fld.get("field", "")))}</dcssch:field>')
|
||||
if fld.get('title'):
|
||||
lines.append(f'{si}\t<dcssch:title xsi:type="v8:LocalStringType">')
|
||||
emit_ml_items(lines, f'{si}\t\t', fld['title'])
|
||||
lines.append(f'{si}\t</dcssch:title>')
|
||||
lines.append(f'{si}</Field>')
|
||||
if s.get('mainTable'):
|
||||
lines.append(f'{si}<MainTable>{normalize_meta_type_ref(str(s["mainTable"]))}</MainTable>')
|
||||
# ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
|
||||
# Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
|
||||
lsi = f'{si}\t'
|
||||
lines.append(f'{si}<ListSettings>')
|
||||
emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID)
|
||||
emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID)
|
||||
emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID)
|
||||
lines.append(f'{lsi}<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>')
|
||||
lines.append(f'{lsi}<dcsset:itemsUserSettingID>{CANON_ITEMS_ID}</dcsset:itemsUserSettingID>')
|
||||
lines.append(f'{si}</ListSettings>')
|
||||
lines.append(f'{inner}</Settings>')
|
||||
|
||||
lines.append(f'{indent}\t</Attribute>')
|
||||
@@ -3047,6 +3495,8 @@ def main():
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
||||
defn = json.load(f)
|
||||
global QUERY_BASE_DIR
|
||||
QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))
|
||||
|
||||
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
|
||||
def _normalize_synonyms(el):
|
||||
|
||||
Reference in New Issue
Block a user