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
+5
View File
@@ -127,6 +127,11 @@
Типичное применение: сложные печатные формы (УПД, УКД), где шапка/подвал/табличная часть имеют разную разбивку на колонки.
Именованная область тоже ссылается на набор — тегом `<columnsID>` внутри `<area>`, последним.
На корпусе привязку несут 913 483 прямоугольных области, 20 063 полосы строк и 743 полосы колонок;
висячих ссылок нет ни одной. Из накрытых строк она выводится не всегда: у областей типа `Rows`
13 623 повторяют раскладку строк, а 8 179 её не несут при тех же строках.
## Строки и ячейки
### Строка
+1
View File
@@ -111,6 +111,7 @@
| `name` | да | Имя области |
| `rows` | \* | Строки: число или диапазон `"N-M"`, 1-based |
| `cols` | \* | Колонки: число или диапазон `"N-M"`, 1-based |
| `columnSet` | нет | Колоночная раскладка области. Без ключа выводится из накрытых строк, `""` — привязки нет |
\* Обязательна хотя бы одна из осей.
@@ -0,0 +1,17 @@
{
"name": "Именованная область с привязкой к колоночной раскладке",
"input": {
"columns": 3,
"columnSets": { "узкая": { "columns": 2, "columnWidths": { "1-2": 20 } } },
"areas": [
{ "name": "Шапка", "rows": [["Заголовок"]] },
{ "name": "Таблица", "columnSet": "узкая", "rows": [["Товар", "Цена"]] }
],
"namedAreas": [
{ "name": "ПоУзкой", "rows": 2, "cols": "1-2" },
{ "name": "БезПривязки", "rows": 2, "cols": 1, "columnSet": "" },
{ "name": "ЯвнаяПривязка", "rows": 1, "cols": 1, "columnSet": "узкая" }
]
},
"params": { "outputPath": "Template.xml" }
}
@@ -126,6 +126,7 @@
<endRow>1</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
<columnsID>12320174-2b07-3806-9f7f-ec3834007fdb</columnsID>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
@@ -115,6 +115,7 @@
<endRow>1</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
<columnsID>01f06d2d-2103-3169-8cc8-32872f100129</columnsID>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>3</size>
</columns>
<columns>
<id>12320174-2b07-3806-9f7f-ec3834007fdb</id>
<size>2</size>
<columnsItem>
<index>0</index>
<column>
<formatIndex>1</formatIndex>
</column>
</columnsItem>
<columnsItem>
<index>1</index>
<column>
<formatIndex>1</formatIndex>
</column>
</columnsItem>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Заголовок</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<columnsID>12320174-2b07-3806-9f7f-ec3834007fdb</columnsID>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Товар</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Цена</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>2</defaultFormatIndex>
<height>2</height>
<vgRows>2</vgRows>
<namedItem xsi:type="NamedItemCells">
<name>БезПривязки</name>
<area>
<type>Rectangle</type>
<beginRow>1</beginRow>
<endRow>1</endRow>
<beginColumn>0</beginColumn>
<endColumn>0</endColumn>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
<name>ПоУзкой</name>
<area>
<type>Rectangle</type>
<beginRow>1</beginRow>
<endRow>1</endRow>
<beginColumn>0</beginColumn>
<endColumn>1</endColumn>
<columnsID>12320174-2b07-3806-9f7f-ec3834007fdb</columnsID>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
<name>Таблица</name>
<area>
<type>Rows</type>
<beginRow>1</beginRow>
<endRow>1</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
<columnsID>12320174-2b07-3806-9f7f-ec3834007fdb</columnsID>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
<name>Шапка</name>
<area>
<type>Rows</type>
<beginRow>0</beginRow>
<endRow>0</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
<name>ЯвнаяПривязка</name>
<area>
<type>Rectangle</type>
<beginRow>0</beginRow>
<endRow>0</endRow>
<beginColumn>0</beginColumn>
<endColumn>0</endColumn>
<columnsID>12320174-2b07-3806-9f7f-ec3834007fdb</columnsID>
</area>
</namedItem>
<format>
<width>20</width>
</format>
<format>
<width>10</width>
</format>
</document>