feat(mxl-compile,mxl-decompile): безымянные блоки и области координатами

Инкремент A кампании mxl-roundtrip. Блочная форма DSL не выражала больше половины
корпуса: у 34% макетов ERP есть строки вне именованных областей, у 21% нет ни одной
области типа Rows. Декомпилятор такие строки терял, а на макетах целиком из
Rectangle отдавал areas: [] — компилятор отвечал "Required field 'areas' is missing".
На пилоте из 40 макетов это 10 отказов из 26.

Что сделано:

- имя у блока стало необязательным. Блок без имени — просто кусок сетки; именованную
  область он не создаёт. Отдельный «плоский режим» не нужен: макет без выразимых
  блоков это один безымянный блок;

- namedAreas — именованные области координатами, для всего, что блоком не ложится
  (не-Rows и пересекающиеся). Тип области НЕ указывается: он выводится из заданных
  осей, ровно как в ТабличныйДокумент.Область() — только строки дают полосу строк,
  только колонки полосу колонок, обе оси прямоугольник. Так нельзя написать
  противоречие вроде type: Rows с колоночными координатами;

- диапазон записывается уже существующей грамматикой DSL (как ключи columnWidths):
  число или "N-M". Список через запятую запрещён — область непрерывна, платформа
  разрывную не хранит;

- прощающим вводом принимается платформенный адрес "R1C1:R2C2" и правило «0 значит 1»;
  в документацию не вынесено;

- декомпилятор перестал пропускать области не-Rows (там стоял безусловный continue) и
  режет сетку на блоки детерминированно: непересекающиеся Rows задают границы, дыры
  становятся безымянными блоками, остальное уходит в namedAreas.

Отдельно: именованные элементы теперь эмитятся отсортированными по имени. Платформа
хранит их именно так — на выборке 541 макета с несколькими элементами иного порядка
нет ни разу. Сортировка ординальная и регистронезависимая; Sort-Object по умолчанию
сортирует по текущей культуре и на кириллице дал бы другой порядок. Отсюда дрейф 13
снэпшотов — чистая перестановка, диффы симметричны, число элементов не изменилось.

На пилоте отказы areas: [] закрыты полностью (10 → 0), цикл переживают 24 макета
вместо 14. Оставшиеся 16 отказов — колоночные раскладки, это инкремент B.

Правка на ps1, зазеркалена в py; вывод портов и в компиляции, и в декомпиляции
совпадает байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-10 16:48:33 +03:00
co-authored by Claude Opus 5
parent f6a478c85c
commit 720ea05325
26 changed files with 977 additions and 188 deletions
@@ -1,4 +1,4 @@
# mxl-decompile v1.3 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.4 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -161,19 +161,29 @@ foreach ($mNode in $root.SelectNodes("d:merge", $ns)) {
# --- 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)
$areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText
if ($areaType -ne "Rows") { continue }
if (-not $areaNode) { continue }
$getCoord = {
param([string]$tag)
$n = $areaNode.SelectSingleNode("d:$tag", $ns)
if ($n) { return [int]$n.InnerText }
return -1
}
$namedAreas += @{
Name = $niNode.SelectSingleNode("d:name", $ns).InnerText
BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText
EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText
Type = $areaNode.SelectSingleNode("d:type", $ns).InnerText
BeginRow = & $getCoord 'beginRow'
EndRow = & $getCoord 'endRow'
BeginCol = & $getCoord 'beginColumn'
EndCol = & $getCoord 'endColumn'
}
}
@@ -434,9 +444,50 @@ function Get-StyleName {
# --- 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 } }
$blockAreas = @()
$overlayAreas = @()
$claimed = @{} # строка → занята блоком
foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{ Expression = { $_.EndRow } })) {
$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) {
for ($r = $a.BeginRow; $r -le $a.EndRow; $r++) { $claimed[$r] = $true }
$blockAreas += $a
} else {
$overlayAreas += $a
}
}
# Блоки в порядке строк + безымянные заполнители дыр.
$blocks = @()
$cursor = 0
foreach ($a in @($blockAreas | Sort-Object @{ Expression = { $_.BeginRow } })) {
if ($a.BeginRow -gt $cursor) {
$blocks += @{ Name = $null; BeginRow = $cursor; EndRow = $a.BeginRow - 1 }
}
$blocks += @{ Name = $a.Name; BeginRow = $a.BeginRow; EndRow = $a.EndRow }
$cursor = $a.EndRow + 1
}
if ($cursor -le $maxRowIdx) {
$blocks += @{ Name = $null; BeginRow = $cursor; EndRow = $maxRowIdx }
}
$dslAreas = @()
foreach ($area in $namedAreas) {
foreach ($area in $blocks) {
$areaRows = @()
for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) {
@@ -557,10 +608,11 @@ foreach ($area in $namedAreas) {
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
}
$dslAreas += [ordered]@{
name = $area.Name
rows = [array]$compressedRows
}
$dslBlock = [ordered]@{}
# Безымянный блок — просто кусок сетки, ключ name у него не пишем.
if (-not [string]::IsNullOrEmpty($area.Name)) { $dslBlock["name"] = $area.Name }
$dslBlock["rows"] = [array]$compressedRows
$dslAreas += $dslBlock
}
# --- 13. Compress columnWidths ---
@@ -632,6 +684,23 @@ $result["fonts"] = $fontsOut
$result["styles"] = $styleDefs
$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 ---
# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline.