diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 676507cd..422722bd 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -1,4 +1,4 @@ -# mxl-compile v1.18 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# mxl-compile v1.19 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -192,11 +192,15 @@ if (-not (Test-Path $JsonPath)) { $json = Get-Content -Raw -Encoding UTF8 $JsonPath $def = $json | ConvertFrom-Json -if (-not $def.columns) { +# Проверяем НАЛИЧИЕ ключа, а не истинность значения: `columns: 0` — осмысленная величина +# (раскладка по умолчанию пустая, все строки живут в именованных раскладках), а пустой +# список областей встречается у макета без строк. Прежняя проверка `-not` объявляла и то +# и другое отсутствующим. +if (-not $def.PSObject.Properties['columns'] -or $null -eq $def.columns) { Write-Error "Required field 'columns' is missing" exit 1 } -if (-not $def.areas) { +if (-not $def.PSObject.Properties['areas'] -or $null -eq $def.areas) { Write-Error "Required field 'areas' is missing" exit 1 } @@ -363,18 +367,40 @@ if ($def.page) { } # Build column width map: 1-based col -> width -$colWidthMap = @{} -if ($def.columnWidths) { - foreach ($prop in $def.columnWidths.PSObject.Properties) { - $val = "$($prop.Value)" - if ($val -match '^([0-9.]+)x$') { - $width = [int][math]::Round([double]$Matches[1] * $defaultWidth) - } else { - $width = [int]$val +function Build-ColWidthMap { + param($widths) + $map = @{} + if ($widths) { + foreach ($prop in $widths.PSObject.Properties) { + $val = "$($prop.Value)" + if ($val -match '^([0-9.]+)x$') { + $width = [int][math]::Round([double]$Matches[1] * $defaultWidth) + } else { + $width = [int]$val + } + foreach ($c in (Parse-ColumnSpec $prop.Name)) { $map[$c] = $width } } - $columns = Parse-ColumnSpec $prop.Name - foreach ($c in $columns) { - $colWidthMap[$c] = $width + } + return $map +} + +$colWidthMap = Build-ColWidthMap $def.columnWidths + +# Колоночные раскладки: документные columns/columnWidths — раскладка по умолчанию (в XML +# элемент БЕЗ , он всегда идёт первым). Дополнительные объявляются в +# columnSets, ключ — идентификатор, на него ссылается область ключом columnSet. +# Склейки по содержимому нет: в корпусе полно раскладок с одинаковым содержимым и разными +# идентификаторами, поэтому опознаёт раскладку только идентификатор. +$columnLayouts = @() +$columnLayouts += @{ Id = $null; Size = $totalColumns; Widths = $colWidthMap } +if ($def.columnSets) { + foreach ($prop in $def.columnSets.PSObject.Properties) { + $cs = $prop.Value + $size = if ($cs.columns) { [int]$cs.columns } else { $totalColumns } + $columnLayouts += @{ + Id = $prop.Name + Size = $size + Widths = Build-ColWidthMap $cs.columnWidths } } } @@ -482,14 +508,17 @@ function Register-Format { $defaultFormatKey = Get-FormatKey -width $defaultWidth $defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth } -# 6b. Column width formats -$colFormatMap = @{} # 1-based col -> format index -foreach ($col in ($colWidthMap.Keys | Sort-Object)) { - $w = $colWidthMap[$col] - $key = Get-FormatKey -width $w - $idx = Register-Format -key $key -props @{ Width = $w } - $colFormatMap[[int]$col] = $idx +# 6b. Column width formats — по одной карте на каждую колоночную раскладку +foreach ($layout in $columnLayouts) { + $map = @{} # 1-based col -> format index + foreach ($col in ($layout.Widths.Keys | Sort-Object)) { + $w = $layout.Widths[$col] + $key = Get-FormatKey -width $w + $map[[int]$col] = Register-Format -key $key -props @{ Width = $w } + } + $layout.FormatMap = $map } +$colFormatMap = $columnLayouts[0].FormatMap # 6c. Scan areas for row heights and cell formats # We need to do two passes: first collect all formats, then generate XML @@ -551,7 +580,7 @@ function Set-CellProp { } function Expand-ShorthandRow { - param($row, [string]$areaName, [int]$rowIdx, $openByCol) + param($row, [string]$areaName, [int]$rowIdx, $openByCol, [int]$maxCols) $cells = @() $placed = @{} # 1-based col -> ячейка, занимающая колонку в ЭТОЙ строке @@ -563,8 +592,8 @@ function Expand-ShorthandRow { $idx++ # Внутри функции пишем в stderr напрямую: Write-Error приписал бы к сообщению имя # функции, и текст перестал бы совпадать с py-портом. - if ($idx -gt $totalColumns) { - [Console]::Error.WriteLine("Row exceeds 'columns' ($totalColumns): area `"$areaName`", row $rowIdx") + if ($idx -gt $maxCols) { + [Console]::Error.WriteLine("Row exceeds 'columns' ($maxCols): area `"$areaName`", row $rowIdx") exit 1 } @@ -647,13 +676,19 @@ function Update-OpenByCol { foreach ($area in $def.areas) { $areaName = $area.name + # Ширина сетки берётся из раскладки области: у каждой она своя. + $areaMaxCols = $totalColumns + if ($area.PSObject.Properties['columnSet'] -and "$($area.columnSet)" -ne '') { + $lay = @($columnLayouts | Where-Object { $_.Id -eq "$($area.columnSet)" })[0] + if ($lay) { $areaMaxCols = [int]$lay.Size } + } $openByCol = @{} $rowIdx = 0 $expandedRows = @() foreach ($row in $area.rows) { $rowIdx++ if ($row -is [array]) { - $expandedRows += Expand-ShorthandRow -row $row -areaName $areaName -rowIdx $rowIdx -openByCol $openByCol + $expandedRows += Expand-ShorthandRow -row $row -areaName $areaName -rowIdx $rowIdx -openByCol $openByCol -maxCols $areaMaxCols } else { $expandedRows += $row if ($row.empty) { $openByCol.Clear() } else { Update-OpenByCol -row $row -openByCol $openByCol } @@ -721,23 +756,27 @@ X "`t`t" X "`t" # 7c. Columns -X "`t" -X "`t`t$totalColumns" +# Раскладка по умолчанию идёт первой и без — так их хранит платформа. +foreach ($layout in $columnLayouts) { + X "`t" + if ($layout.Id) { X "`t`t$($layout.Id)" } + X "`t`t$($layout.Size)" -# Emit columnsItem for columns with non-default widths -foreach ($col in ($colFormatMap.Keys | Sort-Object)) { - $fmtIdx = $colFormatMap[$col] - $colIdx = $col - 1 # Convert to 0-based - X "`t`t" - X "`t`t`t$colIdx" - X "`t`t`t" - X "`t`t`t`t$fmtIdx" - X "`t`t`t" - X "`t`t" + # Emit columnsItem for columns with non-default widths + foreach ($col in ($layout.FormatMap.Keys | Sort-Object)) { + $fmtIdx = $layout.FormatMap[$col] + $colIdx = $col - 1 # Convert to 0-based + X "`t`t" + X "`t`t`t$colIdx" + X "`t`t`t" + X "`t`t`t`t$fmtIdx" + X "`t`t`t" + X "`t`t" + } + + X "`t" } -X "`t" - # 7d. Rows — main generation loop $globalRow = 0 $merges = @() @@ -749,6 +788,19 @@ foreach ($area in $def.areas) { $areaName = $area.name $activeRowspans = @() # @{ColStart=1-based; ColEnd=1-based; EndLocalRow=int} $localRow = 0 + # Ссылка области на колоночную раскладку — её получают все строки области. + $areaColumnSet = if ($area.PSObject.Properties['columnSet']) { "$($area.columnSet)" } else { '' } + $areaLayout = $columnLayouts[0] + if ($areaColumnSet) { + $areaLayout = @($columnLayouts | Where-Object { $_.Id -eq $areaColumnSet })[0] + if (-not $areaLayout) { + [Console]::Error.WriteLine("Unknown 'columnSet': `"$areaColumnSet`" is not declared in columnSets") + exit 1 + } + } + # Ширина сетки — у КАЖДОЙ раскладки своя, поэтому позиции колонок сверяем с ней, + # а не с документным columns (у макетов с раскладками умолчание бывает и пустым). + $areaColumns = [int]$areaLayout.Size foreach ($row in $area.rows) { # Empty row placeholder: emit N empty rows @@ -813,8 +865,8 @@ foreach ($area in $def.areas) { if ($isFree) { break } $cursor++ } - if (($cursor + $colSpan - 1) -gt $totalColumns) { - Write-Error "Row exceeds 'columns' ($totalColumns): area `"$areaName`", row $($localRow + 1)" + if (($cursor + $colSpan - 1) -gt $areaColumns) { + Write-Error "Row exceeds 'columns' ($areaColumns): area `"$areaName`", row $($localRow + 1)" exit 1 } $cell | Add-Member -NotePropertyName col -NotePropertyValue $cursor -Force @@ -831,7 +883,7 @@ foreach ($area in $def.areas) { foreach ($cell in $row.cells) { $cellIdx++ $colParsed = 0 - if (-not [int]::TryParse("$($cell.col)", [ref]$colParsed) -or $colParsed -lt 1 -or $colParsed -gt $totalColumns) { + if (-not [int]::TryParse("$($cell.col)", [ref]$colParsed) -or $colParsed -lt 1 -or $colParsed -gt $areaColumns) { Write-Error "Invalid 'col' value `"$($cell.col)`": area `"$areaName`", row $($localRow + 1), cell $cellIdx" exit 1 } @@ -927,6 +979,10 @@ foreach ($area in $def.areas) { X "`t`t$globalRow" X "`t`t" + if ($areaColumnSet) { + X "`t`t`t$areaColumnSet" + } + if ($rowFormatIdx -gt 0) { X "`t`t`t$rowFormatIdx" } diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 648abed9..51905dff 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-compile v1.18 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# mxl-compile v1.19 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -348,10 +348,14 @@ def main(): with open(json_path, 'r', encoding='utf-8-sig') as f: defn = ci_json(json.load(f)) - if not defn.get('columns'): + # Проверяем НАЛИЧИЕ ключа, а не истинность значения: `columns: 0` — осмысленная величина + # (раскладка по умолчанию пустая, все строки живут в именованных раскладках), а пустой + # список областей встречается у макета без строк. Прежняя проверка объявляла и то + # и другое отсутствующим. + if 'columns' not in defn or defn.get('columns') is None: print("Required field 'columns' is missing", file=sys.stderr) sys.exit(1) - if not defn.get('areas'): + if 'areas' not in defn or defn.get('areas') is None: print("Required field 'areas' is missing", file=sys.stderr) sys.exit(1) @@ -473,18 +477,32 @@ def main(): default_width = round((target_width - absolute_sum) / total_units) # Build column width map: 1-based col -> width - col_width_map = {} - if defn.get('columnWidths'): - for prop_name, prop_value in defn['columnWidths'].items(): - val = str(prop_value) - m = re.match(r'^([0-9.]+)x$', val) - if m: - width = round(float(m.group(1)) * default_width) - else: - width = int(val) - columns = parse_column_spec(prop_name) - for c in columns: - col_width_map[c] = width + def build_col_width_map(widths): + out = {} + if widths: + for prop_name, prop_value in widths.items(): + val = str(prop_value) + m = re.match(r'^([0-9.]+)x$', val) + width = round(float(m.group(1)) * default_width) if m else int(val) + for c in parse_column_spec(prop_name): + out[c] = width + return out + + col_width_map = build_col_width_map(defn.get('columnWidths')) + + # Колоночные раскладки: документные columns/columnWidths — раскладка по умолчанию (в XML + # элемент БЕЗ , он всегда идёт первым). Дополнительные объявляются в + # columnSets, ключ — идентификатор, на него ссылается область ключом columnSet. + # Склейки по содержимому нет: в корпусе полно раскладок с одинаковым содержимым и разными + # идентификаторами, поэтому опознаёт раскладку только идентификатор. + column_layouts = [{'Id': None, 'Size': total_columns, 'Widths': col_width_map}] + for set_id, cs in (defn.get('columnSets') or {}).items(): + size = int(cs['columns']) if cs.get('columns') is not None else total_columns + column_layouts.append({ + 'Id': set_id, + 'Size': size, + 'Widths': build_col_width_map(cs.get('columnWidths')), + }) # --- 5. Style resolver --- def resolve_style(style_name, fill_type): @@ -560,13 +578,14 @@ def main(): default_format_key = get_format_key(width=default_width) default_format_index = register_format(default_format_key, {'Width': default_width}) - # 6b. Column width formats - col_format_map = {} # 1-based col -> format index - for col in sorted(col_width_map): - w = col_width_map[col] - key = get_format_key(width=w) - idx = register_format(key, {'Width': w}) - col_format_map[int(col)] = idx + # 6b. Column width formats — по одной карте на каждую колоночную раскладку + for layout in column_layouts: + fmap = {} # 1-based col -> format index + for col in sorted(layout['Widths']): + w = layout['Widths'][col] + fmap[int(col)] = register_format(get_format_key(width=w), {'Width': w}) + layout['FormatMap'] = fmap + col_format_map = column_layouts[0]['FormatMap'] # 6c. Helper: determine fillType from cell content def get_fill_type(cell): @@ -604,15 +623,15 @@ def main(): # "{Имя}" — параметр. Разворачиваем в обычную строку с явными col/span/rowspan, # поэтому весь код ниже про шорткат не знает. - def expand_shorthand_row(row, area_name, row_idx, open_by_col): + def expand_shorthand_row(row, area_name, row_idx, open_by_col, max_cols): cells = [] placed = {} # 1-based col -> ячейка, занимающая колонку в ЭТОЙ строке extended = [] # ячейки, чей rowspan уже нарастили в этой строке (span>1 даёт несколько "|") last = None # последняя реальная ячейка слева — цель для ">" for idx, el in enumerate(row, start=1): - if idx > total_columns: - print(f'Row exceeds \'columns\' ({total_columns}): area "{area_name}",' + if idx > max_cols: + print(f'Row exceeds \'columns\' ({max_cols}): area "{area_name}",' f' row {row_idx}', file=sys.stderr) sys.exit(1) @@ -693,11 +712,17 @@ def main(): for area in defn['areas']: area_name = area.get('name', '') + # Ширина сетки берётся из раскладки области: у каждой она своя. + area_max_cols = total_columns + if area.get('columnSet'): + lay = next((x for x in column_layouts if x['Id'] == str(area['columnSet'])), None) + if lay: + area_max_cols = int(lay['Size']) open_by_col = {} expanded_rows = [] for row_idx, row in enumerate(area.get('rows', []), start=1): if isinstance(row, list): - expanded_rows.append(expand_shorthand_row(row, area_name, row_idx, open_by_col)) + expanded_rows.append(expand_shorthand_row(row, area_name, row_idx, open_by_col, area_max_cols)) else: expanded_rows.append(row) if row.get('empty'): @@ -757,21 +782,25 @@ def main(): lines.append('\t') # 7c. Columns - lines.append('\t') - lines.append(f'\t\t{total_columns}') + # Раскладка по умолчанию идёт первой и без — так их хранит платформа. + for layout in column_layouts: + lines.append('\t') + if layout['Id']: + lines.append(f'\t\t{layout["Id"]}') + lines.append(f'\t\t{layout["Size"]}') - # Emit columnsItem for columns with non-default widths - for col in sorted(col_format_map.keys()): - fmt_idx = col_format_map[col] - col_idx = col - 1 # Convert to 0-based - lines.append('\t\t') - lines.append(f'\t\t\t{col_idx}') - lines.append('\t\t\t') - lines.append(f'\t\t\t\t{fmt_idx}') - lines.append('\t\t\t') - lines.append('\t\t') + # Emit columnsItem for columns with non-default widths + for col in sorted(layout['FormatMap'].keys()): + fmt_idx = layout['FormatMap'][col] + col_idx = col - 1 # Convert to 0-based + lines.append('\t\t') + lines.append(f'\t\t\t{col_idx}') + lines.append('\t\t\t') + lines.append(f'\t\t\t\t{fmt_idx}') + lines.append('\t\t\t') + lines.append('\t\t') - lines.append('\t') + lines.append('\t') # 7d. Rows -- main generation loop global_row = 0 @@ -784,6 +813,18 @@ def main(): area_name = area.get('name', '') active_rowspans = [] local_row = 0 + # Ссылка области на колоночную раскладку — её получают все строки области. + area_column_set = str(area.get('columnSet') or '') + area_layout = column_layouts[0] + if area_column_set: + area_layout = next((x for x in column_layouts if x['Id'] == area_column_set), None) + if area_layout is None: + print(f'Unknown \'columnSet\': "{area_column_set}" is not declared in columnSets', + file=sys.stderr) + sys.exit(1) + # Ширина сетки — у КАЖДОЙ раскладки своя, поэтому позиции колонок сверяем с ней, + # а не с документным columns (у макетов с раскладками умолчание бывает и пустым). + area_columns = int(area_layout['Size']) for row in area.get('rows', []): # Empty row placeholder: emit N empty rows @@ -832,8 +873,8 @@ def main(): 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}",' + if cursor + col_span - 1 > area_columns: + print(f'Row exceeds \'columns\' ({area_columns}): area "{area_name}",' f' row {local_row + 1}', file=sys.stderr) sys.exit(1) cell['col'] = cursor @@ -847,7 +888,7 @@ def main(): # ронял 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: + if col_parsed is None or col_parsed < 1 or col_parsed > area_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) @@ -933,6 +974,9 @@ def main(): lines.append(f'\t\t{global_row}') lines.append('\t\t') + if area_column_set: + lines.append(f'\t\t\t{area_column_set}') + if row_format_idx > 0: lines.append(f'\t\t\t{row_format_idx}') diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 7aa7c684..b7f792dd 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -1,4 +1,4 @@ -# mxl-decompile v1.4 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.5 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -117,15 +117,11 @@ function Get-Format { # --- 5. Extract columns and default width --- -$colNode = $root.SelectSingleNode("d:columns", $ns) -$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText - -$colFormatIndices = @{} -foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) { - $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText - $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText - $colFormatIndices[$colIdx] = $fmtIdx -} +# Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент . +# Их бывает несколько: раскладка БЕЗ — умолчание (ровно одна в каждом макете корпуса), +# остальные адресуются GUID из , на который ссылаются строки () и +# области (). Раньше читался только первый — отсюда терялись +# ширины и вылезал columns: 0. $defaultFmtIdx = 0 $n = $root.SelectSingleNode("d:defaultFormatIndex", $ns) @@ -137,16 +133,39 @@ if ($defaultFmtIdx -gt 0) { if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width } } -# Build column width map (1-based col → width), only non-default -$colWidthMap = [ordered]@{} -foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) { - $fmt = Get-Format $colFormatIndices[$col0] - if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { - $col1 = [string]($col0 + 1) - $colWidthMap.Add($col1, $fmt.Width) +function Read-ColumnSet { + param($node) + $byIdx = @{} + foreach ($ci in $node.SelectNodes("d:columnsItem", $ns)) { + $colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText + $fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText + $byIdx[$colIdx] = $fmtIdx + } + # Карта ширин (1-based колонка → ширина), только отличные от умолчания. + $widths = [ordered]@{} + foreach ($col0 in ($byIdx.Keys | Sort-Object)) { + $fmt = Get-Format $byIdx[$col0] + if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) { + $widths.Add([string]($col0 + 1), $fmt.Width) + } + } + $sizeNode = $node.SelectSingleNode("d:size", $ns) + $idNode = $node.SelectSingleNode("d:id", $ns) + return @{ + Id = if ($idNode) { $idNode.InnerText } else { $null } + Size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 } + Widths = $widths } } +$columnSets = @() +foreach ($cn in $root.SelectNodes("d:columns", $ns)) { $columnSets += Read-ColumnSet $cn } + +$defaultSet = @($columnSets | Where-Object { -not $_.Id })[0] +if (-not $defaultSet -and $columnSets.Count -gt 0) { $defaultSet = $columnSets[0] } +$totalColumns = if ($defaultSet) { $defaultSet.Size } else { 0 } +$colWidthMap = if ($defaultSet) { $defaultSet.Widths } else { [ordered]@{} } + # --- 6. Extract merges --- $mergeMap = @{} @@ -243,11 +262,17 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { } } + # Ссылка строки на колоночную раскладку; пусто = раскладка по умолчанию. + $rowColumnsId = $null + $cidNode = $rowNode.SelectSingleNode("d:columnsID", $ns) + if ($cidNode) { $rowColumnsId = $cidNode.InnerText } + for ($r = $rowIdx; $r -le $indexTo; $r++) { $rowData[$r] = @{ FormatIdx = $rowFmtIdx Cells = $cells Empty = $isEmpty + ColumnsId = $rowColumnsId } } } @@ -453,9 +478,26 @@ function Get-StyleName { $maxRowIdx = -1 foreach ($k in $rowData.Keys) { if ([int]$k -gt $maxRowIdx) { $maxRowIdx = [int]$k } } +function Get-RowColumnsId { + param([int]$r) + if ($rowData.ContainsKey($r)) { return $rowData[$r].ColumnsId } + return $null +} + +# Область годится в качестве области-диапазона, только если у всех её строк ОДНА раскладка: +# иначе её пришлось бы резать, а имя резать нельзя — такая уходит в координатный список. +function Test-UniformColumnSet { + param([int]$from, [int]$to) + $first = Get-RowColumnsId $from + for ($r = $from + 1; $r -le $to; $r++) { + if ((Get-RowColumnsId $r) -ne $first) { return $false } + } + return $true +} + $blockAreas = @() $overlayAreas = @() -$claimed = @{} # строка → занята блоком +$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) { @@ -463,6 +505,7 @@ foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{ if ($claimed.ContainsKey($r)) { $fitsBlock = $false; break } } } + if ($fitsBlock) { $fitsBlock = Test-UniformColumnSet $a.BeginRow $a.EndRow } if ($fitsBlock) { for ($r = $a.BeginRow; $r -le $a.EndRow; $r++) { $claimed[$r] = $true } $blockAreas += $a @@ -471,18 +514,37 @@ foreach ($a in @($namedAreas | Sort-Object @{ Expression = { $_.BeginRow } }, @{ } } -# Блоки в порядке строк + безымянные заполнители дыр. +# Безымянный промежуток режем на куски с одной раскладкой: границы наборов не совпадают +# с границами именованных областей, а у области должна быть ровно одна раскладка. +function Split-GapByColumnSet { + param([int]$from, [int]$to) + $out = @() + if ($to -lt $from) { return $out } + $runStart = $from + $runId = Get-RowColumnsId $from + for ($r = $from + 1; $r -le $to; $r++) { + $id = Get-RowColumnsId $r + if ($id -ne $runId) { + $out += @{ Name = $null; BeginRow = $runStart; EndRow = $r - 1; ColumnsId = $runId } + $runStart = $r; $runId = $id + } + } + $out += @{ Name = $null; BeginRow = $runStart; EndRow = $to; ColumnsId = $runId } + return $out +} + +# Области в порядке строк + безымянные заполнители дыр. $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 += Split-GapByColumnSet $cursor ($a.BeginRow - 1) } - $blocks += @{ Name = $a.Name; BeginRow = $a.BeginRow; EndRow = $a.EndRow } + $blocks += @{ Name = $a.Name; BeginRow = $a.BeginRow; EndRow = $a.EndRow; ColumnsId = (Get-RowColumnsId $a.BeginRow) } $cursor = $a.EndRow + 1 } if ($cursor -le $maxRowIdx) { - $blocks += @{ Name = $null; BeginRow = $cursor; EndRow = $maxRowIdx } + $blocks += Split-GapByColumnSet $cursor $maxRowIdx } $dslAreas = @() @@ -609,8 +671,11 @@ foreach ($area in $blocks) { } $dslBlock = [ordered]@{} - # Безымянный блок — просто кусок сетки, ключ name у него не пишем. + # Область без имени — просто кусок сетки, ключ name у неё не пишем. if (-not [string]::IsNullOrEmpty($area.Name)) { $dslBlock["name"] = $area.Name } + # Ссылка на колоночную раскладку по имени из columnSets — как style у ячейки на styles. + # Умолчание (раскладка без id) не пишем. + if (-not [string]::IsNullOrEmpty($area.ColumnsId)) { $dslBlock["columnSet"] = $area.ColumnsId } $dslBlock["rows"] = [array]$compressedRows $dslAreas += $dslBlock } @@ -682,6 +747,21 @@ foreach ($s in $toRemove) { $styleDefs.Remove($s) $result["fonts"] = $fontsOut $result["styles"] = $styleDefs + +# Колоночные раскладки помимо умолчания: ключ — идентификатор из макета, на него ссылаются +# области. Содержимое раскладку не опознаёт (в корпусе полно наборов с одинаковым +# содержимым и разными id), поэтому склейки по содержимому нет. +$extraSets = @($columnSets | Where-Object { $_.Id }) +if ($extraSets.Count -gt 0) { + $setsOut = [ordered]@{} + foreach ($cs in $extraSets) { + $entry = [ordered]@{ columns = $cs.Size } + if ($cs.Widths.Count -gt 0) { $entry["columnWidths"] = $cs.Widths } + $setsOut[$cs.Id] = $entry + } + $result["columnSets"] = $setsOut +} + $result["areas"] = [array]$dslAreas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 330cbffd..a7446066 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-decompile v1.4 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.5 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -296,14 +296,11 @@ def main(): # --- 5. Extract columns and default width --- - col_node = find(root, "d:columns") - total_columns = int_of(find(col_node, "d:size")) - - col_format_indices = {} - for ci in findall(col_node, "d:columnsItem"): - col_idx = int_of(find(ci, "d:index")) - fmt_idx = int_of(find(ci, "d:column/d:formatIndex")) - col_format_indices[col_idx] = fmt_idx + # Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент . + # Их бывает несколько: раскладка БЕЗ — умолчание (ровно одна в каждом макете корпуса), + # остальные адресуются GUID из , на который ссылаются строки () и + # области (). Раньше читался только первый — отсюда терялись + # ширины и вылезал columns: 0. default_fmt_idx = 0 n = find(root, "d:defaultFormatIndex") @@ -316,13 +313,31 @@ def main(): if def_fmt and def_fmt["Width"] > 0: default_width = def_fmt["Width"] - # Build column width map (1-based col -> width), only non-default - col_width_map = OrderedDict() - for col0 in sorted(col_format_indices.keys()): - fmt = get_format(col_format_indices[col0]) - if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width: - col1 = str(col0 + 1) - col_width_map[col1] = fmt["Width"] + def read_column_set(node): + by_idx = {} + for ci in findall(node, "d:columnsItem"): + by_idx[int_of(find(ci, "d:index"))] = int_of(find(ci, "d:column/d:formatIndex")) + # Карта ширин (1-based колонка → ширина), только отличные от умолчания. + widths = OrderedDict() + for col0 in sorted(by_idx.keys()): + fmt = get_format(by_idx[col0]) + if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width: + widths[str(col0 + 1)] = fmt["Width"] + id_node = find(node, "d:id") + size_node = find(node, "d:size") + return { + "Id": (text_of(id_node) or None) if id_node is not None else None, + "Size": int_of(size_node) if size_node is not None else 0, + "Widths": widths, + } + + column_sets = [read_column_set(cn) for cn in findall(root, "d:columns")] + + default_set = next((c for c in column_sets if not c["Id"]), None) + if default_set is None and column_sets: + default_set = column_sets[0] + total_columns = default_set["Size"] if default_set else 0 + col_width_map = default_set["Widths"] if default_set else OrderedDict() # --- 6. Extract merges --- @@ -426,11 +441,16 @@ def main(): "Text": text, }) + # Ссылка строки на колоночную раскладку; пусто = раскладка по умолчанию. + cid_node = find(row_node, "d:columnsID") + row_columns_id = text_of(cid_node) if cid_node is not None else None + for r in range(row_idx, index_to + 1): row_data[r] = { "FormatIdx": row_fmt_idx, "Cells": cells, "Empty": is_empty, + "ColumnsId": row_columns_id, } # --- 9. Build style key (ignoring fillType) --- @@ -645,6 +665,16 @@ def main(): max_row_idx = max(row_data.keys()) if row_data else -1 + def row_columns_id(r): + rd = row_data.get(r) + return rd["ColumnsId"] if rd else None + + def uniform_column_set(frm, to): + """Область годится в качестве области-диапазона, только если у всех её строк ОДНА + раскладка: иначе её пришлось бы резать, а имя резать нельзя.""" + first = row_columns_id(frm) + return all(row_columns_id(r) == first for r in range(frm + 1, to + 1)) + block_areas = [] overlay_areas = [] claimed = set() @@ -655,22 +685,41 @@ def main(): if r in claimed: fits = False break + if fits: + fits = uniform_column_set(a["BeginRow"], a["EndRow"]) if fits: claimed.update(range(a["BeginRow"], a["EndRow"] + 1)) block_areas.append(a) else: overlay_areas.append(a) - # Блоки в порядке строк + безымянные заполнители дыр. + def split_gap_by_column_set(frm, to): + """Безымянный промежуток режем на куски с одной раскладкой: границы наборов не + совпадают с границами именованных областей.""" + out = [] + if to < frm: + return out + run_start = frm + run_id = row_columns_id(frm) + for r in range(frm + 1, to + 1): + cid = row_columns_id(r) + if cid != run_id: + out.append({"Name": None, "BeginRow": run_start, "EndRow": r - 1, "ColumnsId": run_id}) + run_start, run_id = r, cid + out.append({"Name": None, "BeginRow": run_start, "EndRow": to, "ColumnsId": run_id}) + return out + + # Области в порядке строк + безымянные заполнители дыр. blocks = [] cursor = 0 for a in sorted(block_areas, key=lambda x: x["BeginRow"]): if a["BeginRow"] > cursor: - blocks.append({"Name": None, "BeginRow": cursor, "EndRow": a["BeginRow"] - 1}) - blocks.append({"Name": a["Name"], "BeginRow": a["BeginRow"], "EndRow": a["EndRow"]}) + blocks.extend(split_gap_by_column_set(cursor, a["BeginRow"] - 1)) + blocks.append({"Name": a["Name"], "BeginRow": a["BeginRow"], "EndRow": a["EndRow"], + "ColumnsId": row_columns_id(a["BeginRow"])}) cursor = a["EndRow"] + 1 if cursor <= max_row_idx: - blocks.append({"Name": None, "BeginRow": cursor, "EndRow": max_row_idx}) + blocks.extend(split_gap_by_column_set(cursor, max_row_idx)) dsl_areas = [] @@ -789,9 +838,13 @@ def main(): compressed_rows.append(OrderedDict([("empty", empty_run)])) dsl_block = OrderedDict() - # Безымянный блок — просто кусок сетки, ключ name у него не пишем. + # Область без имени — просто кусок сетки, ключ name у неё не пишем. if area["Name"]: dsl_block["name"] = area["Name"] + # Ссылка на колоночную раскладку по имени из columnSets — как style у ячейки на styles. + # Умолчание (раскладка без id) не пишем. + if area.get("ColumnsId"): + dsl_block["columnSet"] = area["ColumnsId"] dsl_block["rows"] = compressed_rows dsl_areas.append(dsl_block) @@ -875,6 +928,20 @@ def main(): result["fonts"] = fonts_out result["styles"] = style_defs + + # Колоночные раскладки помимо умолчания: ключ — идентификатор из макета, на него ссылаются + # области. Содержимое раскладку не опознаёт (в корпусе полно наборов с одинаковым + # содержимым и разными id), поэтому склейки по содержимому нет. + extra_sets = [c for c in column_sets if c["Id"]] + if extra_sets: + sets_out = OrderedDict() + for cs in extra_sets: + entry = OrderedDict([("columns", cs["Size"])]) + if cs["Widths"]: + entry["columnWidths"] = cs["Widths"] + sets_out[cs["Id"]] = entry + result["columnSets"] = sets_out + result["areas"] = dsl_areas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится