# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] [Alias('Path')] [string]$TemplatePath, [string]$OutputPath ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- 1. Load and parse XML --- if (-not (Test-Path $TemplatePath)) { Write-Error "File not found: $TemplatePath" exit 1 } $xmlDoc = New-Object System.Xml.XmlDocument $xmlDoc.PreserveWhitespace = $false $xmlDoc.Load((Resolve-Path $TemplatePath).Path) $root = $xmlDoc.DocumentElement $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet") $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") $ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") # --- 2. Extract font palette --- $rawFonts = @() foreach ($fNode in $root.SelectNodes("d:font", $ns)) { $rawFonts += @{ Face = $fNode.GetAttribute("faceName") Size = [int]$fNode.GetAttribute("height") Bold = $fNode.GetAttribute("bold") -eq "true" Italic = $fNode.GetAttribute("italic") -eq "true" Underline = $fNode.GetAttribute("underline") -eq "true" Strikeout = $fNode.GetAttribute("strikeout") -eq "true" } } # --- 3. Extract line palette --- $rawLines = @() foreach ($lNode in $root.SelectNodes("d:line", $ns)) { $rawLines += @{ Width = [int]$lNode.GetAttribute("width") } } # --- 4. Extract format palette --- $rawFormats = @() foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { $fmt = @{ FontIdx = -1 LB = -1; TB = -1; RB = -1; BB = -1 Width = 0; Height = 0 HA = ""; VA = "" Wrap = $false; FillType = ""; DataFormat = "" } $n = $fmtNode.SelectSingleNode("d:font", $ns) if ($n) { $fmt.FontIdx = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:leftBorder", $ns) if ($n) { $fmt.LB = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:topBorder", $ns) if ($n) { $fmt.TB = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:rightBorder", $ns) if ($n) { $fmt.RB = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns) if ($n) { $fmt.BB = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:width", $ns) if ($n) { $fmt.Width = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:height", $ns) if ($n) { $fmt.Height = [int]$n.InnerText } $n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns) if ($n) { $fmt.HA = $n.InnerText } $n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns) if ($n) { $fmt.VA = $n.InnerText } $n = $fmtNode.SelectSingleNode("d:textPlacement", $ns) if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true } $n = $fmtNode.SelectSingleNode("d:fillType", $ns) if ($n) { $fmt.FillType = $n.InnerText } $n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns) if ($n) { $fmt.DataFormat = $n.InnerText } $rawFormats += $fmt } function Get-Format { param([int]$idx) if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null } return $rawFormats[$idx - 1] } # --- 5. Extract columns and default width --- $colNode = $root.SelectSingleNode("d:columns", $ns) $totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText $colFormatIndices = @{} foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) { $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText $colFormatIndices[$colIdx] = $fmtIdx } $defaultFmtIdx = 0 $n = $root.SelectSingleNode("d:defaultFormatIndex", $ns) if ($n) { $defaultFmtIdx = [int]$n.InnerText } $defaultWidth = 10 if ($defaultFmtIdx -gt 0) { $defFmt = Get-Format $defaultFmtIdx if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width } } # Build column width map (1-based col → width), only non-default $colWidthMap = [ordered]@{} foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) { $fmt = Get-Format $colFormatIndices[$col0] if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { $col1 = [string]($col0 + 1) $colWidthMap.Add($col1, $fmt.Width) } } # --- 6. Extract merges --- $mergeMap = @{} foreach ($mNode in $root.SelectNodes("d:merge", $ns)) { $r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText $c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText $w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText $hNode = $mNode.SelectSingleNode("d:h", $ns) $h = if ($hNode) { [int]$hNode.InnerText } else { 0 } $mergeMap["$r,$c"] = @{ W = $w; H = $h } } # --- 7. Extract named items --- $namedAreas = @() foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) { $xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") if ($xsiType -ne "NamedItemCells") { continue } $areaNode = $niNode.SelectSingleNode("d:area", $ns) $areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText if ($areaType -ne "Rows") { continue } $namedAreas += @{ Name = $niNode.SelectSingleNode("d:name", $ns).InnerText BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText } } # --- 8. Extract rows --- $rowData = @{} foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { $rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText $rowNode = $riNode.SelectSingleNode("d:row", $ns) $indexTo = $rowIdx $itNode = $riNode.SelectSingleNode("d:indexTo", $ns) if ($itNode) { $indexTo = [int]$itNode.InnerText } $rowFmtIdx = 0 $fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns) if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText } $isEmpty = $false $emptyNode = $rowNode.SelectSingleNode("d:empty", $ns) if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true } $cells = @() if (-not $isEmpty) { $col = -1 foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) { $iNode = $cGroup.SelectSingleNode("d:i", $ns) if ($iNode) { $col = [int]$iNode.InnerText } else { $col++ } $cContent = $cGroup.SelectSingleNode("d:c", $ns) if (-not $cContent) { continue } $cellFmtIdx = 0 $fNode = $cContent.SelectSingleNode("d:f", $ns) if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText } $param = $null $pNode = $cContent.SelectSingleNode("d:parameter", $ns) if ($pNode) { $param = $pNode.InnerText } $detail = $null $dNode = $cContent.SelectSingleNode("d:detailParameter", $ns) if ($dNode) { $detail = $dNode.InnerText } $text = $null $tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns) if ($tNode) { $text = $tNode.InnerText } $cells += @{ Col = $col FormatIdx = $cellFmtIdx Param = $param Detail = $detail Text = $text } } } for ($r = $rowIdx; $r -le $indexTo; $r++) { $rowData[$r] = @{ FormatIdx = $rowFmtIdx Cells = $cells Empty = $isEmpty } } } # --- 9. Build style key (ignoring fillType) --- function Get-BorderDesc { param($fmt) if (-not $fmt) { return @{ Border = "none"; Thick = $false } } $lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0 $rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0 if (-not $lb -and -not $tb -and -not $rb -and -not $bb) { return @{ Border = "none"; Thick = $false } } $thick = $false foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) { if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) { $thick = $true; break } } if ($lb -and $tb -and $rb -and $bb) { return @{ Border = "all"; Thick = $thick } } $sides = @() if ($tb) { $sides += "top" } if ($bb) { $sides += "bottom" } if ($lb) { $sides += "left" } if ($rb) { $sides += "right" } return @{ Border = ($sides -join ","); Thick = $thick } } function Get-StyleKey { param($fmt) if (-not $fmt) { return "empty" } $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } $bd = Get-BorderDesc $fmt return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)" } # --- 10. Name fonts --- $fontNames = @{} $fontDefs = [ordered]@{} if ($rawFonts.Count -gt 0) { $fontNames[0] = "default" $fontDefs["default"] = $rawFonts[0] } function Get-FontKey { param($f) return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)" } $fontKeyMap = @{} $fontKeyMap[(Get-FontKey $rawFonts[0])] = "default" for ($i = 1; $i -lt $rawFonts.Count; $i++) { $f = $rawFonts[$i] $df = $rawFonts[0] # Dedup: if identical font already named, reuse $fKey = Get-FontKey $f if ($fontKeyMap.ContainsKey($fKey)) { $fontNames[$i] = $fontKeyMap[$fKey] continue } $name = $null if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) { if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) { $name = "bold" } elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) { $name = "italic" } elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) { $name = "underline" } } elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) { $name = "header" } elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) { $name = "small" } if (-not $name) { $parts = @() if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() } $parts += "$($f.Size)" if ($f.Bold) { $parts += "bold" } if ($f.Italic) { $parts += "italic" } if ($f.Underline) { $parts += "underline" } if ($f.Strikeout) { $parts += "strikeout" } $name = $parts -join "-" } $baseName = $name; $suffix = 2 while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } $fontNames[$i] = $name $fontDefs[$name] = $f $fontKeyMap[$fKey] = $name } # --- 11. Collect and name styles --- $styleKeys = [ordered]@{} $formatToStyleKey = @{} foreach ($r in $rowData.Values) { foreach ($cell in $r.Cells) { $fmt = Get-Format $cell.FormatIdx if (-not $fmt) { continue } $key = Get-StyleKey $fmt if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } $formatToStyleKey[$cell.FormatIdx] = $key } } function Name-Style { param($fmt) if (-not $fmt) { return "default" } $parts = @() $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { $parts += $fontNames[$fi] } $bd = Get-BorderDesc $fmt if ($bd.Border -ne "none") { if ($bd.Border -eq "all") { $parts += "bordered" } else { $parts += "border-$($bd.Border)" } } if ($fmt.HA -eq "Center") { $parts += "center" } elseif ($fmt.HA -eq "Right") { $parts += "right" } if ($fmt.VA -eq "Center") { $parts += "vcenter" } elseif ($fmt.VA -eq "Top") { $parts += "vtop" } if ($fmt.Wrap) { $parts += "wrap" } if ($fmt.DataFormat) { $parts += "fmt" } if ($parts.Count -eq 0) { return "default" } return ($parts -join "-") } $styleNames = [ordered]@{} $styleDefs = [ordered]@{} foreach ($key in $styleKeys.Keys) { $fmt = $styleKeys[$key] $name = Name-Style $fmt $baseName = $name; $suffix = 2 while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ } $styleNames[$key] = $name $sDef = [ordered]@{} $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { $sDef["font"] = $fontNames[$fi] } if ($fmt.HA) { $a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } } if ($a) { $sDef["align"] = $a } } if ($fmt.VA) { $a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } } if ($a) { $sDef["valign"] = $a } } $bd = Get-BorderDesc $fmt if ($bd.Border -ne "none") { $sDef["border"] = $bd.Border if ($bd.Thick) { $sDef["borderWidth"] = "thick" } } if ($fmt.Wrap) { $sDef["wrap"] = $true } if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat } $styleDefs[$name] = $sDef } function Get-StyleName { param([int]$fmtIdx) $key = $formatToStyleKey[$fmtIdx] if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] } return "default" } # --- 12. Build areas --- $dslAreas = @() foreach ($area in $namedAreas) { $areaRows = @() for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) { $rd = $rowData[$globalRow] if (-not $rd -or $rd.Empty) { $areaRows += [ordered]@{} continue } $dslRow = [ordered]@{} # Row height if ($rd.FormatIdx -gt 0) { $rowFmt = Get-Format $rd.FormatIdx if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } } # Separate content cells from gap-fill cells $contentCells = @() $gapCells = @() foreach ($cell in $rd.Cells) { $hasContent = $cell.Param -or $cell.Text $hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)") if ($hasContent -or $hasMerge) { $contentCells += $cell } else { $gapCells += $cell } } # Detect rowStyle $rowStyleName = $null $rowStyleKey = $null if ($gapCells.Count -gt 0) { $gapKeys = @{} foreach ($gc in $gapCells) { $fmt = Get-Format $gc.FormatIdx $gapKeys[(Get-StyleKey $fmt)] = $true } if ($gapKeys.Count -eq 1) { $rowStyleKey = @($gapKeys.Keys)[0] if ($styleNames.Contains($rowStyleKey)) { $rowStyleName = $styleNames[$rowStyleKey] } } } if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName } # Build cell list $dslCells = @() foreach ($cell in ($contentCells | Sort-Object { $_.Col })) { $dslCell = [ordered]@{ col = $cell.Col + 1 } # Span/rowspan from merge $mk = "$globalRow,$($cell.Col)" if ($mergeMap.ContainsKey($mk)) { $m = $mergeMap[$mk] if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 } if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 } } # Style $cellFmt = Get-Format $cell.FormatIdx $cellStyleKey = Get-StyleKey $cellFmt if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) { # Inherits rowStyle } else { $sn = Get-StyleName $cell.FormatIdx if ($sn -ne "default" -or -not $rowStyleName) { $dslCell["style"] = $sn } } # Content $fillType = if ($cellFmt) { $cellFmt.FillType } else { "" } if ($cell.Param) { $dslCell["param"] = $cell.Param if ($cell.Detail) { $dslCell["detail"] = $cell.Detail } } elseif ($fillType -eq "Template" -and $cell.Text) { $dslCell["template"] = $cell.Text } elseif ($cell.Text) { $dslCell["text"] = $cell.Text } $dslCells += $dslCell } if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells } $areaRows += $dslRow } # Compress consecutive empty rows ({}) into { empty = N } $compressedRows = @() $emptyRun = 0 foreach ($r in $areaRows) { if ($r.Count -eq 0) { $emptyRun++ } else { if ($emptyRun -gt 0) { if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } else { $compressedRows += [ordered]@{ empty = $emptyRun } } $emptyRun = 0 } $compressedRows += $r } } if ($emptyRun -gt 0) { if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} } else { $compressedRows += [ordered]@{ empty = $emptyRun } } } $dslAreas += [ordered]@{ name = $area.Name rows = [array]$compressedRows } } # --- 13. Compress columnWidths --- $compressedWidths = [ordered]@{} if ($colWidthMap.Count -gt 0) { $grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] } foreach ($g in $grouped) { $width = [int]$g.Name $cols = @($g.Group | Sort-Object { [int]$_ }) $ranges = @() $rangeStart = $cols[0]; $rangePrev = $cols[0] for ($i = 1; $i -lt $cols.Count; $i++) { if ([int]$cols[$i] -eq [int]$rangePrev + 1) { $rangePrev = $cols[$i] } else { if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } else { $ranges += "$rangeStart-$rangePrev" } $rangeStart = $cols[$i]; $rangePrev = $cols[$i] } } if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" } else { $ranges += "$rangeStart-$rangePrev" } foreach ($range in $ranges) { $compressedWidths[$range] = $width } } } # --- 14. Build fonts output --- $fontsOut = [ordered]@{} foreach ($name in $fontDefs.Keys) { $f = $fontDefs[$name] $fOut = [ordered]@{ face = $f.Face; size = $f.Size } if ($f.Bold) { $fOut["bold"] = $true } if ($f.Italic) { $fOut["italic"] = $true } if ($f.Underline) { $fOut["underline"] = $true } if ($f.Strikeout) { $fOut["strikeout"] = $true } $fontsOut[$name] = $fOut } # --- 15. Assemble result --- $result = [ordered]@{ columns = $totalColumns defaultWidth = $defaultWidth } if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths } # Remove empty "default" style if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) { $styleDefs.Remove("default") } # Remove unused styles $usedStyles = @{} foreach ($a in $dslAreas) { foreach ($r in $a.rows) { if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true } if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } } } } $toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) foreach ($s in $toRemove) { $styleDefs.Remove($s) } $result["fonts"] = $fontsOut $result["styles"] = $styleDefs $result["areas"] = [array]$dslAreas # --- 16. Convert to JSON and fix Unicode --- $json = $result | ConvertTo-Json -Depth 10 # PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8 $json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', { param($m) [char][int]("0x" + $m.Groups[1].Value) }) # --- 17. Output --- if ($OutputPath) { $enc = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText( (Join-Path (Get-Location) $OutputPath), $json, $enc ) Write-Host "[OK] Decompiled: $OutputPath" } else { Write-Output $json } Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray