fix(mxl-decompile): свести порты — пробелы, порядок и свойства значения ячейки

Расхождение JSON между портами было 26 макетов из 40, стало 5. Расхождение
собранного XML — 20 из 40, стало 7. Причин оказалось четыре, и три из них
дефекты, а не просто разнобой.

1. ps1 грузил XML с PreserveWhitespace = $false, и текст ячейки из одних
   пробелов схлопывался в пустой. Молчаливая потеря данных в каноничном
   порте, 25 макетов пилота.

2. Стили обнаруживались обходом хэш-таблицы строк, а её порядок в PowerShell
   НЕ определён. Платформа же кладёт записи палитры в порядке документа, так
   что порядок обхода — часть верности вывода, а не деталь. Обход теперь по
   возрастанию номера строки. ([ordered] тут не годится: с целочисленными
   ключами он индексируется по позиции, а не по ключу.)

3. Именованные области сортировались нестабильным Sort-Object (-Stable
   появился только в PowerShell 6.2), поэтому области с одинаковыми границами
   получали произвольный порядок. Добавлен явный ключ исходного порядка —
   в оба порта, чтобы совпадение было по построению, а не по совпадению.

4. containsValue / valueType / controlType протекали в styles сквозным
   пробросом неизвестных тегов. Это свойства ЗНАЧЕНИЯ ячейки, а не оформления,
   и компилятор таких ключей не знает — в DSL они были чистым шумом. Заодно
   вложенный элемент больше не читается как скаляр: ps1 брал InnerText и
   получал склейку поддерева, py брал .text и получал пустоту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 21:40:44 +03:00
co-authored by Claude Opus 5
parent e46db618e3
commit c11f4837fd
2 changed files with 37 additions and 9 deletions
@@ -1,4 +1,4 @@
# mxl-decompile v1.16 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.17 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -19,7 +19,9 @@ if (-not (Test-Path $TemplatePath)) {
}
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
# PreserveWhitespace = $false схлопывает узел из одних пробелов в пустой, и текст ячейки
# вида " " терялся молча (25 макетов пилота). Значащие пробелы в тексте надписи есть.
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
$root = $xmlDoc.DocumentElement
@@ -139,6 +141,9 @@ foreach ($fmtNode in $root.SelectNodes("d:format", $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 }
@@ -246,6 +251,10 @@ foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) {
}
$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'
@@ -368,7 +377,11 @@ function Get-DslText {
# Свойства формата, которые стилем НЕ являются: ширина принадлежит колонке, высота —
# строке, вид заполнения выводится из того, чем задано содержимое ячейки.
$nonStyleTags = @('width', 'height', 'fillType', 'font')
# containsValue/valueType/controlType — свойства ЗНАЧЕНИЯ ячейки, а не оформления: стиль
# общий на многие ячейки, а они индивидуальны. Компилятор таких ключей не знает, так что
# в DSL они были чистым шумом.
$nonStyleTags = @('width', 'height', 'fillType', 'font',
'containsValue', 'valueType', 'controlType')
$borderTags = @('border', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder')
function ConvertTo-DslLine {
@@ -492,7 +505,11 @@ for ($i = 1; $i -lt $rawFonts.Count; $i++) {
$styleKeys = [ordered]@{}
$formatToStyleKey = @{}
foreach ($r in $rowData.Values) {
# Обход по ВОЗРАСТАНИЮ номера строки: у обычной хэш-таблицы порядок в 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 }
@@ -520,7 +537,7 @@ function Get-RowStyleFmt {
}
# Строка — тоже владелец формата: её оформление становится именованным стилем.
foreach ($rd in $rowData.Values) {
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 }
@@ -844,7 +861,7 @@ function Test-UniformColumnSet {
$blockAreas = @()
$overlayAreas = @()
$claimed = @{} # строка → занята областью-диапазоном
foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{ Expression = { $_.EndRow } })) {
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++) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.16 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.17 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -308,6 +308,10 @@ def main():
if val:
fmt["Props"][tag] = val
continue
# Вложенный элемент скаляром не является: .text дал бы пустоту, а ps1-порт брал
# InnerText и получал склейку поддерева — отсюда расхождение портов.
if len(child):
continue
val = (child.text or "").strip()
if not val:
continue
@@ -409,6 +413,9 @@ def main():
return int_of(n) if n is not None else -1
named_areas.append({
# Порядок в исходнике — явный ключ сортировки: sorted тут стабилен, а
# Sort-Object в ps1 нет, и без него порты расходились.
"Ord": len(named_areas),
"Name": text_of(find(ni_node, "d:name")) or "",
"Type": text_of(find(area_node, "d:type")) or "",
"BeginRow": coord("beginRow"),
@@ -535,7 +542,11 @@ def main():
# Свойства формата, которые стилем НЕ являются: ширина принадлежит колонке, высота —
# строке, вид заполнения выводится из того, чем задано содержимое ячейки.
NON_STYLE_TAGS = ("width", "height", "fillType", "font")
# containsValue/valueType/controlType — свойства ЗНАЧЕНИЯ ячейки, а не оформления: стиль
# общий на многие ячейки, а они индивидуальны. Компилятор таких ключей не знает, так что
# в DSL они были чистым шумом.
NON_STYLE_TAGS = ("width", "height", "fillType", "font",
"containsValue", "valueType", "controlType")
BORDER_TAGS = ("border", "leftBorder", "topBorder", "rightBorder", "bottomBorder")
def line_to_dsl(idx):
@@ -853,7 +864,7 @@ def main():
block_areas = []
overlay_areas = []
claimed = set()
for a in sorted(named_areas, key=lambda x: (x["BeginRow"], x["EndRow"])):
for a in sorted(named_areas, key=lambda x: (x["BeginRow"], x["EndRow"], x["Ord"])):
fits = a["Type"] == "Rows" and a["BeginRow"] >= 0 and a["EndRow"] >= a["BeginRow"]
if fits:
for r in range(a["BeginRow"], a["EndRow"] + 1):