diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index 3b2162ee..b37097a2 100644 --- a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 @@ -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 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 +} + +# → 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 +} + +# → 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 +} + +# → 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) {