# mxl-decompile v1.30 — 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' = 'line' '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 } $CFG_NS = 'http://v8.1c.ru/8.1/data/enterprise/current-config' # Элемент управления ячейки: в выгрузке это GUID, имён у платформы в XML нет. $valueControlNames = @{ '381ed624-9217-4e63-85db-c4c3cb87daae' = 'input' '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26' = 'checkbox' } # Обратная функция к эмиттеру типа компилятора: → строка DSL в КРАТЧАЙШЕЙ форме # (Boolean, String, String(10,fixed), Number(15,3,nonneg), DateTime, CatalogRef.X, AnyRef), # из которой компилятор восстановит тот же XML. Пустой тег → пустая строка: «содержит # значение» без указания типа — самостоятельное состояние. # Префикс ссылочного типа разрешаем по URI, а не по имени: локальное объявление платформа # называет сгенерированным dNpM. Чужое пространство имён оставляем как есть. # Значение ячейки-поля ввода → литерал DSL. Тип значения выражается самим литералом: # число → числом, булево → true/false, строка и дата → строкой. Пустое значение пишем # как есть (0, false, пустая дата), а не схлопываем: так оно собирается обратно тем же # типом независимо от того, какой тип объявлен у ячейки. function ConvertTo-DslValue { param([string]$xsiType, [string]$text) if ($null -eq $text) { $text = '' } if ($xsiType -ceq 'xs:boolean') { return ($text.Trim() -ceq 'true') } if ($xsiType -ceq 'xs:decimal') { $s = $text.Trim() $i = 0; $d = 0.0 if ($s -notlike '*.*' -and [int]::TryParse($s, [ref]$i)) { return $i } if ([double]::TryParse($s, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { return $d } return $s } return $text } function Get-ValueQualifier { param($quals, [string]$kind, [string]$name, [string]$default = '') if (-not $quals.ContainsKey($kind)) { return $default } $sub = $quals[$kind].SelectSingleNode("v8:$name", $ns) if ($sub -and $sub.InnerText) { return $sub.InnerText.Trim() } return $default } function ConvertFrom-ValueTypeNode { param($node) $types = @() $quals = @{} foreach ($child in $node.ChildNodes) { if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } $tag = $child.get_LocalName() if ($tag -ceq 'Type' -or $tag -ceq 'TypeSet') { $types += ,@($child, $child.InnerText.Trim()) } elseif ($tag -like '*Qualifiers') { $quals[$tag.Substring(0, $tag.Length - 'Qualifiers'.Length)] = $child } } $parts = @() foreach ($t in $types) { $child = $t[0] $val = $t[1] if ($val -like '*:*') { $prefix = $val.Substring(0, $val.IndexOf(':')) $name = $val.Substring($val.IndexOf(':') + 1) # get_*(): XML-адаптер PowerShell перекрывает .NET-члены одноимёнными атрибутами узла. $uri = $child.GetNamespaceOfPrefix($prefix) if ($uri -ceq $CFG_NS) { $val = $name } elseif ($uri -ceq 'http://www.w3.org/2001/XMLSchema') { $val = "xs:$name" } } if ($val -ceq 'xs:boolean') { $parts += 'Boolean' } elseif ($val -ceq 'xs:string') { $length = Get-ValueQualifier $quals 'String' 'Length' '0' $fixed = (Get-ValueQualifier $quals 'String' 'AllowedLength' 'Variable') -ceq 'Fixed' if ($fixed) { $parts += "String($length,fixed)" } elseif ($length -ne '0') { $parts += "String($length)" } else { $parts += 'String' } } elseif ($val -ceq 'xs:decimal') { $digits = Get-ValueQualifier $quals 'Number' 'Digits' '0' $fraction = Get-ValueQualifier $quals 'Number' 'FractionDigits' '0' $nonneg = (Get-ValueQualifier $quals 'Number' 'AllowedSign' 'Any') -ceq 'Nonnegative' if ($nonneg) { $parts += "Number($digits,$fraction,nonneg)" } elseif ($digits -ne '0' -or $fraction -ne '0') { $parts += "Number($digits,$fraction)" } else { $parts += 'Number' } } elseif ($val -ceq 'xs:dateTime') { $parts += (Get-ValueQualifier $quals 'Date' 'DateFractions' 'DateTime') } else { $parts += $val } } return ($parts -join ' + ') } # --- 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 } # Тип значения ячейки — единственный тег со СВОЕЙ структурой: разбираем его до обеих # отсечек ниже. Пустой тоже значим — это «содержит значение» без # указания типа. if ($tag -ceq 'valueType') { $fmt.Props[$tag] = ConvertFrom-ValueTypeNode $child 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] } # --- 4-bis. Палитра картинок и рисунки --- # Картинка хранится ссылкой на библиотеку либо данными base64; ссылки на палитру (у рисунка # pictureIndex, у ячейки picIndex) — 1-based, сам — 0-based. $picturesOut = [ordered]@{} $pictureKey = @{} $picI = 0 foreach ($picNode in $root.SelectNodes("d:picture", $ns)) { $inner = $picNode.SelectSingleNode("d:picture", $ns) if (-not $inner) { continue } $picI++ $name = "pic$picI" $entry = [ordered]@{} if ($inner.GetAttribute('ref')) { $entry["ref"] = $inner.GetAttribute('ref') } elseif (($inner.InnerText).Trim()) { # System.Xml переводы строк в тексте не нормализует, а lxml нормализует: без этой # замены порты дали бы разный JSON на одном и том же файле. $entry["data"] = ($inner.InnerText -replace "`r`n", "`n").Trim() } # Прозрачность живёт и у ссылочной картинки (в БП 8.3.27 таких записей 67), поэтому # читаем её вне ветвления. Пиксель прозрачного цвета — координата внутри картинки. if ($entry.Count -gt 0 -and $inner.HasAttribute('t')) { $entry["transparent"] = ($inner.GetAttribute('t') -ceq 'true') } if ($entry.Count -gt 0 -and $inner.HasAttribute('tx')) { $entry["transparent"] = [ordered]@{ x = [int]$inner.GetAttribute('tx') y = [int]$inner.GetAttribute('ty') } } $picturesOut[$name] = $entry $pictureKey[$picI] = $name } $drawingNames = @{} foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) { if ($niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") -cne 'NamedItemDrawing') { continue } $idN = $niNode.SelectSingleNode("d:drawingID", $ns) $nmN = $niNode.SelectSingleNode("d:name", $ns) $drawingNames[$(if ($idN) { $idN.InnerText } else { '' })] = $(if ($nmN) { $nmN.InnerText } else { '' }) } $drawingsOut = @() $drI = 0 foreach ($dr in $root.SelectNodes("d:drawing", $ns)) { $drI++ $item = [ordered]@{} $tN = $dr.SelectSingleNode("d:drawingType", $ns) $item["type"] = if ($tN) { $tN.InnerText } else { 'Picture' } $idN = $dr.SelectSingleNode("d:id", $ns) $did = if ($idN) { $idN.InnerText } else { "$drI" } if ($did -cne "$drI") { $item["id"] = [int]$did } $zN = $dr.SelectSingleNode("d:zOrder", $ns) $zorder = if ($zN) { $zN.InnerText } else { "$drI" } if ($zorder -cne "$drI") { $item["zOrder"] = [int]$zorder } if ($drawingNames.ContainsKey($did)) { $item["name"] = $drawingNames[$did] } $piN = $dr.SelectSingleNode("d:pictureIndex", $ns) if ($piN -and $pictureKey.ContainsKey([int]$piN.InnerText)) { $item["picture"] = $pictureKey[[int]$piN.InnerText] } $dpN = $dr.SelectSingleNode("d:detailParameter", $ns) if ($dpN -and $dpN.InnerText) { $item["detail"] = $dpN.InnerText } $tEl = $dr.SelectSingleNode("d:text", $ns) if ($tEl) { $byLang = [ordered]@{} foreach ($it in $tEl.SelectNodes("v8:item", $ns)) { $l = $it.SelectSingleNode("v8:lang", $ns) $cnt = $it.SelectSingleNode("v8:content", $ns) $byLang[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) } $item["text"] = $byLang } # Оформление разбирается позже: линию нужно развернуть в описание, а помощник для # этого появляется вместе с палитрой стилей. $fN = $dr.SelectSingleNode("d:formatIndex", $ns) foreach ($pair in @(@('begin', 'beginRowOffset', 'beginColumnOffset'), @('end', 'endRowOffset', 'endColumnOffset'))) { $prefix = $pair[0] $rN = $dr.SelectSingleNode("d:$($prefix)Row", $ns) $cN = $dr.SelectSingleNode("d:$($prefix)Column", $ns) $dyN = $dr.SelectSingleNode("d:$($pair[1])", $ns) $dxN = $dr.SelectSingleNode("d:$($pair[2])", $ns) $item[$prefix] = [ordered]@{ row = $(if ($rN) { [int]$rN.InnerText } else { 0 }) + 1 col = $(if ($cN) { [int]$cN.InnerText } else { 0 }) + 1 dy = $(if ($dyN) { [int]$dyN.InnerText } else { 0 }) dx = $(if ($dxN) { [int]$dxN.InnerText } else { 0 }) } } $psN = $dr.SelectSingleNode("d:pictureSize", $ns) if ($psN -and $psN.InnerText -cne 'Stretch') { $item["pictureSize"] = $psN.InnerText } $item["_fmt"] = if ($fN) { [int]$fN.InnerText } else { 0 } $drawingsOut += , $item } # --- 4a. Колонтитулы и параметры печати --- # Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую # страницу читаем только у записи, где есть : палитра дедуплицирована, и колонтитул без # своих настроек ссылается на чужую запись, где — ширина колонки, а не номер страницы. function Read-HeaderPart { param([string]$suffix) $out = [ordered]@{} $fmtIdx = $null foreach ($slot in @('left', 'center', 'right')) { $el = $root.SelectSingleNode("d:$slot$suffix", $ns) if (-not $el) { continue } if ($null -eq $fmtIdx) { $fNode = $el.SelectSingleNode("d:f", $ns) $fmtIdx = if ($fNode) { [int]$fNode.InnerText } else { 0 } } foreach ($textTag in @('tl', 'tfl')) { $t = $el.SelectSingleNode("d:$textTag", $ns) if (-not $t) { continue } $byLang = [ordered]@{} foreach ($it in $t.SelectNodes("v8:item", $ns)) { $l = $it.SelectSingleNode("v8:lang", $ns) $cnt = $it.SelectSingleNode("v8:content", $ns) $byLang[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) } $value = Get-DslText $byLang $out[$slot] = if ($textTag -ceq 'tfl') { [ordered]@{ formatted = $value } } else { $value } break } } if ($out.Count -eq 0 -and $null -eq $fmtIdx) { return $null } $fmt = if ($fmtIdx) { Get-Format $fmtIdx } else { $null } if ($fmt) { $props = $fmt.Props $head = [ordered]@{} if ($fmt.FontIdx -ge 0) { $head["font"] = $fontNames[$fmt.FontIdx] } if ($props.Contains('height')) { if ("$($props['height'])" -ceq '-1') { $head["show"] = $false } $w = if ($props.Contains('width')) { [int]$props['width'] } else { 1 } if ($w -ne 1) { $head["startPage"] = $w } } if ($props.Contains('verticalAlignment')) { $head["verticalAlignment"] = $props['verticalAlignment'] } foreach ($k in $out.Keys) { $head[$k] = $out[$k] } $out = $head } return $out } $printSettings = $null $psNode = $root.SelectSingleNode("d:printSettings", $ns) if ($psNode) { $printSettings = [ordered]@{} foreach ($c in $psNode.ChildNodes) { if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } $v = $c.InnerText.Trim() $i = 0 if ($v -ceq 'true') { $printSettings[$c.get_LocalName()] = $true } elseif ($v -ceq 'false') { $printSettings[$c.get_LocalName()] = $false } elseif ([int]::TryParse($v, [ref]$i)) { $printSettings[$c.get_LocalName()] = $i } else { $printSettings[$c.get_LocalName()] = $v } } } # --- 4b. Группировки строк и колонок --- # Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней # () не читаем: оно выводится из вложенности — совпало на всех 1797 макетах. function Read-Groups { param([string]$tag, [string]$axis) $out = @() foreach ($g in $root.SelectNodes("d:$tag", $ns)) { $bNode = $g.SelectSingleNode("d:b", $ns) $b = if ($bNode) { [int]$bNode.InnerText } else { 0 } $eNode = $g.SelectSingleNode("d:e", $ns) $e = if ($eNode) { [int]$eNode.InnerText } else { $b } $item = [ordered]@{} $item[$axis] = if ($e -eq $b) { $b + 1 } else { "$($b + 1)-$($e + 1)" } $tEl = $g.SelectSingleNode("d:t", $ns) if ($tEl) { $byLang = [ordered]@{} foreach ($it in $tEl.SelectNodes("v8:item", $ns)) { $l = $it.SelectSingleNode("v8:lang", $ns) $cnt = $it.SelectSingleNode("v8:content", $ns) $byLang[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) } if ($byLang.Count -gt 0) { $item["name"] = $byLang } } $oNode = $g.SelectSingleNode("d:o", $ns) if ($oNode -and $oNode.InnerText.Trim() -ceq 'false') { $item["collapsed"] = $true } $gNode = $g.SelectSingleNode("d:g", $ns) if ($gNode -and $gNode.InnerText) { $item["titleLocation"] = $gNode.InnerText.Trim().ToLowerInvariant() } $out += $item } return @($out) } $rowGroups = Read-Groups 'vg' 'rows' $colGroups = Read-Groups 'hg' 'cols' # --- 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' # Привязка области к колоночной раскладке: у 913 483 прямоугольных областей корпуса # она есть, и из накрытых строк выводится не всегда. ColumnsId = $(if ($areaNode.SelectSingleNode("d:columnsID", $ns)) { $areaNode.SelectSingleNode("d:columnsID", $ns).InnerText } else { '' }) } } # --- 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 } # Имя параметра, которым подставляют картинку. Сама картинка сидит в # оформлении (picIndex), поэтому этот тег живёт отдельно от неё. $picParam = $null $ppNode = $cContent.SelectSingleNode("d:pictureParameter", $ns) if ($ppNode) { $picParam = $ppNode.InnerText } # Значение ячейки-поля ввода. Тип значения свой, из объявленного не выводится, # поэтому читаем и его. $value = $null $vNode = $cContent.SelectSingleNode("d:v", $ns) if ($vNode) { $value = @{ Type = $vNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance"); Text = $vNode.InnerText } } # Примечание к ячейке. Якоря не читаем как данные: конец — координаты самой # ячейки, начало — 1/1 у 1085 примечаний корпуса из 1087; остальное авторское. $note = $null $noteNode = $cContent.SelectSingleNode("d:note", $ns) if ($noteNode) { $noteText = [ordered]@{} $tEl = $noteNode.SelectSingleNode("d:text", $ns) if ($tEl) { foreach ($it in $tEl.SelectNodes("v8:item", $ns)) { $l = $it.SelectSingleNode("v8:lang", $ns) $cnt = $it.SelectSingleNode("v8:content", $ns) $noteText[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) } } $ng = @{} foreach ($ch in $noteNode.ChildNodes) { if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $ng[$ch.get_LocalName()] = $ch.InnerText.Trim() } } $note = @{ FormatIdx = [int]($(if ($ng['formatIndex']) { $ng['formatIndex'] } else { 0 })) Text = $noteText AutoSize = ($(if ($ng.ContainsKey('autoSize')) { $ng['autoSize'] } else { 'true' }) -ceq 'true') Box = [ordered]@{ top = [int]($(if ($ng['beginRowOffset']) { $ng['beginRowOffset'] } else { 0 })) left = [int]($(if ($ng['beginColumnOffset']) { $ng['beginColumnOffset'] } else { 0 })) bottom = [int]($(if ($ng['endRowOffset']) { $ng['endRowOffset'] } else { 0 })) right = [int]($(if ($ng['endColumnOffset']) { $ng['endColumnOffset'] } else { 0 })) } AnchorRow = [int]($(if ($ng['beginRow']) { $ng['beginRow'] } else { 1 })) AnchorCol = [int]($(if ($ng['beginColumn']) { $ng['beginColumn'] } else { 1 })) } } # Настройки элемента управления — сериализованный base64 у самой ячейки. # Структуру не разбираем, возим дословно. $control = $null $ctlNode = $cContent.SelectSingleNode("d:control", $ns) # Переводы строк внутри блоба приводим к LF: XML-парсеры нормализуют их # по-разному, и без этого порты давали разный JSON на одном файле. if ($ctlNode -and $ctlNode.InnerText) { $control = $ctlNode.InnerText -replace "`r`n", "`n" } # Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё # остальное терялось — в корпусе 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 PictureParam = $picParam Value = $value Control = $control Note = $note 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 } # Пустой текст () записывается в DSL пустой строкой — той же формой, что # авторская. Без этой ветки макет, где ДРУГОГО текста нет вовсе, отдавал $null, # и ячейка с пустым текстом теряла содержимое. if ($byLang.Count -eq 0) { return '' } 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', # Линия рисунка и её стороны — свойства рисунка, а не общего оформления: # на корпусе все 2 135 записей с ними принадлежат только рисункам. 'drawingBorder', 'drawingHaveLeftBorder', 'drawingHaveTopBorder', 'drawingHaveRightBorder', 'drawingHaveBottomBorder') $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 # Формат примечания живёт в той же палитре и тоже заслуживает имени: иначе # оформление подсказки терялось бы при обратной сборке. if ($cell.Note) { $nfmt = Get-Format $cell.Note.FormatIdx if ($nfmt) { $nkey = Get-StyleKey $nfmt if (-not $styleKeys.Contains($nkey)) { $styleKeys[$nkey] = $nfmt } $formatToStyleKey[$cell.Note.FormatIdx] = $nkey } } } } # Формат рисунка — ещё один владелец записи палитры, наравне с ячейкой, строкой, колонкой # и примечанием: без этого оформление рисунка теряло имя и вырезалось как неиспользуемое. foreach ($item in $drawingsOut) { $fi = [int]$item['_fmt'] if ($fi -le 0) { continue } $dfmt = Get-Format $fi if (-not $dfmt) { continue } if ((Get-StyleProps $dfmt).Count -gt 0 -or $dfmt.FontIdx -ge 0) { $dkey = Get-StyleKey $dfmt if (-not $styleKeys.Contains($dkey)) { $styleKeys[$dkey] = $dfmt } $formatToStyleKey[$fi] = $dkey } } # Оформление строки без её собственных свойств: скрытие уезжает инлайном к 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 } # Запятая обязательна: без неё список из одной ячейки разворачивается в саму ячейку, # а список из одного $null — в $null, и строка целиком уходила в пустые. 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) { # Блок задаёт раскладку строкам, и область наследует её же. Когда область несёт ДРУГУЮ # привязку, блоком её не выразить — уводим в namedAreas, где привязка пишется явным ключом. $rowSet = Get-RowColumnsId $a.BeginRow if ($null -eq $rowSet) { $rowSet = '' } $fitsBlock = ($a.ColumnsId -ceq $rowSet) } 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) { # Ячейка-поле ввода — содержательная, даже когда ни текста, ни параметра в ней нет: # значение задаёт её формат. Без этого такая строка уходила бы в пустые. $cf = Get-Format $cell.FormatIdx $hasValue = ($cf -and $cf.Props['containsValue'] -ceq 'true') # Расшифровка сама по себе делает ячейку содержательной: в корпусе 12 653 ячейки # несут только её. Без этого такая ячейка уходила в заполнители и терялась. $hasContent = $cell.Param -or $cell.HasText -or $hasValue -or $cell.Detail -or $cell.Note -or $cell.PictureParam $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 } } # Значение (ячейка-поле ввода). Свойство живёт в формате, но принадлежит ячейке: # на корпусе на такие записи ссылаются только . if ($cellFmt -and $cellFmt.Props['containsValue'] -ceq 'true') { $dslCell["valueType"] = if ($cellFmt.Props.Contains('valueType')) { $cellFmt.Props['valueType'] } else { "" } if (-not $cellFmt.Props.Contains('controlType')) { # Формат без вовсе — так пишут машинно-сгенерированные макеты # регламентированной отчётности; компилятор такое повторит только по # явному указанию. $dslCell["controlType"] = "none" } else { $ctl = "$($cellFmt.Props['controlType'])" $name = if ($valueControlNames.ContainsKey($ctl.ToLowerInvariant())) { $valueControlNames[$ctl.ToLowerInvariant()] } else { $ctl } # input — умолчание компилятора, писать его значит шуметь на 63 629 # форматах корпуса. if ($name -cne 'input') { $dslCell["controlType"] = $name } } if ($null -ne $cell.Value) { $dslCell["value"] = ConvertTo-DslValue $cell.Value.Type $cell.Value.Text } if ($cell.Control) { $dslCell["control"] = $cell.Control } } # Content $fillType = if ($cellFmt) { $cellFmt.FillType } else { "" } if ($cell.Param) { $dslCell["param"] = $cell.Param } elseif ($fillType -eq "Template" -and $cell.HasText) { $dslCell["template"] = Get-DslText $cell.Text } elseif ($cell.HasText) { $dslCell["text"] = Get-DslText $cell.Text } # Расшифровка живёт отдельно от параметра заполнения: на корпусе 20 404 ячейки # несут её без параметра против 8 582 с ним. Пока она читалась только вместе # с параметром, две трети расшифровок терялись молча. if ($cell.Detail) { $dslCell["detail"] = $cell.Detail } if ($cell.PictureParam) { $dslCell["pictureParameter"] = $cell.PictureParam } if ($cell.Note) { $n = $cell.Note $dslNote = [ordered]@{} $dslNote["text"] = Get-DslText $n.Text $styleName = Get-StyleName $n.FormatIdx if ($styleName -ne "default") { $dslNote["style"] = $styleName } if (-not $n.AutoSize) { $dslNote["autoSize"] = $false } $dslNote["box"] = $n.Box if ($n.AnchorRow -ne 1 -or $n.AnchorCol -ne 1) { $dslNote["anchor"] = [ordered]@{ row = $n.AnchorRow; col = $n.AnchorCol } } $dslCell["note"] = $dslNote } $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 } # Четвёртый владелец формата — примечание: его стиль тоже держит ссылку, # иначе он вырезается как неиспользуемый и ссылка остаётся висячей. if ($c -isnot [string] -and $c.note -and $c.note['style']) { $usedStyles[$c.note['style']] = $true } } } } } # Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. Берём стили # ИЗ САМИХ РАСКЛАДОК, а не из $result: columnSets попадает в результат ПОЗЖЕ этой проверки, # и стиль, на который ссылается только дополнительная раскладка, был бы отсечён, а ссылка # на него осталась бы висячей. foreach ($cs in $columnSets) { foreach ($name in (Get-ColumnStyles $cs).Values) { if ($name) { $usedStyles[$name] = $true } } } # Рисунок — пятый владелец формата. Имя ему присваивается позже, при сборке результата, # поэтому здесь считаем его из ссылки на палитру: иначе стиль рисунка вырежется как # неиспользуемый, а ссылка на него останется висячей. foreach ($item in $drawingsOut) { $fi = [int]$item['_fmt'] if ($fi -le 0) { continue } $dname = Get-StyleName $fi if ($dname -cne 'default') { $usedStyles[$dname] = $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 } # Имя группировки — та же мультиязычная строка, что текст ячейки, и сворачивается так же. foreach ($g in ($rowGroups + $colGroups)) { if ($g.Contains("name")) { $g["name"] = Get-DslText $g["name"] } } if ($rowGroups.Count -gt 0) { $result["rowGroups"] = [array]$rowGroups } if ($colGroups.Count -gt 0) { $result["columnGroups"] = [array]$colGroups } # Читаем здесь, а не в разделе 4a: тексту колонтитула нужен свод языков макета, который # становится известен только после разбора строк. $headerPart = Read-HeaderPart 'Header' $footerPart = Read-HeaderPart 'Footer' # Пустой словарь в PowerShell истинен, поэтому проверяем именно наполнение: макет со ссылкой # на общую запись палитры, но без своих настроек колонтитула, иначе получал в DSL пустой ключ. if ($headerPart -and $headerPart.Count -gt 0) { $result["header"] = $headerPart } if ($footerPart -and $footerPart.Count -gt 0) { $result["footer"] = $footerPart } if ($printSettings) { $result["printSettings"] = $printSettings } # Оформление рисунка: линия и её стороны — его собственные ключи, общее — именованный стиль. # Порядок ключей задаём здесь же, чтобы он не зависел от того, что разбиралось раньше. $drawingsOrdered = @() foreach ($item in $drawingsOut) { $fmtIdx = [int]$item['_fmt'] $item.Remove('_fmt') if ($item.Contains("text")) { $item["text"] = Get-DslText $item["text"] } $lineVal = $null $sides = [ordered]@{} if ($fmtIdx -gt 0) { $dfmt = Get-Format $fmtIdx $props = if ($dfmt) { $dfmt.Props } else { @{} } if ($props.Contains('drawingBorder')) { $lineVal = ConvertTo-DslLine ([int]$props['drawingBorder']) } foreach ($side in @('Left', 'Top', 'Right', 'Bottom')) { $tag = "drawingHave${side}Border" if ($props.Contains($tag)) { $sides[$side.ToLowerInvariant()] = ($props[$tag] -ceq 'true') } } $dname = Get-StyleName $fmtIdx if ($dname -cne 'default') { $item["style"] = $dname } } if ($null -ne $lineVal) { $item["line"] = $lineVal } if ($sides.Count -gt 0) { $item["sides"] = $sides } $outItem = [ordered]@{} foreach ($key in @('type', 'id', 'zOrder', 'name', 'picture', 'detail', 'text', 'style', 'line', 'sides', 'begin', 'end', 'pictureSize')) { if ($item.Contains($key)) { $outItem[$key] = $item[$key] } } $drawingsOrdered += , $outItem } if ($picturesOut.Count -gt 0) { $result["pictures"] = $picturesOut } if ($drawingsOrdered.Count -gt 0) { $result["drawings"] = [array]$drawingsOrdered } $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 } } # Привязку пишем, только когда она не выводится из накрытых строк. $derived = '' if ($a.BeginRow -ge 0) { $covered = @() for ($r = $a.BeginRow; $r -le $a.EndRow; $r++) { $cid = Get-RowColumnsId $r $covered += $(if ($null -eq $cid) { '' } else { $cid }) } $uniq = @($covered | Select-Object -Unique) if ($uniq.Count -eq 1) { $derived = $uniq[0] } } if ($a.ColumnsId -cne $derived) { $entry["columnSet"] = $a.ColumnsId } $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