mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-02 01:37:45 +03:00
feat(skd-decompile): слои 6-9 — templates (cells, merge, style, drilldown)
- Парсинг <template>/<template xsi:type=AreaTemplate>/<dcsat:TableRow>/
<dcsat:tableCell> в rows[][].
- Распознавание содержимого ячейки: dcsat:Text → строка, dcsat:Field с
dcscor:Parameter → "{Имя}", dcsat:Field с LocalStringType → строка/multilang,
пустая → null.
- Merge через appearance-флаги ОбъединятьПоВертикали/ОбъединятьПоГоризонтали
на пустых ячейках → "|"/">".
- Детект built-in стилей (header/data/subheader/total) через нормализованный
fingerprint appearance — без сравнения per-cell ширин/высот/merge-флагов.
При несовпадении или неоднородности — sentinel TemplateStyleMismatch.
- Извлечение widths из appearance первого row и minHeight из первой ячейки.
- Drilldown-свёртка: для cells с appearance Расшифровка=Расшифровка_X
и template-параметром DetailsAreaTemplateParameter Расшифровка_X →
свертываем в `{name, expression, drilldown: X}`.
- Сохранение порядка template parameters через [ordered]@{}.
- Fix namespace URI для areatemplate (`area-template` с дефисом).
Bit-perfect round-trip 55924→55924 и 28590→28590 на синтетике с header/data
стилями, merge, drilldown, шаблонными параметрами.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
be69bc231c
commit
61c4bd418d
@@ -1,4 +1,4 @@
|
|||||||
# skd-decompile v0.3 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
# skd-decompile v0.4 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -38,7 +38,7 @@ $NS_SCHEMA = "http://v8.1c.ru/8.1/data-composition-system/schema"
|
|||||||
$NS_COM = "http://v8.1c.ru/8.1/data-composition-system/common"
|
$NS_COM = "http://v8.1c.ru/8.1/data-composition-system/common"
|
||||||
$NS_COR = "http://v8.1c.ru/8.1/data-composition-system/core"
|
$NS_COR = "http://v8.1c.ru/8.1/data-composition-system/core"
|
||||||
$NS_SET = "http://v8.1c.ru/8.1/data-composition-system/settings"
|
$NS_SET = "http://v8.1c.ru/8.1/data-composition-system/settings"
|
||||||
$NS_AT = "http://v8.1c.ru/8.1/data-composition-system/areatemplate"
|
$NS_AT = "http://v8.1c.ru/8.1/data-composition-system/area-template"
|
||||||
$NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
$NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||||
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
|
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
|
||||||
$NS_XS = "http://www.w3.org/2001/XMLSchema"
|
$NS_XS = "http://www.w3.org/2001/XMLSchema"
|
||||||
@@ -492,6 +492,302 @@ function Render-Parameter {
|
|||||||
return $obj
|
return $obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 3b. Built-in style fingerprints ---
|
||||||
|
|
||||||
|
# Fingerprints for built-in styles (header/data/subheader/total).
|
||||||
|
# Each is the set of "shape-defining" appearance items the style emits.
|
||||||
|
# Width/Height/merge flags are excluded — they're per-cell.
|
||||||
|
$script:builtinStyleFingerprints = @{
|
||||||
|
'header' = @{
|
||||||
|
ЦветФона = 'd8p1:ReportHeaderBackColor'
|
||||||
|
ЦветГраницы = 'd8p1:ReportLineColor'
|
||||||
|
СтильГраницы = 'None|0'
|
||||||
|
'СтильГраницы.Слева' = 'Solid|1'
|
||||||
|
'СтильГраницы.Сверху' = 'Solid|1'
|
||||||
|
'СтильГраницы.Справа' = 'Solid|1'
|
||||||
|
'СтильГраницы.Снизу' = 'Solid|1'
|
||||||
|
Шрифт = 'Arial|10|false|false|false|false'
|
||||||
|
ГоризонтальноеПоложение = 'Center'
|
||||||
|
Размещение = 'Wrap'
|
||||||
|
}
|
||||||
|
'data' = @{
|
||||||
|
ЦветФона = 'd8p1:ReportGroup1BackColor'
|
||||||
|
ЦветГраницы = 'd8p1:ReportLineColor'
|
||||||
|
СтильГраницы = 'None|0'
|
||||||
|
'СтильГраницы.Слева' = 'Solid|1'
|
||||||
|
'СтильГраницы.Сверху' = 'Solid|1'
|
||||||
|
'СтильГраницы.Справа' = 'Solid|1'
|
||||||
|
'СтильГраницы.Снизу' = 'Solid|1'
|
||||||
|
Шрифт = 'Arial|10|false|false|false|false'
|
||||||
|
}
|
||||||
|
'subheader' = @{
|
||||||
|
ЦветГраницы = 'd8p1:ReportLineColor'
|
||||||
|
СтильГраницы = 'None|0'
|
||||||
|
'СтильГраницы.Слева' = 'Solid|1'
|
||||||
|
'СтильГраницы.Сверху' = 'Solid|1'
|
||||||
|
'СтильГраницы.Справа' = 'Solid|1'
|
||||||
|
'СтильГраницы.Снизу' = 'Solid|1'
|
||||||
|
Шрифт = 'Arial|10|false|false|false|false'
|
||||||
|
ГоризонтальноеПоложение = 'Center'
|
||||||
|
}
|
||||||
|
'total' = @{
|
||||||
|
ЦветГраницы = 'd8p1:ReportLineColor'
|
||||||
|
СтильГраницы = 'None|0'
|
||||||
|
'СтильГраницы.Слева' = 'Solid|1'
|
||||||
|
'СтильГраницы.Сверху' = 'Solid|1'
|
||||||
|
'СтильГраницы.Справа' = 'Solid|1'
|
||||||
|
'СтильГраницы.Снизу' = 'Solid|1'
|
||||||
|
Шрифт = 'Arial|10|false|false|false|false'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Normalize an <dcsat:appearance> node to a hashtable of "shape" keys (excluding
|
||||||
|
# per-cell items: widths, heights, merge flags, drilldown). Used for style matching.
|
||||||
|
function Get-AppearanceFingerprint {
|
||||||
|
param($appNode)
|
||||||
|
$fp = @{}
|
||||||
|
if (-not $appNode) { return $fp }
|
||||||
|
# Top-level dcscor:item children
|
||||||
|
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
|
||||||
|
$pName = Get-Text $it "dcscor:parameter"
|
||||||
|
$val = $it.SelectSingleNode("dcscor:value", $ns)
|
||||||
|
if (-not $pName -or -not $val) { continue }
|
||||||
|
# Skip per-cell keys
|
||||||
|
if ($pName -in @('МинимальнаяШирина','МаксимальнаяШирина','МинимальнаяВысота','ОбъединятьПоВертикали','ОбъединятьПоГоризонтали','Расшифровка')) { continue }
|
||||||
|
$valType = Get-LocalXsiType $val
|
||||||
|
switch ($valType) {
|
||||||
|
'Color' { $fp[$pName] = $val.InnerText }
|
||||||
|
'Line' {
|
||||||
|
$w = $val.GetAttribute("width")
|
||||||
|
$styleNode = $val.SelectSingleNode("v8ui:style", $ns)
|
||||||
|
$lineStyle = if ($styleNode) { $styleNode.InnerText } else { '' }
|
||||||
|
$fp[$pName] = "$lineStyle|$w"
|
||||||
|
}
|
||||||
|
'Font' {
|
||||||
|
$face = $val.GetAttribute("faceName")
|
||||||
|
$h = $val.GetAttribute("height")
|
||||||
|
$b = $val.GetAttribute("bold")
|
||||||
|
$i = $val.GetAttribute("italic")
|
||||||
|
$u = $val.GetAttribute("underline")
|
||||||
|
$s = $val.GetAttribute("strikeout")
|
||||||
|
$fp[$pName] = "$face|$h|$b|$i|$u|$s"
|
||||||
|
}
|
||||||
|
default { $fp[$pName] = $val.InnerText }
|
||||||
|
}
|
||||||
|
# Nested sub-items under СтильГраницы (left/top/right/bottom)
|
||||||
|
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
|
||||||
|
$subName = Get-Text $sub "dcscor:parameter"
|
||||||
|
$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
|
||||||
|
if (-not $subName -or -not $subVal) { continue }
|
||||||
|
$subType = Get-LocalXsiType $subVal
|
||||||
|
if ($subType -eq 'Line') {
|
||||||
|
$w = $subVal.GetAttribute("width")
|
||||||
|
$styleNode = $subVal.SelectSingleNode("v8ui:style", $ns)
|
||||||
|
$lineStyle = if ($styleNode) { $styleNode.InnerText } else { '' }
|
||||||
|
$fp[$subName] = "$lineStyle|$w"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Translate d8p1: colors to canonical "d8p1:Name" form (InnerText already contains prefix)
|
||||||
|
return $fp
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if appearance fingerprint matches a built-in style.
|
||||||
|
# Returns style name or $null.
|
||||||
|
function Match-BuiltinStyle {
|
||||||
|
param($fp)
|
||||||
|
foreach ($styleName in $script:builtinStyleFingerprints.Keys) {
|
||||||
|
$expected = $script:builtinStyleFingerprints[$styleName]
|
||||||
|
$match = $true
|
||||||
|
# All expected keys must be present with matching value
|
||||||
|
foreach ($k in $expected.Keys) {
|
||||||
|
if (-not $fp.ContainsKey($k) -or $fp[$k] -ne $expected[$k]) { $match = $false; break }
|
||||||
|
}
|
||||||
|
if (-not $match) { continue }
|
||||||
|
# Must not have extra keys that aren't in the expected fingerprint
|
||||||
|
$extras = @($fp.Keys | Where-Object { -not $expected.ContainsKey($_) })
|
||||||
|
if ($extras.Count -ne 0) { continue }
|
||||||
|
return $styleName
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract per-cell width/minHeight/merge from appearance.
|
||||||
|
function Get-CellPerCellAttrs {
|
||||||
|
param($appNode)
|
||||||
|
$attrs = @{ width = $null; height = $null; mergeV = $false; mergeH = $false; drilldown = $null }
|
||||||
|
if (-not $appNode) { return $attrs }
|
||||||
|
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
|
||||||
|
$pName = Get-Text $it "dcscor:parameter"
|
||||||
|
$val = $it.SelectSingleNode("dcscor:value", $ns)
|
||||||
|
if (-not $pName) { continue }
|
||||||
|
switch ($pName) {
|
||||||
|
'МинимальнаяШирина' { if ($val) { $attrs.width = $val.InnerText } }
|
||||||
|
'МинимальнаяВысота' { if ($val) { $attrs.height = $val.InnerText } }
|
||||||
|
'ОбъединятьПоВертикали' { if ($val -and $val.InnerText -eq 'true') { $attrs.mergeV = $true } }
|
||||||
|
'ОбъединятьПоГоризонтали' { if ($val -and $val.InnerText -eq 'true') { $attrs.mergeH = $true } }
|
||||||
|
'Расшифровка' {
|
||||||
|
# value xsi:type=dcscor:Parameter pointing to Расшифровка_X
|
||||||
|
if ($val) {
|
||||||
|
$paramRef = $val.InnerText
|
||||||
|
if ($paramRef -match '^Расшифровка_(.+)$') { $attrs.drilldown = $matches[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract cell content: string text, "{ParamName}", "|", ">", or $null
|
||||||
|
function Get-CellContent {
|
||||||
|
param($cellNode, $perCellAttrs)
|
||||||
|
# Check merge flags first — empty cells with these flags are "|" or ">"
|
||||||
|
if ($perCellAttrs.mergeV) { return '|' }
|
||||||
|
if ($perCellAttrs.mergeH) { return '>' }
|
||||||
|
|
||||||
|
$item = $cellNode.SelectSingleNode("dcsat:item", $ns)
|
||||||
|
if (-not $item) { return $null }
|
||||||
|
$itemType = Get-LocalXsiType $item
|
||||||
|
$valNode = $item.SelectSingleNode("dcsat:value", $ns)
|
||||||
|
if (-not $valNode) { return $null }
|
||||||
|
$valType = Get-LocalXsiType $valNode
|
||||||
|
|
||||||
|
if ($itemType -eq 'Field' -and $valType -eq 'Parameter') {
|
||||||
|
return '{' + $valNode.InnerText + '}'
|
||||||
|
}
|
||||||
|
if ($valType -eq 'LocalStringType') {
|
||||||
|
$text = Get-MLText $valNode
|
||||||
|
if ($text -is [System.Collections.IDictionary]) {
|
||||||
|
# multilang in template cell — keep as-is; emit via object form (Ring 2 candidate)
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
# Fallback: take inner text
|
||||||
|
return $valNode.InnerText
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build template parameter entry. Returns hashtable with `name` + `expression` (+ optional `drilldown`)
|
||||||
|
function Build-TemplateParameter {
|
||||||
|
param($pNode)
|
||||||
|
$pType = Get-LocalXsiType $pNode
|
||||||
|
$obj = [ordered]@{}
|
||||||
|
$obj['name'] = Get-Text $pNode "dcsat:name"
|
||||||
|
if ($pType -eq 'ExpressionAreaTemplateParameter') {
|
||||||
|
$obj['expression'] = Get-Text $pNode "dcsat:expression"
|
||||||
|
} elseif ($pType -eq 'DetailsAreaTemplateParameter') {
|
||||||
|
# Marker — handled by drilldown folding logic in Build-Template
|
||||||
|
$obj['__details__'] = $true
|
||||||
|
$obj['expression'] = Get-Text $pNode "dcsat:expression"
|
||||||
|
}
|
||||||
|
return $obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build template entry from <template> node
|
||||||
|
function Build-Template {
|
||||||
|
param($templateNode, [string]$loc)
|
||||||
|
$tmplObj = [ordered]@{ name = Get-Text $templateNode "r:name" }
|
||||||
|
$inner = $templateNode.SelectSingleNode("r:template", $ns)
|
||||||
|
if (-not $inner) { return $tmplObj }
|
||||||
|
|
||||||
|
# Walk rows
|
||||||
|
$rowNodes = $inner.SelectNodes("dcsat:item[@xsi:type='dcsat:TableRow']", $ns)
|
||||||
|
# fallback: any dcsat:item (in case xsi prefix differs)
|
||||||
|
if ($rowNodes.Count -eq 0) {
|
||||||
|
$allItems = $inner.SelectNodes("dcsat:item", $ns)
|
||||||
|
$rowNodes = @()
|
||||||
|
foreach ($n in $allItems) { if ((Get-LocalXsiType $n) -eq 'TableRow') { $rowNodes += $n } }
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = @()
|
||||||
|
$widths = $null
|
||||||
|
$minHeight = $null
|
||||||
|
$detectedStyle = $null
|
||||||
|
$styleMismatch = $false
|
||||||
|
$drilldownByParam = @{} # param name → field name (X from Расшифровка_X)
|
||||||
|
|
||||||
|
$rowIdx = 0
|
||||||
|
foreach ($rowNode in $rowNodes) {
|
||||||
|
$cells = @()
|
||||||
|
$cellNodes = $rowNode.SelectNodes("dcsat:tableCell", $ns)
|
||||||
|
$colIdx = 0
|
||||||
|
# First-row collects widths
|
||||||
|
$rowWidths = @()
|
||||||
|
foreach ($cellNode in $cellNodes) {
|
||||||
|
$appNode = $cellNode.SelectSingleNode("dcsat:appearance", $ns)
|
||||||
|
$perCell = Get-CellPerCellAttrs $appNode
|
||||||
|
$content = Get-CellContent $cellNode $perCell
|
||||||
|
|
||||||
|
# Style detection (skip empty cells with no appearance, and merge cells)
|
||||||
|
if ($appNode -and -not $perCell.mergeV -and -not $perCell.mergeH) {
|
||||||
|
$fp = Get-AppearanceFingerprint $appNode
|
||||||
|
$matched = Match-BuiltinStyle $fp
|
||||||
|
if ($null -eq $detectedStyle) {
|
||||||
|
$detectedStyle = $matched
|
||||||
|
} elseif ($matched -ne $detectedStyle) {
|
||||||
|
$styleMismatch = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drilldown attachment
|
||||||
|
if ($content -match '^\{(.+)\}$' -and $perCell.drilldown) {
|
||||||
|
$drilldownByParam[$matches[1]] = $perCell.drilldown
|
||||||
|
}
|
||||||
|
|
||||||
|
# First row collects widths from any non-merge cell
|
||||||
|
if ($rowIdx -eq 0 -and $perCell.width) { $rowWidths += $perCell.width }
|
||||||
|
# First row collects minHeight from the first non-empty cell
|
||||||
|
if ($rowIdx -eq 0 -and $colIdx -eq 0 -and $perCell.height) { $minHeight = $perCell.height }
|
||||||
|
|
||||||
|
$cells += $content
|
||||||
|
$colIdx++
|
||||||
|
}
|
||||||
|
if ($rowIdx -eq 0 -and $rowWidths.Count -gt 0) { $widths = $rowWidths }
|
||||||
|
$rows += ,$cells
|
||||||
|
$rowIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
# Template parameters (and drilldown folding)
|
||||||
|
$paramNodes = $templateNode.SelectNodes("r:parameter", $ns)
|
||||||
|
$exprParams = [ordered]@{}
|
||||||
|
$detailParams = @{}
|
||||||
|
foreach ($pn in $paramNodes) {
|
||||||
|
$pType = Get-LocalXsiType $pn
|
||||||
|
$pName = Get-Text $pn "dcsat:name"
|
||||||
|
if ($pType -eq 'ExpressionAreaTemplateParameter') {
|
||||||
|
$exprParams[$pName] = Get-Text $pn "dcsat:expression"
|
||||||
|
} elseif ($pType -eq 'DetailsAreaTemplateParameter') {
|
||||||
|
# Name format: Расшифровка_<X>
|
||||||
|
if ($pName -match '^Расшифровка_(.+)$') {
|
||||||
|
$detailParams[$matches[1]] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateParams = @()
|
||||||
|
foreach ($pname in $exprParams.Keys) {
|
||||||
|
$entry = [ordered]@{ name = $pname; expression = $exprParams[$pname] }
|
||||||
|
if ($drilldownByParam.ContainsKey($pname)) {
|
||||||
|
$entry['drilldown'] = $drilldownByParam[$pname]
|
||||||
|
}
|
||||||
|
$templateParams += $entry
|
||||||
|
}
|
||||||
|
|
||||||
|
# Decide output form
|
||||||
|
if ($detectedStyle -and -not $styleMismatch) {
|
||||||
|
$tmplObj['style'] = $detectedStyle
|
||||||
|
} elseif ($styleMismatch -or ($null -eq $detectedStyle -and $rows.Count -gt 0)) {
|
||||||
|
# Couldn't unify style — emit sentinel
|
||||||
|
$tmplObj['__unsupported__'] = (New-Sentinel -kind 'TemplateStyleMismatch' -loc $loc -detail 'Шаблон содержит ячейки с непокрытым/неоднородным оформлением (Кольцо 2)')['__unsupported__']
|
||||||
|
}
|
||||||
|
if ($widths) { $tmplObj['widths'] = $widths }
|
||||||
|
if ($minHeight) { $tmplObj['minHeight'] = $minHeight }
|
||||||
|
$tmplObj['rows'] = $rows
|
||||||
|
if ($templateParams.Count -gt 0) { $tmplObj['parameters'] = $templateParams }
|
||||||
|
|
||||||
|
return $tmplObj
|
||||||
|
}
|
||||||
|
|
||||||
# --- 4. dataSources ---
|
# --- 4. dataSources ---
|
||||||
|
|
||||||
$dataSources = @()
|
$dataSources = @()
|
||||||
@@ -622,6 +918,17 @@ if ($calculatedFields.Count -gt 0) { $out['calculatedFields'] = $calculatedField
|
|||||||
if ($totalFields.Count -gt 0) { $out['totalFields'] = $totalFields }
|
if ($totalFields.Count -gt 0) { $out['totalFields'] = $totalFields }
|
||||||
if ($parameters.Count -gt 0) { $out['parameters'] = $parameters }
|
if ($parameters.Count -gt 0) { $out['parameters'] = $parameters }
|
||||||
|
|
||||||
|
# --- 5e. templates ---
|
||||||
|
|
||||||
|
$templates = @()
|
||||||
|
$tNodes = $root.SelectNodes("r:template", $ns)
|
||||||
|
$ti = 0
|
||||||
|
foreach ($tn in $tNodes) {
|
||||||
|
$templates += (Build-Template -templateNode $tn -loc "template[$ti]")
|
||||||
|
$ti++
|
||||||
|
}
|
||||||
|
if ($templates.Count -gt 0) { $out['templates'] = $templates }
|
||||||
|
|
||||||
# --- 7. Serialize ---
|
# --- 7. Serialize ---
|
||||||
|
|
||||||
$json = $out | ConvertTo-Json -Depth 32
|
$json = $out | ConvertTo-Json -Depth 32
|
||||||
@@ -653,7 +960,7 @@ if ($OutputPath) {
|
|||||||
Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow
|
Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
[Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), calc=$($calculatedFields.Count), totals=$($totalFields.Count), params=$($parameters.Count), warnings=$($script:warnings.Count)")
|
[Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), calc=$($calculatedFields.Count), totals=$($totalFields.Count), params=$($parameters.Count), templates=$($templates.Count), warnings=$($script:warnings.Count)")
|
||||||
} else {
|
} else {
|
||||||
Write-Output $json
|
Write-Output $json
|
||||||
if ($script:warnings.Count -gt 0) {
|
if ($script:warnings.Count -gt 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user