mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
feat(skd-decompile): слои 2-3 — dataSources, dataSets, поля
- Распознавание типов (string/decimal/boolean/date/dateTime/time/CatalogRef.X
и пр.) с qualifiers (decimal-точность/знак, string Length/AllowedLength,
date fractions) → shorthand или composite-массив.
- Роли (@dimension/@account/@balance/@period) с детектом сложных roleAttributes
как sentinel.
- Restrictions (#noField/#noFilter/#noGroup/#noOrder) из useRestriction.
- Multilingual title с авто-сворачиванием {ru:"..."} в строку.
- appearance с поддержкой LocalStringType значений (например, Формат).
- presentationExpression.
- Свёртка дефолтного dataSource (ИсточникДанных1/Local) в умолчание.
- Автодетект object vs shorthand формы поля.
Bit-perfect round-trip на синтетике из 11 разнотипных полей.
Реальный ERP-отчёт АнализВерсийОбъектов декомпилируется с 0 warnings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# skd-decompile v0.1 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# skd-decompile v0.2 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -34,22 +34,27 @@ if ($root.LocalName -ne 'DataCompositionSchema') {
|
||||
|
||||
# --- 1. Namespace manager ---
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("dcs", "http://v8.1c.ru/8.1/data-composition-system/schema")
|
||||
$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common")
|
||||
$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core")
|
||||
$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings")
|
||||
$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/areatemplate")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
|
||||
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$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_COR = "http://v8.1c.ru/8.1/data-composition-system/core"
|
||||
$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_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
|
||||
$NS_XS = "http://www.w3.org/2001/XMLSchema"
|
||||
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
$NS_CFG = "http://v8.1c.ru/8.1/data/enterprise/current-config"
|
||||
|
||||
# Root may use default namespace = schema; XPath needs explicit prefix
|
||||
$rootNS = $root.NamespaceURI
|
||||
if (-not $rootNS) { $rootNS = "http://v8.1c.ru/8.1/data-composition-system/schema" }
|
||||
# Re-bind dcs to actual root namespace if different
|
||||
$ns.AddNamespace("r", $rootNS)
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("r", $NS_SCHEMA)
|
||||
$ns.AddNamespace("dcscom", $NS_COM)
|
||||
$ns.AddNamespace("dcscor", $NS_COR)
|
||||
$ns.AddNamespace("dcsset", $NS_SET)
|
||||
$ns.AddNamespace("dcsat", $NS_AT)
|
||||
$ns.AddNamespace("v8", $NS_V8)
|
||||
$ns.AddNamespace("v8ui", $NS_V8UI)
|
||||
$ns.AddNamespace("xs", $NS_XS)
|
||||
$ns.AddNamespace("xsi", $NS_XSI)
|
||||
|
||||
# --- 2. Warnings accumulator ---
|
||||
|
||||
@@ -70,7 +75,7 @@ function New-Sentinel {
|
||||
return [ordered]@{ '__unsupported__' = [ordered]@{ id = $id; kind = $kind; loc = $loc } }
|
||||
}
|
||||
|
||||
# --- 3. Extract: dataSets (query only, no fields yet) ---
|
||||
# --- 3. Helpers ---
|
||||
|
||||
function Get-Text {
|
||||
param($node, [string]$xpath)
|
||||
@@ -79,45 +84,290 @@ function Get-Text {
|
||||
if ($n) { return $n.InnerText } else { return $null }
|
||||
}
|
||||
|
||||
# Extract LocalStringType (multilingual title) → string (if only ru) or hashtable
|
||||
function Get-MLText {
|
||||
param($node)
|
||||
if (-not $node) { return $null }
|
||||
$items = $node.SelectNodes("v8:item", $ns)
|
||||
if ($items.Count -eq 0) { return $null }
|
||||
$dict = [ordered]@{}
|
||||
foreach ($it in $items) {
|
||||
$lang = Get-Text $it "v8:lang"
|
||||
$content = Get-Text $it "v8:content"
|
||||
if ($lang) { $dict[$lang] = if ($content) { $content } else { "" } }
|
||||
}
|
||||
if ($dict.Count -eq 1 -and $dict.Contains('ru')) { return $dict['ru'] }
|
||||
return $dict
|
||||
}
|
||||
|
||||
# Strip namespace prefix from xsi:type value (e.g. "dcsset:Foo" → "Foo")
|
||||
function Get-LocalXsiType {
|
||||
param($node)
|
||||
if (-not $node) { return $null }
|
||||
$t = $node.GetAttribute("type", $NS_XSI)
|
||||
if ($t -match ':(.+)$') { return $matches[1] }
|
||||
return $t
|
||||
}
|
||||
|
||||
# Convert one <v8:Type> element + sibling qualifiers → shorthand type string
|
||||
function Get-OneTypeShorthand {
|
||||
param($typeNode, $qualNumber, $qualString, $qualDate)
|
||||
$raw = $typeNode.InnerText.Trim()
|
||||
# Strip namespace prefix; check if it's d5p1: (config refs)
|
||||
$local = $raw
|
||||
if ($raw -match '^([^:]+):(.+)$') {
|
||||
$prefix = $matches[1]
|
||||
$local = $matches[2]
|
||||
# Resolve prefix → namespace URI
|
||||
$uri = $typeNode.GetNamespaceOfPrefix($prefix)
|
||||
if ($uri -eq $NS_CFG) {
|
||||
return $local # CatalogRef.X, DocumentRef.X, etc.
|
||||
}
|
||||
if ($uri -eq $NS_XS) {
|
||||
switch ($local) {
|
||||
'string' {
|
||||
if ($qualString) {
|
||||
$len = [int](Get-Text $qualString "v8:Length")
|
||||
$allowed = Get-Text $qualString "v8:AllowedLength"
|
||||
if ($len -eq 0) { return 'string' }
|
||||
if ($allowed -eq 'Fixed') { return "string($len,fix)" }
|
||||
return "string($len)"
|
||||
}
|
||||
return 'string'
|
||||
}
|
||||
'boolean' { return 'boolean' }
|
||||
'decimal' {
|
||||
if ($qualNumber) {
|
||||
$d = [int](Get-Text $qualNumber "v8:Digits")
|
||||
$f = [int](Get-Text $qualNumber "v8:FractionDigits")
|
||||
$sign = Get-Text $qualNumber "v8:AllowedSign"
|
||||
$signSuf = ''
|
||||
if ($sign -eq 'Nonnegative') { $signSuf = ',nonneg' }
|
||||
# defaults: 10,2,Any → "decimal"
|
||||
if ($d -eq 10 -and $f -eq 2 -and -not $signSuf) { return 'decimal' }
|
||||
if ($f -eq 0) { return "decimal($d$signSuf)" }
|
||||
return "decimal($d,$f$($signSuf -replace '^,',''))".Replace('decimal(', 'decimal(').Replace(',,',',')
|
||||
}
|
||||
return 'decimal'
|
||||
}
|
||||
'dateTime' {
|
||||
$frac = if ($qualDate) { Get-Text $qualDate "v8:DateFractions" } else { 'DateTime' }
|
||||
switch ($frac) {
|
||||
'Date' { return 'date' }
|
||||
'Time' { return 'time' }
|
||||
default { return 'dateTime' }
|
||||
}
|
||||
}
|
||||
default { return $local }
|
||||
}
|
||||
}
|
||||
if ($uri -eq $NS_V8) {
|
||||
# v8:StandardPeriod, etc.
|
||||
return $local
|
||||
}
|
||||
}
|
||||
return $local
|
||||
}
|
||||
|
||||
# valueType → string shorthand OR array of shorthands (composite)
|
||||
function Get-ValueTypeShorthand {
|
||||
param($valueTypeNode)
|
||||
if (-not $valueTypeNode) { return $null }
|
||||
$types = $valueTypeNode.SelectNodes("v8:Type", $ns)
|
||||
if ($types.Count -eq 0) { return $null }
|
||||
$qualN = $valueTypeNode.SelectSingleNode("v8:NumberQualifiers", $ns)
|
||||
$qualS = $valueTypeNode.SelectSingleNode("v8:StringQualifiers", $ns)
|
||||
$qualD = $valueTypeNode.SelectSingleNode("v8:DateQualifiers", $ns)
|
||||
$shorts = @()
|
||||
foreach ($t in $types) { $shorts += (Get-OneTypeShorthand -typeNode $t -qualNumber $qualN -qualString $qualS -qualDate $qualD) }
|
||||
if ($shorts.Count -eq 1) { return $shorts[0] }
|
||||
return ,$shorts
|
||||
}
|
||||
|
||||
# <role> → array of @tokens; if non-simple — null + sentinel via $script:roleSentinel
|
||||
function Get-RoleTokens {
|
||||
param($roleNode, [string]$loc)
|
||||
if (-not $roleNode) { return $null }
|
||||
$tokens = @()
|
||||
$hasComplex = $false
|
||||
foreach ($child in $roleNode.ChildNodes) {
|
||||
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
if ($child.NamespaceURI -ne $NS_COM) { $hasComplex = $true; continue }
|
||||
switch ($child.LocalName) {
|
||||
'dimension' { if ($child.InnerText -eq 'true') { $tokens += '@dimension' } }
|
||||
'account' { if ($child.InnerText -eq 'true') { $tokens += '@account' } }
|
||||
'balance' { if ($child.InnerText -eq 'true') { $tokens += '@balance' } }
|
||||
'periodNumber' {
|
||||
# Expect periodNumber=1 + periodType=Main → @period
|
||||
$pType = Get-Text $roleNode "dcscom:periodType"
|
||||
if ($child.InnerText -eq '1' -and $pType -eq 'Main') { $tokens += '@period' } else { $hasComplex = $true }
|
||||
}
|
||||
'periodType' { } # handled with periodNumber above
|
||||
default {
|
||||
$hasComplex = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($hasComplex) {
|
||||
# emit sentinel separately so caller can attach it to field obj
|
||||
$null = New-Sentinel -kind 'ComplexRole' -loc $loc -detail 'Роль с дополнительными атрибутами не сворачивается в @-флаг'
|
||||
}
|
||||
return $tokens
|
||||
}
|
||||
|
||||
# <useRestriction> → array of #tokens
|
||||
function Get-RestrictionTokens {
|
||||
param($urNode)
|
||||
if (-not $urNode) { return @() }
|
||||
$tokens = @()
|
||||
$map = @{ 'field' = '#noField'; 'condition' = '#noFilter'; 'group' = '#noGroup'; 'order' = '#noOrder' }
|
||||
foreach ($key in 'field','condition','group','order') {
|
||||
$v = Get-Text $urNode "r:$key"
|
||||
if ($v -eq 'true') { $tokens += $map[$key] }
|
||||
}
|
||||
return $tokens
|
||||
}
|
||||
|
||||
# <appearance> → hashtable {param: value}
|
||||
function Get-AppearanceDict {
|
||||
param($appNode)
|
||||
if (-not $appNode) { return $null }
|
||||
$dict = [ordered]@{}
|
||||
$items = $appNode.SelectNodes("dcscor:item", $ns)
|
||||
foreach ($it in $items) {
|
||||
$p = Get-Text $it "dcscor:parameter"
|
||||
$valNode = $it.SelectSingleNode("dcscor:value", $ns)
|
||||
if (-not $p -or -not $valNode) { continue }
|
||||
# Value can be xs:string, v8ui:HorizontalAlign, v8:LocalStringType, etc.
|
||||
$valType = Get-LocalXsiType $valNode
|
||||
if ($valType -eq 'LocalStringType') {
|
||||
$dict[$p] = Get-MLText $valNode
|
||||
} else {
|
||||
$dict[$p] = $valNode.InnerText
|
||||
}
|
||||
}
|
||||
return $dict
|
||||
}
|
||||
|
||||
# Build a field JSON entry (shorthand if possible, object form otherwise)
|
||||
function Build-Field {
|
||||
param($fieldNode, [string]$loc)
|
||||
$dataPath = Get-Text $fieldNode "r:dataPath"
|
||||
$fieldName = Get-Text $fieldNode "r:field"
|
||||
$titleNode = $fieldNode.SelectSingleNode("r:title", $ns)
|
||||
$title = Get-MLText $titleNode
|
||||
$valueTypeNode = $fieldNode.SelectSingleNode("r:valueType", $ns)
|
||||
$typeShort = Get-ValueTypeShorthand $valueTypeNode
|
||||
$roleTokens = Get-RoleTokens $fieldNode.SelectSingleNode("r:role", $ns) "$loc/role"
|
||||
$restrictTokens = Get-RestrictionTokens $fieldNode.SelectSingleNode("r:useRestriction", $ns)
|
||||
$appNode = $fieldNode.SelectSingleNode("r:appearance", $ns)
|
||||
$appearance = Get-AppearanceDict $appNode
|
||||
$presExpr = Get-Text $fieldNode "r:presentationExpression"
|
||||
|
||||
$needsObject = $title -or $appearance -or $presExpr -or ($typeShort -is [array])
|
||||
|
||||
if (-not $needsObject) {
|
||||
# shorthand: "Name: type @role #restrict"
|
||||
$parts = @($fieldName)
|
||||
if ($typeShort) { $parts[0] = "$fieldName`: $typeShort" }
|
||||
if ($roleTokens) { $parts[0] += ' ' + ($roleTokens -join ' ') }
|
||||
if ($restrictTokens) { $parts[0] += ' ' + ($restrictTokens -join ' ') }
|
||||
# dataPath ≠ field — fall back to object form
|
||||
if ($dataPath -and $dataPath -ne $fieldName) {
|
||||
# unusual case; use object form
|
||||
} else {
|
||||
return $parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
$obj = [ordered]@{ field = $fieldName }
|
||||
if ($dataPath -and $dataPath -ne $fieldName) { $obj['dataPath'] = $dataPath }
|
||||
if ($title) { $obj['title'] = $title }
|
||||
if ($typeShort) { $obj['type'] = $typeShort }
|
||||
if ($roleTokens) {
|
||||
if ($roleTokens.Count -eq 1) { $obj['role'] = $roleTokens[0] -replace '^@','' }
|
||||
else { $obj['role'] = ($roleTokens | ForEach-Object { $_ -replace '^@','' }) }
|
||||
}
|
||||
if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) }
|
||||
if ($presExpr) { $obj['presentationExpression'] = $presExpr }
|
||||
if ($appearance) { $obj['appearance'] = $appearance }
|
||||
return $obj
|
||||
}
|
||||
|
||||
# --- 4. dataSources ---
|
||||
|
||||
$dataSources = @()
|
||||
$dsourceNodes = $root.SelectNodes("r:dataSource", $ns)
|
||||
foreach ($dsn in $dsourceNodes) {
|
||||
$nm = Get-Text $dsn "r:name"
|
||||
$tp = Get-Text $dsn "r:dataSourceType"
|
||||
$dataSources += [ordered]@{ name = $nm; type = $tp }
|
||||
}
|
||||
# Default: single ИсточникДанных1/Local → omit from output
|
||||
$emitDataSources = $true
|
||||
if ($dataSources.Count -eq 1 -and $dataSources[0].name -eq 'ИсточникДанных1' -and $dataSources[0].type -eq 'Local') {
|
||||
$emitDataSources = $false
|
||||
}
|
||||
|
||||
# --- 5. dataSets ---
|
||||
|
||||
$dataSets = @()
|
||||
$dsNodes = $root.SelectNodes("r:dataSet", $ns)
|
||||
foreach ($dsNode in $dsNodes) {
|
||||
$xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$xsiType = Get-LocalXsiType $dsNode
|
||||
$name = Get-Text $dsNode "r:name"
|
||||
$ds = [ordered]@{ name = $name }
|
||||
|
||||
switch -Regex ($xsiType) {
|
||||
'DataSetQuery$' {
|
||||
$query = Get-Text $dsNode "r:query"
|
||||
# Decode XML entities — XmlDocument already decoded & → & in InnerText
|
||||
$ds['query'] = $query
|
||||
# fields — layer 3
|
||||
switch ($xsiType) {
|
||||
'DataSetQuery' {
|
||||
$ds['query'] = Get-Text $dsNode "r:query"
|
||||
}
|
||||
'DataSetObject$' {
|
||||
'DataSetObject' {
|
||||
$ds['objectName'] = Get-Text $dsNode "r:objectName"
|
||||
}
|
||||
'DataSetUnion$' {
|
||||
$ds['__unsupported__'] = (New-Sentinel -kind 'DataSetUnion' -loc "dataSet[name=$name]" -detail 'Реализуется в слое 15')['__unsupported__']
|
||||
'DataSetUnion' {
|
||||
$ds['__unsupported__'] = (New-Sentinel -kind 'DataSetUnion' -loc "dataSet[$name]" -detail 'Реализуется в слое 15')['__unsupported__']
|
||||
}
|
||||
default {
|
||||
$ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc "dataSet[name=$name]" -detail "Неизвестный тип набора данных")['__unsupported__']
|
||||
$ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc "dataSet[$name]" -detail "Неизвестный тип набора данных")['__unsupported__']
|
||||
}
|
||||
}
|
||||
|
||||
# Fields
|
||||
$fieldNodes = $dsNode.SelectNodes("r:field", $ns)
|
||||
if ($fieldNodes.Count -gt 0) {
|
||||
$fields = @()
|
||||
$fi = 0
|
||||
foreach ($fn in $fieldNodes) {
|
||||
$fxsi = Get-LocalXsiType $fn
|
||||
if ($fxsi -ne 'DataSetFieldField') {
|
||||
$fields += (New-Sentinel -kind "FieldType:$fxsi" -loc "dataSet[$name]/field[$fi]" -detail 'Тип поля не DataSetFieldField')
|
||||
} else {
|
||||
$fields += (Build-Field -fieldNode $fn -loc "dataSet[$name]/field[$fi]")
|
||||
}
|
||||
$fi++
|
||||
}
|
||||
$ds['fields'] = $fields
|
||||
}
|
||||
|
||||
# dataSource attachment — omit if matches default
|
||||
$dsSrc = Get-Text $dsNode "r:dataSource"
|
||||
if ($emitDataSources -and $dsSrc) { $ds['dataSource'] = $dsSrc }
|
||||
|
||||
$dataSets += $ds
|
||||
}
|
||||
|
||||
# --- 4. Build top-level JSON object ---
|
||||
# --- 6. Build top-level JSON object ---
|
||||
|
||||
$out = [ordered]@{
|
||||
dataSets = $dataSets
|
||||
}
|
||||
$out = [ordered]@{}
|
||||
if ($emitDataSources) { $out['dataSources'] = $dataSources }
|
||||
$out['dataSets'] = $dataSets
|
||||
|
||||
# --- 5. Serialize ---
|
||||
# --- 7. Serialize ---
|
||||
|
||||
$json = $out | ConvertTo-Json -Depth 32
|
||||
|
||||
# Unescape \uXXXX → UTF-8 literals (PS 5.1 ConvertTo-Json escapes non-ASCII)
|
||||
# Unescape \uXXXX → UTF-8 literals
|
||||
$json = [regex]::Replace($json, '\\u([0-9a-fA-F]{4})', {
|
||||
param($m)
|
||||
[char][int]("0x" + $m.Groups[1].Value)
|
||||
@@ -130,7 +380,6 @@ if ($OutputPath) {
|
||||
$enc = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($OutputPath, $json, $enc)
|
||||
|
||||
# Write warnings.md alongside, if any
|
||||
if ($script:warnings.Count -gt 0) {
|
||||
$wPath = [System.IO.Path]::ChangeExtension($OutputPath, $null).TrimEnd('.') + '.warnings.md'
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
@@ -142,12 +391,10 @@ if ($OutputPath) {
|
||||
[void]$sb.AppendLine("- **$($w.id)** ($($w.kind)) at `$($w.loc)`: $($w.detail)")
|
||||
}
|
||||
[System.IO.File]::WriteAllText($wPath, $sb.ToString(), $enc)
|
||||
Write-Host "Warnings written: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow
|
||||
Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Summary to stderr
|
||||
$stats = "dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)"
|
||||
[Console]::Error.WriteLine("Decompiled: $stats")
|
||||
[Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), warnings=$($script:warnings.Count)")
|
||||
} else {
|
||||
Write-Output $json
|
||||
if ($script:warnings.Count -gt 0) {
|
||||
|
||||
Reference in New Issue
Block a user