Files
cc-1c-skills/.codeassistant/skills/mxl-decompile/scripts/mxl-decompile.ps1
T

1270 lines
54 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 — ссылка в палитру <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 = 'Русский'
# Теги, значение которых — многоязычная строка (<v8:item> на язык), а не скаляр.
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 ---
# Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент <columns>.
# Их бывает несколько: раскладка БЕЗ <id> — умолчание (ровно одна в каждом макете корпуса),
# остальные адресуются GUID из <id>, на который ссылаются строки (<row><columnsID>) и
# области (<area><columnsID>). Раньше читался только первый <columns> — отсюда терялись
# ширины и вылезал 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
# Пустой <tl/> — отдельное состояние: тег текста есть, языков в нём нет. Без
# этого ветвления такая ячейка теряла текст вовсе (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