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

Именованная область ссылается на дополнительный набор колонок тегом
columnsID, и эта ссылка терялась целиком: на пилоте кампании она давала
3572 расхождения из 3572 в своей категории, на корпусе таких областей
913 483 прямоугольных, 20 063 полосы строк и 743 полосы колонок.

Ключ namedAreas[].columnSet имеет три состояния: ключа нет — привязка
выводится из накрытых строк (совпадает у 1 021 570 областей корпуса из
1 044 339), "" — привязки нет вовсе, имя набора — явная привязка.
Переопределение обязательно: у областей типа Rows 13 623 повторяют
раскладку строк, а 8 179 её не несут при тех же строках.

Декомпилятор пишет ключ, только когда он не выводится, а область, чья
привязка расходится с раскладкой её строк, уводит из блочной формы в
namedAreas — иначе блоком её не выразить.

Пустая строка тоже попадает в карту «строка → раскладка»: без этого
привязка области, накрывающей пустые строки, не выводилась.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-15 17:49:24 +03:00
co-authored by Claude Opus 5
parent 702a3b6a8d
commit b32bb5899b
11 changed files with 278 additions and 14 deletions
@@ -111,6 +111,7 @@
| `name` | да | Имя области |
| `rows` | \* | Строки: число или диапазон `"N-M"`, 1-based |
| `cols` | \* | Колонки: число или диапазон `"N-M"`, 1-based |
| `columnSet` | нет | Колоночная раскладка области. Без ключа выводится из накрытых строк, `""` — привязки нет |
\* Обязательна хотя бы одна из осей.
@@ -1,4 +1,4 @@
# mxl-compile v1.45 — Compile 1C spreadsheet from JSON
# mxl-compile v1.46 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1703,6 +1703,9 @@ foreach ($layout in $columnLayouts) {
$globalRow = 0
$merges = @()
$namedItems = @()
# Строка документа → колоночная раскладка. Нужна для вывода привязки именованной области:
# на корпусе она совпадает с раскладкой накрытых строк у 1 021 570 областей из 1 044 339.
$rowColumnSet = @{}
$totalRowCount = 0
# Копилка подряд идущих одинаковых пустых строк — пишется одним rowsItem с indexTo.
@@ -1938,6 +1941,9 @@ foreach ($area in $def.areas) {
# непустые не схлопывает, даже когда они совпадают. Поэтому пустую строку не пишем
# сразу, а копим в пробеле и сбрасываем, когда он оборвался.
if (-not $rowHasContent) {
# Пустая строка тоже принадлежит раскладке: без этой записи привязка именованной
# области, накрывающей пустые строки, не выводилась.
$rowColumnSet[$globalRow] = $areaColumnSet
Add-GapRow -key "$areaColumnSet|$rowFormatIdx" -row $globalRow `
-columnSet $areaColumnSet -formatIdx $rowFormatIdx
$localRow++
@@ -1950,6 +1956,7 @@ foreach ($area in $def.areas) {
X "`t`t<index>$globalRow</index>"
X "`t`t<row>"
$rowColumnSet[$globalRow] = $areaColumnSet
if ($areaColumnSet) {
X "`t`t`t<columnsID>$areaColumnSet</columnsID>"
}
@@ -2028,11 +2035,12 @@ foreach ($area in $def.areas) {
# Имя на блоке — сахар: разворачиваем его в обычную именованную область типа Rows.
if (-not [string]::IsNullOrEmpty($areaName)) {
$namedItems += @{
Name = $areaName
BeginRow = $areaStartRow
EndRow = $areaEndRow
BeginCol = -1
EndCol = -1
Name = $areaName
BeginRow = $areaStartRow
EndRow = $areaEndRow
BeginCol = -1
EndCol = -1
ColumnSet = $areaColumnSet
}
}
}
@@ -2125,13 +2133,38 @@ if ($def.namedAreas) {
[Console]::Error.WriteLine("namedAreas: at least one of 'rows'/'cols' is required: $where")
exit 1
}
# Привязка к колоночной раскладке: своим ключом либо выводится из накрытых строк.
# Три состояния: ключа нет → выводим из накрытых строк; "" → привязки нет вовсе;
# имя → явная привязка. На корпусе область типа Rows в 13 623 случаях повторяет
# раскладку строк и в 8 179 её не несёт, поэтому вывод обязан переопределяться.
$naSet = ''
$naHasKey = [bool]$na.PSObject.Properties['columnSet']
if ($naHasKey -and -not "$($na.columnSet)") {
$naSet = ''
} elseif ($naHasKey) {
$naSetName = "$($na.columnSet)"
$naLayout = @($columnLayouts | Where-Object { $_.Name -eq $naSetName })[0]
if (-not $naLayout) {
[Console]::Error.WriteLine("namedAreas: unknown 'columnSet' `"$naSetName`": $where")
exit 1
}
$naSet = $naLayout.Id
} elseif ($rows) {
$sets = @()
for ($r = $rows.From - 1; $r -le $rows.To - 1; $r++) {
if ($rowColumnSet.ContainsKey($r)) { $sets += $rowColumnSet[$r] }
}
$uniq = @($sets | Select-Object -Unique)
if ($uniq.Count -eq 1) { $naSet = $uniq[0] }
}
# DSL 1-based, XML 0-based; отсутствующая ось помечается -1.
$namedItems += @{
Name = $naName
BeginRow = if ($rows) { $rows.From - 1 } else { -1 }
EndRow = if ($rows) { $rows.To - 1 } else { -1 }
BeginCol = if ($cols) { $cols.From - 1 } else { -1 }
EndCol = if ($cols) { $cols.To - 1 } else { -1 }
Name = $naName
BeginRow = if ($rows) { $rows.From - 1 } else { -1 }
EndRow = if ($rows) { $rows.To - 1 } else { -1 }
BeginCol = if ($cols) { $cols.From - 1 } else { -1 }
EndCol = if ($cols) { $cols.To - 1 } else { -1 }
ColumnSet = $naSet
}
}
}
@@ -2302,6 +2335,7 @@ foreach ($ni in $sortedNamedItems) {
X "`t`t`t<endRow>$($ni.EndRow)</endRow>"
X "`t`t`t<beginColumn>$($ni.BeginCol)</beginColumn>"
X "`t`t`t<endColumn>$($ni.EndCol)</endColumn>"
if ($ni.ColumnSet) { X "`t`t`t<columnsID>$($ni.ColumnSet)</columnsID>" }
X "`t`t</area>"
X "`t</namedItem>"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.45 — Compile 1C spreadsheet from JSON
# mxl-compile v1.46 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -1644,6 +1644,9 @@ def main():
global_row = 0
merges = []
named_items = []
# Строка документа → колоночная раскладка. Нужна для вывода привязки именованной области:
# на корпусе она совпадает с раскладкой накрытых строк у 1 021 570 областей из 1 044 339.
row_column_set = {}
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
# Копилка подряд идущих одинаковых пустых строк — пишется одним rowsItem с indexTo.
@@ -1857,6 +1860,9 @@ def main():
# непустые не схлопывает, даже когда они совпадают. Поэтому пустую строку не пишем
# сразу, а копим в пробеле и сбрасываем, когда он оборвался.
if not row_has_content:
# Пустая строка тоже принадлежит раскладке: без этой записи привязка
# именованной области, накрывающей пустые строки, не выводилась.
row_column_set[global_row] = area_column_set
add_gap_row(f'{area_column_set}|{row_format_idx}', global_row,
area_column_set, row_format_idx)
local_row += 1
@@ -1868,6 +1874,7 @@ def main():
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
row_column_set[global_row] = area_column_set
if area_column_set:
lines.append(f'\t\t\t<columnsID>{area_column_set}</columnsID>')
@@ -1945,6 +1952,7 @@ def main():
'EndRow': area_end_row,
'BeginCol': -1,
'EndCol': -1,
'ColumnSet': area_column_set,
})
flush_gap()
@@ -2027,6 +2035,23 @@ def main():
if not rows and not cols:
print(f'namedAreas: at least one of \'rows\'/\'cols\' is required: {where}', file=sys.stderr)
sys.exit(1)
# Привязка к колоночной раскладке. Три состояния: ключа нет → выводим из накрытых
# строк; "" → привязки нет вовсе; имя → явная. На корпусе область типа Rows в 13 623
# случаях повторяет раскладку строк и в 8 179 её не несёт, поэтому вывод обязан
# переопределяться.
na_set = ''
if 'columnSet' in na:
na_set_name = str(na.get('columnSet') or '')
if na_set_name:
na_layout = next((x for x in column_layouts if x['Name'] == na_set_name), None)
if na_layout is None:
print(f"namedAreas: unknown 'columnSet' \"{na_set_name}\": {where}", file=sys.stderr)
sys.exit(1)
na_set = na_layout['Id']
elif rows:
covered = {row_column_set[r] for r in range(rows[0] - 1, rows[1]) if r in row_column_set}
if len(covered) == 1:
na_set = covered.pop()
# DSL 1-based, XML 0-based; отсутствующая ось помечается -1.
named_items.append({
'Name': na_name,
@@ -2034,6 +2059,7 @@ def main():
'EndRow': rows[1] - 1 if rows else -1,
'BeginCol': cols[0] - 1 if cols else -1,
'EndCol': cols[1] - 1 if cols else -1,
'ColumnSet': na_set,
})
# 7d-ter. Колонтитулы: шесть слотов идут после строк и перед скалярными свойствами документа.
@@ -2185,6 +2211,8 @@ def main():
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
lines.append(f'\t\t\t<beginColumn>{ni.get("BeginCol", -1)}</beginColumn>')
lines.append(f'\t\t\t<endColumn>{ni.get("EndCol", -1)}</endColumn>')
if ni.get('ColumnSet'):
lines.append(f'\t\t\t<columnsID>{ni["ColumnSet"]}</columnsID>')
lines.append('\t\t</area>')
lines.append('\t</namedItem>')
@@ -1,4 +1,4 @@
# mxl-decompile v1.25 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.26 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -459,6 +459,9 @@ foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) {
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 { '' })
}
}
@@ -1140,6 +1143,13 @@ foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{
}
}
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
@@ -1561,6 +1571,18 @@ if ($overlayAreas.Count -gt 0) {
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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.25 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.26 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -609,6 +609,9 @@ def main():
"EndRow": coord("endRow"),
"BeginCol": coord("beginColumn"),
"EndCol": coord("endColumn"),
# Привязка области к колоночной раскладке: у 913 483 прямоугольных областей корпуса
# она есть, и из накрытых строк выводится не всегда.
"ColumnsId": text_of(find(area_node, "d:columnsID")) or "",
})
# --- 8. Extract rows ---
@@ -1130,6 +1133,11 @@ def main():
break
if fits:
fits = uniform_column_set(a["BeginRow"], a["EndRow"])
if fits:
# Блок задаёт раскладку строкам, и область наследует её же. Когда область несёт
# ДРУГУЮ привязку, блоком её не выразить — уводим в namedAreas, где привязка пишется
# явным ключом.
fits = a["ColumnsId"] == (row_columns_id(a["BeginRow"]) or "")
if fits:
claimed.update(range(a["BeginRow"], a["EndRow"] + 1))
block_areas.append(a)
@@ -1558,6 +1566,14 @@ def main():
if a["BeginCol"] >= 0:
entry["cols"] = (f'{a["BeginCol"] + 1}-{a["EndCol"] + 1}'
if a["EndCol"] > a["BeginCol"] else a["BeginCol"] + 1)
# Привязку пишем, только когда она не выводится из накрытых строк.
derived = ""
if a["BeginRow"] >= 0:
covered = {row_columns_id(r) or "" for r in range(a["BeginRow"], a["EndRow"] + 1)}
if len(covered) == 1:
derived = covered.pop()
if a["ColumnsId"] != derived:
entry["columnSet"] = a["ColumnsId"]
na_out.append(entry)
result["namedAreas"] = na_out