# mxl-decompile v1.20 — 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 # PreserveWhitespace = $false схлопывает узел из одних пробелов в пустой, и текст ячейки # вида " " терялся молча (25 макетов пилота). Значащие пробелы в тексте надписи есть. $xmlDoc.PreserveWhitespace = $true $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") # Тип значения каждого тега формата — выведен из корпуса, а не выписан на глаз. # line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: # ml — многоязычная строка; enum — замкнутый список. # containsValue / valueType / controlType сюда НЕ входят: это свойства конкретной ячейки, # а стиль — сущность общая, один на многие ячейки. function Get-FormatTagKind { return @{ 'autoIndent' = 'int'; 'autoMarkIncomplete' = 'bool'; 'autoWidthCalculation' = 'bool' 'backColor' = 'color'; 'border' = 'line'; 'borderColor' = 'color' 'bottomBorder' = 'line'; 'bySelectedColumns' = 'bool'; 'columnSizeChange' = 'enum' 'detailsUse' = 'enum'; 'drawingBorder' = 'int' 'drawingHaveBottomBorder' = 'bool'; 'drawingHaveLeftBorder' = 'bool' 'drawingHaveRightBorder' = 'bool'; 'drawingHaveTopBorder' = 'bool' 'editFormat' = 'ml'; 'fillType' = 'enum'; 'font' = 'int'; 'format' = 'ml' 'height' = 'int'; 'hidden' = 'bool'; 'horizontalAlignment' = 'enum' 'hyperLink' = 'bool'; 'indent' = 'int'; 'leftBorder' = 'line' 'markNegatives' = 'bool'; 'mask' = 'ml'; 'pattern' = 'enum'; 'patternColor' = 'color' 'picHorizontalAlignment' = 'enum'; 'picIndex' = 'int'; 'picVerticalAlignment' = 'enum' 'pictureSizeMode' = 'enum'; 'print' = 'bool'; 'protection' = 'bool' 'rightBorder' = 'line'; 'textColor' = 'color'; 'textOrientation' = 'int' 'textPlacement' = 'enum'; 'textPosition' = 'enum'; 'topBorder' = 'line' 'verticalAlignment' = 'enum'; 'width' = 'int'; 'widthWeightFactor' = 'int' } } $RU_NAME = 'Русский' # Теги, значение которых — многоязычная строка ( на язык), а не скаляр. function Get-FormatMlTags { return @{ 'format' = $true; 'editFormat' = $true; 'mask' = $true } } # Цвет платформа хранит значением с префиксом пространства имён. style: объявлен в корне # документа и читается как есть; web/win объявлены прямо на узле сгенерированным префиксом # (d3p1), поэтому его надо разрешить в URI и вернуть привычное web:/win:. function ConvertTo-DslColor { param($node, [string]$val) if ($val -notlike '*:*' -or $val -like 'style:*') { return $val } $prefix = $val.Substring(0, $val.IndexOf(':')) $name = $val.Substring($val.IndexOf(':') + 1) # get_*(): XML-адаптер PowerShell перекрывает .NET-члены одноимёнными атрибутами узла. $uri = $node.GetNamespaceOfPrefix($prefix) if ($uri -ceq 'http://v8.1c.ru/8.1/data/ui/colors/web') { return "web:$name" } if ($uri -ceq 'http://v8.1c.ru/8.1/data/ui/colors/windows') { return "win:$name" } return $val } # --- 2. Extract font palette --- # Размер шрифта бывает дробным (8.3, 11.3 — в корпусе ERP это треть макетов). [int] его # ТИХО округлял, а py-порт падал на int(). Читаем инвариантной культурой и держим целым, # когда дробной части нет, — иначе "10" превратилось бы в "10.0". function ConvertTo-FontSize { param($raw) $s = [string]$raw if ([string]::IsNullOrWhiteSpace($s)) { return 0 } $d = 0.0 if (-not [double]::TryParse($s, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { return 0 } if ($d -eq [math]::Floor($d)) { return [int]$d } return $d } $rawFonts = @() foreach ($fNode in $root.SelectNodes("d:font", $ns)) { $rawFonts += @{ # Шрифт бывает ссылкой на элемент стиля (style:) или системный шрифт (sys:) — # тогда своих атрибутов у него нет, и без этого он превращался в пустую запись. Ref = $fNode.GetAttribute("ref") Face = $fNode.GetAttribute("faceName") Size = ConvertTo-FontSize $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)) { $st = $lNode.SelectSingleNode("v8ui:style", $ns) $rawLines += @{ Width = [int]$lNode.GetAttribute("width") Gap = $lNode.GetAttribute("gap") Style = if ($st) { $st.InnerText } else { "Solid" } } } # --- 4. Extract format palette --- # Формат читаем целиком — «тег → значение», а не выборочные девять полей. Тип значения # берём из общей таблицы, она же у компилятора. $formatKinds = Get-FormatTagKind $formatMl = Get-FormatMlTags $rawFormats = @() foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { # Совместимость с прежними полями: ими пользуются ширина колонки, высота строки и # признак заполнения — они не часть стиля. $fmt = @{ FontIdx = -1; Width = 0; Height = 0; FillType = "" Props = [ordered]@{} } foreach ($child in $fmtNode.ChildNodes) { if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } $tag = $child.get_LocalName() if ($formatMl.ContainsKey($tag)) { $item = $child.SelectSingleNode("v8:item/v8:content", $ns) if ($item -and $item.InnerText) { $fmt.Props[$tag] = $item.InnerText } continue } # Вложенный элемент скаляром не является: InnerText склеил бы всё поддерево вместе # с переводами строк. py-порт брал .text и получал пустоту — отсюда расхождение портов. if ($child.SelectNodes('*').Count -gt 0) { continue } $val = $child.InnerText if ([string]::IsNullOrWhiteSpace($val)) { continue } if ($formatKinds[$tag] -ceq 'color') { $val = ConvertTo-DslColor $child $val } $fmt.Props[$tag] = $val } if ($fmt.Props.Contains('font')) { $fmt.FontIdx = [int]$fmt.Props['font'] } if ($fmt.Props.Contains('width')) { $fmt.Width = [int]$fmt.Props['width'] } if ($fmt.Props.Contains('height')) { $fmt.Height = [int]$fmt.Props['height'] } if ($fmt.Props.Contains('fillType')) { $fmt.FillType = $fmt.Props['fillType'] } $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 --- # Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент . # Их бывает несколько: раскладка БЕЗ — умолчание (ровно одна в каждом макете корпуса), # остальные адресуются GUID из , на который ссылаются строки () и # области (). Раньше читался только первый — отсюда терялись # ширины и вылезал columns: 0. $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 } } function Read-ColumnSet { param($node) $byIdx = @{} foreach ($ci in $node.SelectNodes("d:columnsItem", $ns)) { $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText $byIdx[$colIdx] = $fmtIdx } # Карта ширин (1-based колонка → ширина), только отличные от умолчания. $widths = [ordered]@{} foreach ($col0 in ($byIdx.Keys | Sort-Object)) { $fmt = Get-Format $byIdx[$col0] if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { $widths.Add([string]($col0 + 1), $fmt.Width) } } # Формат колонки несёт не только ширину — имя стиля подставим ниже, когда стили # будут поименованы. $fmtIdx = [ordered]@{} foreach ($col0 in ($byIdx.Keys | Sort-Object)) { $fmtIdx.Add([string]($col0 + 1), $byIdx[$col0]) } $sizeNode = $node.SelectSingleNode("d:size", $ns) $idNode = $node.SelectSingleNode("d:id", $ns) return @{ Id = if ($idNode) { $idNode.InnerText } else { $null } Size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } Widths = $widths FmtIdx = $fmtIdx } } $columnSets = @() foreach ($cn in $root.SelectNodes("d:columns", $ns)) { $columnSets += Read-ColumnSet $cn } $defaultSet = @($columnSets | Where-Object { -not $_.Id })[0] if (-not $defaultSet -and $columnSets.Count -gt 0) { $defaultSet = $columnSets[0] } $totalColumns = if ($defaultSet) { $defaultSet.Size } else { 0 } $colWidthMap = if ($defaultSet) { $defaultSet.Widths } else { [ordered]@{} } # --- 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 --- # Захватываем области ВСЕХ типов. Раньше здесь стоял `if ($areaType -ne "Rows") { continue }`, # из-за чего терялись Rectangle и Columns — а они есть у 61% макетов корпуса. $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) if (-not $areaNode) { continue } $getCoord = { param([string]$tag) $n = $areaNode.SelectSingleNode("d:$tag", $ns) if ($n) { return [int]$n.InnerText } return -1 } $namedAreas += @{ # Порядок в исходнике: Sort-Object в PowerShell 5.1 НЕстабилен (-Stable появился # в 6.2), поэтому области с одинаковыми границами получали произвольный порядок, # и вывод расходился с py-портом от запуска к запуску. Ord = $namedAreas.Count Name = $niNode.SelectSingleNode("d:name", $ns).InnerText Type = $areaNode.SelectSingleNode("d:type", $ns).InnerText BeginRow = & $getCoord 'beginRow' EndRow = & $getCoord 'endRow' BeginCol = & $getCoord 'beginColumn' EndCol = & $getCoord 'endColumn' } } # --- 8. Extract rows --- $rowData = @{} # Языки, на которых в макете вообще есть текст. Порядок — первого появления. $docLangs = [ordered]@{} 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 } # Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё # остальное терялось — в корпусе ERP 98% макетов держат текст и под ru, и под en. # Здесь собираем «язык → текст» как есть; свернётся в строку позже, когда станет # известен набор языков всего макета (Get-DslText). $text = $null $hasText = $false # Пустой — отдельное состояние: тег текста есть, языков в нём нет. Без # этого ветвления такая ячейка теряла текст вовсе (1 из 15 ячеек с тегом текста). $tlNode = $cContent.SelectSingleNode("d:tl", $ns) $items = $cContent.SelectNodes("d:tl/v8:item", $ns) if ($tlNode -and (-not $items -or $items.Count -eq 0)) { $text = [ordered]@{} $hasText = $true } elseif ($items -and $items.Count -gt 0) { $byLang = [ordered]@{} foreach ($it in $items) { $langNode = $it.SelectSingleNode("v8:lang", $ns) $contentNode = $it.SelectSingleNode("v8:content", $ns) $lang = if ($langNode) { $langNode.InnerText } else { '' } $byLang[$lang] = if ($contentNode) { $contentNode.InnerText } else { '' } $docLangs[$lang] = $true } $text = $byLang # Единственный пустой русский текст содержимым не считается — такая ячейка # уходит в заполнители строки. $hasText = -not ($byLang.Count -eq 1 -and $byLang.Contains('ru') -and "$($byLang['ru'])" -eq '') } $cells += @{ Col = $col FormatIdx = $cellFmtIdx Param = $param Detail = $detail Text = $text HasText = $hasText } } } # Ссылка строки на колоночную раскладку; пусто = раскладка по умолчанию. $rowColumnsId = $null $cidNode = $rowNode.SelectSingleNode("d:columnsID", $ns) if ($cidNode) { $rowColumnsId = $cidNode.InnerText } for ($r = $rowIdx; $r -le $indexTo; $r++) { $rowData[$r] = @{ FormatIdx = $rowFmtIdx Cells = $cells Empty = $isEmpty ColumnsId = $rowColumnsId } } } # Языки текстов макета и языки, объявленные в конфигурации, — разные вещи: у типовых они не # совпадают. Строкой пишем текст, одинаковый на ВСЁМ наборе языков макета; остальное — объектом. $textLanguages = @($docLangs.Keys) function Get-DslText { param($byLang) if ($byLang -isnot [System.Collections.IDictionary]) { return $byLang } if ($byLang.Count -ne $textLanguages.Count) { return $byLang } $common = $null foreach ($l in $textLanguages) { if (-not $byLang.Contains($l)) { return $byLang } if ($null -eq $common) { $common = "$($byLang[$l])" } elseif ("$($byLang[$l])" -cne $common) { return $byLang } } return $common } # --- 9. Build style key (ignoring fillType) --- # Свойства формата, которые стилем НЕ являются: ширина принадлежит колонке, высота — # строке, вид заполнения выводится из того, чем задано содержимое ячейки. # containsValue/valueType/controlType — свойства ЗНАЧЕНИЯ ячейки, а не оформления: стиль # общий на многие ячейки, а они индивидуальны. Компилятор таких ключей не знает, так что # в DSL они были чистым шумом. $nonStyleTags = @('width', 'height', 'fillType', 'font', 'containsValue', 'valueType', 'controlType') $borderTags = @('border', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder') function ConvertTo-DslLine { # Ссылку в палитру разворачиваем в описание линии. Ширина 1 подразумевается, поэтому # обычная линия пишется просто именем стиля. param($idx) $i = [int]$idx if ($i -lt 0 -or $i -ge $rawLines.Count) { return "Solid" } $ln = $rawLines[$i] $out = [ordered]@{ style = $ln.Style } if ($ln.Width -ne 1) { $out['width'] = $ln.Width } if ($ln.Gap -ceq 'true') { $out['gap'] = $true } if ($out.Count -eq 1) { return $ln.Style } return $out } function Get-StyleProps { # Свойства формата, попадающие в стиль, — в каноническом порядке тегов. param($fmt) $out = [ordered]@{} if (-not $fmt) { return $out } foreach ($tag in $fmt.Props.Keys) { if ($nonStyleTags -ccontains $tag) { continue } $val = $fmt.Props[$tag] if ($borderTags -ccontains $tag) { $out[$tag] = ConvertTo-DslLine $val } elseif ($formatKinds[$tag] -ceq 'bool') { $out[$tag] = ($val -ceq 'true') } elseif ($formatKinds[$tag] -ceq 'int') { $out[$tag] = [int]$val } else { $out[$tag] = $val } } return $out } function Get-StyleKey { param($fmt) if (-not $fmt) { return "empty" } # «шрифта нет» и «шрифт с индексом 0» — разные форматы: нулевой шрифт вовсе не обязан # быть обычным, в макете он бывает курсивным или жирным. $parts = @("f=" + $(if ($fmt.FontIdx -ge 0) { "$($fmt.FontIdx)" } else { "none" })) $props = Get-StyleProps $fmt foreach ($tag in $props.Keys) { $v = $props[$tag] if ($v -is [System.Collections.IDictionary]) { $v = (($v.Keys | ForEach-Object { "$_=$($v[$_])" }) -join ',') } $parts += "$tag=$v" } return ($parts -join '|') } # --- 10. Name fonts --- $fontNames = @{} $fontDefs = [ordered]@{} if ($rawFonts.Count -gt 0) { $fontNames[0] = "default" $fontDefs["default"] = $rawFonts[0] } function Get-FontKey { param($f) if ($f.Ref) { return "ref=$($f.Ref)" } 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.Ref) { # Имя из ссылки: style:SmallTextFont → small-text-font-подобное читаемое имя не даёт # ничего сверх самой ссылки, поэтому берём часть после префикса. $name = ($f.Ref -replace '^[^:]+:', '').ToLowerInvariant() } elseif ($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 = @{} # Обход по ВОЗРАСТАНИЮ номера строки: у обычной хэш-таблицы порядок в PowerShell не # определён, и стили обнаруживались в порядке хэшей. Платформа же кладёт записи палитры # в порядке документа, поэтому порядок обхода здесь — часть верности вывода. # ([ordered] с целочисленными ключами не годится: он индексируется по позиции, а не по ключу.) foreach ($r in ($rowData.Keys | Sort-Object { [int]$_ } | ForEach-Object { $rowData[$_] })) { 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 } } # Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, # поэтому в именованный стиль попадать не должно. function Get-RowStyleFmt { param($fmt) if (-not $fmt) { return $null } $props = Get-StyleProps $fmt $props.Remove('hidden') | Out-Null # Шрифт в Get-StyleProps не входит (он адресуется именем), поэтому формат строки, # несущий ТОЛЬКО шрифт, здесь выглядел пустым и терялся. if ($props.Count -eq 0 -and $fmt.FontIdx -lt 0) { return $null } $reduced = [ordered]@{} foreach ($tag in $fmt.Props.Keys) { if ($tag -cne 'hidden') { $reduced[$tag] = $fmt.Props[$tag] } } return @{ FontIdx = $fmt.FontIdx; Width = 0; Height = 0; FillType = ""; Props = $reduced } } # Строка — тоже владелец формата: её оформление становится именованным стилем. foreach ($rd in ($rowData.Keys | Sort-Object { [int]$_ } | ForEach-Object { $rowData[$_] })) { if ($rd.FormatIdx -le 0) { continue } $rf = Get-RowStyleFmt (Get-Format $rd.FormatIdx) if (-not $rf) { continue } $key = Get-StyleKey $rf if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $rf } } # Колонка — третий владелец формата, её оформление тоже становится именованным стилем. foreach ($cs in $columnSets) { foreach ($fi in $cs.FmtIdx.Values) { $fmt = Get-Format $fi if (-not $fmt) { continue } if ((Get-StyleProps $fmt).Count -eq 0) { continue } $key = Get-StyleKey $fmt if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } $formatToStyleKey[$fi] = $key } } # Имя стиля — читаемая метка по самым заметным свойствам. Полнота тут не нужна: # различает стили ключ, имя лишь помогает автору ориентироваться. function Name-Style { param($fmt) if (-not $fmt) { return "default" } $parts = @() $props = Get-StyleProps $fmt $fi = $fmt.FontIdx if ($fi -ge 0 -and $fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { $parts += $fontNames[$fi] } if ($props.Contains('border')) { $parts += "bordered" } else { $sides = @() foreach ($s in @('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder')) { if ($props.Contains($s)) { $sides += $s.Substring(0, $s.Length - 6) } } if ($sides.Count -gt 0) { $parts += "border-$($sides -join ',')" } } if ($props['horizontalAlignment'] -ceq "Center") { $parts += "center" } elseif ($props['horizontalAlignment'] -ceq "Right") { $parts += "right" } if ($props['verticalAlignment'] -ceq "Center") { $parts += "vcenter" } elseif ($props['verticalAlignment'] -ceq "Top") { $parts += "vtop" } if ($props['textPlacement'] -ceq "Wrap") { $parts += "wrap" } if ($props.Contains('backColor')) { $parts += "bg" } if ($props.Contains('format')) { $parts += "fmt" } # Имя "default" зарезервировано за стилем БЕЗ свойств: под ним компилятор понимает # отсутствие оформления. Набор из одних неприметных свойств (отступ, защита) обязан # получить своё имя, иначе он потеряется. # Имя "default" зарезервировано за стилем БЕЗ свойств: под ним компилятор понимает # отсутствие оформления, и ячейка такой стиль не записывает. Набор из одних неприметных # свойств или из одного шрифта обязан получить своё имя, иначе он потеряется. if ($parts.Count -eq 0) { $hasContent = ($props.Count -gt 0) -or ($fmt.FontIdx -ge 0) return $(if ($hasContent) { "style" } else { "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]@{} if ($fmt.FontIdx -ge 0) { $sDef["font"] = $(if ($fontNames.ContainsKey($fmt.FontIdx)) { $fontNames[$fmt.FontIdx] } else { "default" }) } $props = Get-StyleProps $fmt foreach ($tag in $props.Keys) { $sDef[$tag] = $props[$tag] } $styleDefs[$name] = $sDef } function Get-StyleName { param([int]$fmtIdx) $key = $formatToStyleKey[$fmtIdx] if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] } return "default" } # Колонки раскладки, у которых формат несёт не только ширину, — «колонка → стиль». function Get-ColumnStyles { param($cs) $out = [ordered]@{} foreach ($col in $cs.FmtIdx.Keys) { $fi = $cs.FmtIdx[$col] if ($fi -eq 0) { # Колонка перечислена, формата у неё нет. Для авторинга бесполезно, но без # этого раундтрип теряет элемент целиком. $out[$col] = $null continue } $fmt = Get-Format $fi if ($fmt -and (Get-StyleProps $fmt).Count -gt 0) { $out[$col] = Get-StyleName $fi } } return $out } # Список позиционный, если позиция в нём НЕ записана: ни у одного элемента нет col. # Прежняя проверка искала элемент-строку, поэтому строка из одних многоязычных текстов # позиционной не признавалась и оставалась объектной. function Test-PositionalList { param($cells) foreach ($el in $cells) { if ($el -is [System.Collections.IDictionary] -and $el.Contains('col')) { return $false } } return $true } # Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline. # В отличие от ConvertTo-Json (PS5.1): # - не выравнивает ключи объекта по самому длинному # - не разворачивает массивы примитивов на отдельные строки # - кириллица в UTF-8 (без \uXXXX-escapes) function Convert-StringToJsonLiteral { param([string]$s) if ($null -eq $s) { return 'null' } $sb = New-Object System.Text.StringBuilder [void]$sb.Append('"') foreach ($ch in $s.ToCharArray()) { $code = [int]$ch if ($code -eq 0x22) { [void]$sb.Append('\"') } elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } elseif ($code -eq 0x08) { [void]$sb.Append('\b') } elseif ($code -eq 0x09) { [void]$sb.Append('\t') } elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } else { [void]$sb.Append($ch) } } [void]$sb.Append('"') return $sb.ToString() } # Попробовать сериализовать значение полностью inline (одна строка). # Возвращает строку либо $null, если содержимое не помещается. function Try-InlineJson { param($obj) if ($null -eq $obj) { return 'null' } if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } if ($obj -is [int] -or $obj -is [long]) { return "$obj" } if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) } if ($obj -is [System.Collections.IDictionary]) { if ($obj.Count -eq 0) { return '{}' } $parts = @() foreach ($k in $obj.Keys) { $v = Try-InlineJson $obj[$k] if ($null -eq $v) { return $null } $parts += "$(Convert-StringToJsonLiteral "$k"): $v" } return '{ ' + ($parts -join ', ') + ' }' } if ($obj -is [System.Management.Automation.PSCustomObject]) { $props = @($obj.PSObject.Properties) if ($props.Count -eq 0) { return '{}' } $parts = @() foreach ($p in $props) { $v = Try-InlineJson $p.Value if ($null -eq $v) { return $null } $parts += "$(Convert-StringToJsonLiteral "$($p.Name)"): $v" } return '{ ' + ($parts -join ', ') + ' }' } if ($obj -is [array] -or $obj -is [System.Collections.IList]) { $items = @($obj) if ($items.Count -eq 0) { return '[]' } $parts = @() foreach ($it in $items) { $v = Try-InlineJson $it if ($null -eq $v) { return $null } $parts += $v } return '[' + ($parts -join ', ') + ']' } return $null } function ConvertTo-CompactJson { param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 400) $indent = $indentUnit * $depth $childIndent = $indentUnit * ($depth + 1) if ($null -eq $obj) { return 'null' } if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } if ($obj -is [int] -or $obj -is [long]) { return "$obj" } if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) } # Try inline для объектов и массивов с объектами — если помещается в lineLimit с учётом текущего indent. $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [System.Management.Automation.PSCustomObject]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) if ($isContainer) { $inlineAttempt = Try-InlineJson $obj if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } } # Hashtable / OrderedDictionary — объект multi-line if ($obj -is [System.Collections.IDictionary]) { $keys = @($obj.Keys) if ($keys.Count -eq 0) { return '{}' } $parts = @() foreach ($k in $keys) { $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" } return "{`n" + ($parts -join ",`n") + "`n$indent}" } if ($obj -is [System.Management.Automation.PSCustomObject]) { $props = @($obj.PSObject.Properties) if ($props.Count -eq 0) { return '{}' } $parts = @() foreach ($p in $props) { $val = ConvertTo-CompactJson -obj $p.Value -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit $parts += "$childIndent$(Convert-StringToJsonLiteral "$($p.Name)"): $val" } return "{`n" + ($parts -join ",`n") + "`n$indent}" } # Array / IList multi-line if ($obj -is [array] -or $obj -is [System.Collections.IList]) { $items = @($obj) if ($items.Count -eq 0) { return '[]' } $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) return "[`n" + ($parts -join ",`n") + "`n$indent]" } # Fallback return (Convert-StringToJsonLiteral "$obj") } # Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется. # Ячейка, у которой кроме текста или параметра ничего нет, пишется значением; span # раскрывается маркерами ">"; пропуск колонки — null; всё прочее — объектным элементом # без col. Выбираем ту запись, которая КОРОЧЕ. function ConvertTo-PositionalCells { param($cells) # Пропуск колонки — null. Выбираем ту запись, которая КОРОЧЕ: раньше позиционная форма # отбрасывалась, как только первая ячейка стояла не в первой колонке, хотя один-два null # впереди обычно короче объектной записи с `col`. if ($cells.Count -eq 0) { return @() } $out = @() $expected = 1 foreach ($c in $cells) { $col = [int]$c["col"] if ($col -lt $expected) { return $cells } # перекрытие — позиционно не выразить while ($expected -lt $col) { $out += $null; $expected++ } $span = if ($c.Contains("span")) { [int]$c["span"] } else { 1 } $keys = @($c.Keys | Where-Object { $_ -notin @("col", "span") }) # Текст — единственное содержимое: пишем значением. Оно бывает строкой либо # объектом «язык → текст», и в позиционной записи элемент И ЕСТЬ это значение. $plainText = ($keys.Count -eq 1 -and $keys[0] -eq "text") $plainParam = ($keys.Count -eq 1 -and $keys[0] -eq "param") if ($plainText) { $out += , $c["text"] } elseif ($plainParam) { $out += "{$($c["param"])}" } else { $obj = [ordered]@{} foreach ($k in $c.Keys) { if ($k -ne "col") { $obj[$k] = $c[$k] } } $out += $obj } # span раскрываем маркерами — кроме объектного элемента, он несёт span сам. if (($plainText -or $plainParam) -and $span -gt 1) { for ($i = 1; $i -lt $span; $i++) { $out += ">" } } $expected = $col + $span } # Позиционная форма ценна компактностью: если она длиннее объектной, смысла в ней нет. $a = Try-InlineJson $out $b = Try-InlineJson $cells if ($null -ne $a -and $null -ne $b -and $a.Length -gt $b.Length) { return $cells } return $out } # --- 12. Build areas --- # Сетка нарезается на блоки: непересекающиеся области типа Rows задают границы, строки вне # них становятся БЕЗЫМЯННЫМИ блоками. Раньше строки вне областей просто терялись — в корпусе # такие дыры у 34% макетов, а макетов вовсе без Rows-областей 21%. # Всё, что блоком не выражается (области не-Rows и пересекающиеся Rows), уходит в namedAreas # координатами. Правило детерминированное — иначе раундтрип поехал бы. $maxRowIdx = -1 foreach ($k in $rowData.Keys) { if ([int]$k -gt $maxRowIdx) { $maxRowIdx = [int]$k } } function Get-RowColumnsId { param([int]$r) if ($rowData.ContainsKey($r)) { return $rowData[$r].ColumnsId } return $null } # Область годится в качестве области-диапазона, только если у всех её строк ОДНА раскладка: # иначе её пришлось бы резать, а имя резать нельзя — такая уходит в координатный список. function Test-UniformColumnSet { param([int]$from, [int]$to) $first = Get-RowColumnsId $from for ($r = $from + 1; $r -le $to; $r++) { if ((Get-RowColumnsId $r) -ne $first) { return $false } } return $true } $blockAreas = @() $overlayAreas = @() $claimed = @{} # строка → занята областью-диапазоном foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{ Expression = { $_.EndRow } }, @{ Expression = { $_.Ord } })) { $fitsBlock = ($a.Type -eq 'Rows' -and $a.BeginRow -ge 0 -and $a.EndRow -ge $a.BeginRow) if ($fitsBlock) { for ($r = $a.BeginRow; $r -le $a.EndRow; $r++) { if ($claimed.ContainsKey($r)) { $fitsBlock = $false; break } } } if ($fitsBlock) { $fitsBlock = Test-UniformColumnSet $a.BeginRow $a.EndRow } if ($fitsBlock) { for ($r = $a.BeginRow; $r -le $a.EndRow; $r++) { $claimed[$r] = $true } $blockAreas += $a } else { $overlayAreas += $a } } # Безымянный промежуток режем на куски с одной раскладкой: границы наборов не совпадают # с границами именованных областей, а у области должна быть ровно одна раскладка. function Split-GapByColumnSet { param([int]$from, [int]$to) $out = @() if ($to -lt $from) { return $out } $runStart = $from $runId = Get-RowColumnsId $from for ($r = $from + 1; $r -le $to; $r++) { $id = Get-RowColumnsId $r if ($id -ne $runId) { $out += @{ Name = $null; BeginRow = $runStart; EndRow = $r - 1; ColumnsId = $runId } $runStart = $r; $runId = $id } } $out += @{ Name = $null; BeginRow = $runStart; EndRow = $to; ColumnsId = $runId } return $out } # Области в порядке строк + безымянные заполнители дыр. $blocks = @() $cursor = 0 foreach ($a in @($blockAreas | Sort-Object @{ Expression = { $_.BeginRow } })) { if ($a.BeginRow -gt $cursor) { $blocks += Split-GapByColumnSet $cursor ($a.BeginRow - 1) } $blocks += @{ Name = $a.Name; BeginRow = $a.BeginRow; EndRow = $a.EndRow; ColumnsId = (Get-RowColumnsId $a.BeginRow) } $cursor = $a.EndRow + 1 } if ($cursor -le $maxRowIdx) { $blocks += Split-GapByColumnSet $cursor $maxRowIdx } $dslAreas = @() foreach ($area in $blocks) { $areaRows = @() for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) { $rd = $rowData[$globalRow] if (-not $rd -or $rd.Empty) { $areaRows += [ordered]@{} continue } $dslRow = [ordered]@{} # Формат самой строки: высота и скрытие — её собственные свойства (у ячейки таких # нет), остальное — оформление, оно же может лежать и на ячейках. $rowFmt = if ($rd.FormatIdx -gt 0) { Get-Format $rd.FormatIdx } else { $null } $rowOwn = if ($rowFmt) { Get-StyleProps $rowFmt } else { [ordered]@{} } $rowHidden = ($rowOwn['hidden'] -eq $true) if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } if ($rowHidden) { $dslRow["hidden"] = $true } # Separate content cells from gap-fill cells $contentCells = @() $gapCells = @() foreach ($cell in $rd.Cells) { $hasContent = $cell.Param -or $cell.HasText $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] } } } # Стиль строки и стиль ячеек — независимые вещи: платформа их часто, но не всегда # пишет одинаковыми. Один ключ с модификатором apply покрывает все три случая. $ownFmt = Get-RowStyleFmt $rowFmt $ownKey = if ($ownFmt) { Get-StyleKey $ownFmt } else { $null } $ownName = if ($ownKey -and $styleNames.Contains($ownKey)) { $styleNames[$ownKey] } else { $null } $cellsName = if ($rowStyleName -and $rowStyleName -ne "default") { $rowStyleName } else { $null } if ($ownName -and $cellsName -and $ownKey -ceq $rowStyleKey) { $dslRow["rowStyle"] = $ownName } elseif ($ownName -and -not $cellsName) { $dslRow["rowStyle"] = [ordered]@{ style = $ownName; apply = "row" } } elseif ($cellsName -and -not $ownName) { $dslRow["rowStyle"] = [ordered]@{ style = $cellsName; apply = "cells" } } elseif ($ownName -and $cellsName) { # Стили разные — строке своё, ячейкам своё; ячейкам стиль раздаётся поячеечно. $dslRow["rowStyle"] = [ordered]@{ style = $ownName; apply = "row" } $rowStyleName = $null $rowStyleKey = $null $cellsName = $null } # 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 { # Стиль пишем, только когда он отличается от того, что подставит компилятор: # без rowStyle умолчание и есть "default", поэтому такой ключ — шум (56% ячеек). # А при заданном rowStyle стиль "default" писать ОБЯЗАТЕЛЬНО: раньше здесь стояло # `-not $rowStyleName`, и такая ячейка теряла стиль вовсе — при обратной сборке # она наследовала rowStyle. $sn = Get-StyleName $cell.FormatIdx if ($sn -ne "default" -or $cellsName) { $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.HasText) { $dslCell["template"] = Get-DslText $cell.Text } elseif ($cell.HasText) { $dslCell["text"] = Get-DslText $cell.Text } $dslCells += $dslCell } if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array](ConvertTo-PositionalCells $dslCells) } # Самая короткая из применимых форм: если у строки нет своих свойств, а список ячеек # позиционный — строка пишется просто массивом, без ключа cells. if ($dslRow.Count -eq 1 -and $dslRow.Contains("cells") -and (Test-PositionalList $dslRow["cells"])) { $areaRows += ,([array]$dslRow["cells"]) } else { $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 } } } $dslBlock = [ordered]@{} # Область без имени — просто кусок сетки, ключ name у неё не пишем. if (-not [string]::IsNullOrEmpty($area.Name)) { $dslBlock["name"] = $area.Name } # Ссылка на колоночную раскладку по имени из columnSets — как style у ячейки на styles. # Умолчание (раскладка без id) не пишем. if (-not [string]::IsNullOrEmpty($area.ColumnsId)) { $dslBlock["columnSet"] = $area.ColumnsId } $dslBlock["rows"] = [array]$compressedRows $dslAreas += $dslBlock } # --- 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] if ($f.Ref) { $fontsOut[$name] = [ordered]@{ ref = $f.Ref }; continue } $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 } if ($defaultSet) { $defaultColStyles = Get-ColumnStyles $defaultSet if ($defaultColStyles.Count -gt 0) { $result["columnStyles"] = $defaultColStyles } } # Набор языков объявляем, только если он отличается от умолчания компилятора (один ru). if ($textLanguages.Count -gt 0 -and -not ($textLanguages.Count -eq 1 -and $textLanguages[0] -ceq 'ru')) { $result["textLanguages"] = [array]$textLanguages } # Объявление языков МАКЕТА — не то же, что языки текста: почти во всех макетах ERP объявлен # один ru, а текст лежит и под ru, и под en. Пишем, только если отличается от умолчания. $lsNode = $root.SelectSingleNode("d:languageSettings", $ns) if ($lsNode) { $langs = @() foreach ($li in $lsNode.SelectNodes("d:languageInfo", $ns)) { $idN = $li.SelectSingleNode("d:id", $ns) $codeN = $li.SelectSingleNode("d:code", $ns) $descN = $li.SelectSingleNode("d:description", $ns) $langs += [ordered]@{ id = $(if ($idN) { $idN.InnerText } else { "" }) code = $(if ($codeN) { $codeN.InnerText } else { "" }) description = $(if ($descN) { $descN.InnerText } else { "" }) } } $isDefault = ($langs.Count -eq 1 -and $langs[0].id -ceq 'ru' -and $langs[0].code -ceq $RU_NAME -and $langs[0].description -ceq $RU_NAME) if (-not $isDefault) { $result["languages"] = [array]$langs } $curN = $lsNode.SelectSingleNode("d:currentLanguage", $ns) $cur = if ($curN) { $curN.InnerText } else { $null } if ($cur -cne 'ru') { $result["currentLanguage"] = $cur } $dfltN = $lsNode.SelectSingleNode("d:defaultLanguage", $ns) if ($dfltN -and $dfltN.InnerText -cne 'ru') { $result["defaultLanguage"] = $dfltN.InnerText } } # 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) { # Строка может быть массивом (короткая форма) — у неё нет свойства cells, и без этой # ветки стиль, использованный только внутри такой строки, вырезался как «неиспользуемый». $cellList = if ($r -is [array]) { $r } else { $r.cells } if ($r -isnot [array] -and $r.rowStyle) { $rs = $r.rowStyle $usedStyles[$(if ($rs -is [System.Collections.IDictionary]) { $rs['style'] } else { $rs })] = $true } if ($cellList) { foreach ($c in $cellList) { if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } } } } } # Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. Берём стили # ИЗ САМИХ РАСКЛАДОК, а не из $result: columnSets попадает в результат ПОЗЖЕ этой проверки, # и стиль, на который ссылается только дополнительная раскладка, был бы отсечён, а ссылка # на него осталась бы висячей. foreach ($cs in $columnSets) { foreach ($name in (Get-ColumnStyles $cs).Values) { if ($name) { $usedStyles[$name] = $true } } } $toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) foreach ($s in $toRemove) { $styleDefs.Remove($s) } $result["fonts"] = $fontsOut $result["styles"] = $styleDefs # Колоночные раскладки помимо умолчания: ключ — идентификатор из макета, на него ссылаются # области. Содержимое раскладку не опознаёт (в корпусе полно наборов с одинаковым # содержимым и разными id), поэтому склейки по содержимому нет. $extraSets = @($columnSets | Where-Object { $_.Id }) if ($extraSets.Count -gt 0) { $setsOut = [ordered]@{} foreach ($cs in $extraSets) { $entry = [ordered]@{ columns = $cs.Size } if ($cs.Widths.Count -gt 0) { $entry["columnWidths"] = $cs.Widths } $csStyles = Get-ColumnStyles $cs if ($csStyles.Count -gt 0) { $entry["columnStyles"] = $csStyles } $setsOut[$cs.Id] = $entry } $result["columnSets"] = $setsOut } $result["areas"] = [array]$dslAreas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится # из указанных осей (как в ТабличныйДокумент.Область()). DSL 1-based, XML 0-based. if ($overlayAreas.Count -gt 0) { $naOut = @() foreach ($a in $overlayAreas) { $entry = [ordered]@{ name = $a.Name } if ($a.BeginRow -ge 0) { $entry["rows"] = if ($a.EndRow -gt $a.BeginRow) { "$($a.BeginRow + 1)-$($a.EndRow + 1)" } else { $a.BeginRow + 1 } } if ($a.BeginCol -ge 0) { $entry["cols"] = if ($a.EndCol -gt $a.BeginCol) { "$($a.BeginCol + 1)-$($a.EndCol + 1)" } else { $a.BeginCol + 1 } } $naOut += $entry } $result["namedAreas"] = [array]$naOut } # --- 16. Convert to JSON --- $json = ConvertTo-CompactJson $result # --- 17. Output --- if ($OutputPath) { # Путь принимаем и относительным, и абсолютным: безусловная склейка с текущим каталогом # давала "C:\cwd\C:\out.json" и роняла WriteAllText сообщением про формат пути. $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location).Path $OutputPath } $enc = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText( $outAbs, $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