feat(mxl-compile): прощающий ввод — ячейка без col

Ячейка без col роняла py голым KeyError, а ps1 молча брал null как 0 и писал
Col = -1 — битую ячейку без единого сообщения. При этом опустить col модели
естественно: DSL устроен как HTML-таблица (rows → cells), и одиночный заголовок
или обычная строка пишутся без позиций.

Строка, в которой col нет НИ У ОДНОЙ ячейки, теперь раскладывается слева направо
с учётом span и занятых сверху rowspan-колонок. Смешанную строку не угадываем —
это опечатка; переполнение columns и явный col вне 1..columns тоже дают внятную
ошибку в stderr вместо тихой порчи.

Документация не менялась: col остаётся единственной каноничной формой, прощающий
ввод живёт только в коде — как регистронезависимость ключей DSL. Второй способ
адресации в SKILL.md превратил бы одно правило в развилку.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-10 13:04:03 +03:00
co-authored by Claude Opus 5
parent 863c7ebbff
commit f33436adff
7 changed files with 360 additions and 2 deletions
@@ -1,4 +1,4 @@
# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# mxl-compile v1.15 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -652,6 +652,49 @@ foreach ($area in $def.areas) {
if ($row.cells -and $row.cells.Count -gt 0) {
$rowHasContent = $true
# Прощающий ввод: строка, в которой НИ У ОДНОЙ ячейки нет col, раскладывается
# слева направо с учётом span и rowspan сверху. Канон один и он в документации —
# col обязателен; здесь мы лишь спасаем естественный DSL вместо тихой порчи
# ($null -> [int]0 -> Col = -1). Смешанную строку не угадываем: это опечатка.
$positioned = @($row.cells | Where-Object {
$_.PSObject.Properties['col'] -and $null -ne $_.col -and "$($_.col)" -ne ""
})
if ($positioned.Count -eq 0) {
$cursor = 1
foreach ($cell in $row.cells) {
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
while ($true) {
$isFree = $true
for ($c = $cursor; $c -lt ($cursor + $colSpan); $c++) {
if ($rowspanOccupied[$c]) { $isFree = $false; break }
}
if ($isFree) { break }
$cursor++
}
if (($cursor + $colSpan - 1) -gt $totalColumns) {
Write-Error "Row exceeds 'columns' ($totalColumns): area `"$areaName`", row $($localRow + 1)"
exit 1
}
$cell | Add-Member -NotePropertyName col -NotePropertyValue $cursor -Force
$cursor += $colSpan
}
} elseif ($positioned.Count -ne $row.cells.Count) {
Write-Error "Cell without 'col' mixed with positioned cells: area `"$areaName`", row $($localRow + 1)"
exit 1
}
# Позиция обязана быть в 1..columns: до этой проверки нечисловой или нулевой col
# молча превращался в Col = -1 и давал битую ячейку без единого сообщения.
$cellIdx = 0
foreach ($cell in $row.cells) {
$cellIdx++
$colParsed = 0
if (-not [int]::TryParse("$($cell.col)", [ref]$colParsed) -or $colParsed -lt 1 -or $colParsed -gt $totalColumns) {
Write-Error "Invalid 'col' value `"$($cell.col)`": area `"$areaName`", row $($localRow + 1), cell $cellIdx"
exit 1
}
}
# Build set of occupied columns (1-based): explicit cells + rowspan from above
$occupiedCols = @{}
foreach ($rsk in $rowspanOccupied.Keys) { $occupiedCols[$rsk] = $true }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# mxl-compile v1.15 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -297,6 +297,21 @@ def format_rank(ver):
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def parse_col_value(val):
"""Позиция колонки как целое, иначе None. Аналог [int]::TryParse в ps1:
целое из JSON приходит int, "3" — строкой, 3.0 — float (ps1 печатает такое как "3")."""
if isinstance(val, bool) or val is None:
return None
if isinstance(val, int):
return val
if isinstance(val, float):
return int(val) if val.is_integer() else None
try:
return int(str(val).strip())
except (TypeError, ValueError):
return None
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -690,6 +705,38 @@ def main():
if row.get('cells') and len(row['cells']) > 0:
row_has_content = True
# Прощающий ввод: строка, в которой НИ У ОДНОЙ ячейки нет col, раскладывается
# слева направо с учётом span и rowspan сверху. Канон один и он в документации —
# col обязателен; здесь мы лишь спасаем естественный DSL вместо тихой порчи
# (в ps1 $null -> [int]0 -> Col = -1). Смешанную строку не угадываем: это опечатка.
positioned = [c for c in row['cells']
if 'col' in c and c.get('col') is not None and str(c.get('col')) != '']
if len(positioned) == 0:
cursor = 1
for cell in row['cells']:
col_span = int(cell.get('span', 1))
while any(c in rowspan_occupied for c in range(cursor, cursor + col_span)):
cursor += 1
if cursor + col_span - 1 > total_columns:
print(f'Row exceeds \'columns\' ({total_columns}): area "{area_name}",'
f' row {local_row + 1}', file=sys.stderr)
sys.exit(1)
cell['col'] = cursor
cursor += col_span
elif len(positioned) != len(row['cells']):
print(f'Cell without \'col\' mixed with positioned cells: area "{area_name}",'
f' row {local_row + 1}', file=sys.stderr)
sys.exit(1)
# Позиция обязана быть в 1..columns: до этой проверки нечисловой или нулевой col
# ронял py голым KeyError/ValueError, а ps1 молча писал Col = -1.
for cell_idx, cell in enumerate(row['cells'], start=1):
col_parsed = parse_col_value(cell.get('col'))
if col_parsed is None or col_parsed < 1 or col_parsed > total_columns:
print(f'Invalid \'col\' value "{cell.get("col")}": area "{area_name}",'
f' row {local_row + 1}, cell {cell_idx}', file=sys.stderr)
sys.exit(1)
# Build set of occupied columns (1-based)
occupied_cols = dict(rowspan_occupied)
for cell in row['cells']: