diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 1eff2ed1..bac1708d 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.24 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# mxl-compile v1.25 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -700,6 +700,13 @@ function Expand-ShorthandRow { $cells += $cell $placed[$idx] = $cell $last = $cell + # Ячейка занимает СТОЛЬКО позиций, каков её span. У строки со следующими ">" это + # получается само (каждый маркер съедает позицию), а объектный элемент несёт span + # внутри — без этого сдвига всё правее него уезжало влево. + $elSpan = if ($cell.span) { [int]$cell.span } else { 1 } + if ($elSpan -gt 1) { + for ($s = 1; $s -lt $elSpan; $s++) { $idx++; $placed[$idx] = $cell } + } } # Колонки, не занятые в этой строке, теряют «ячейку сверху». @@ -709,6 +716,18 @@ function Expand-ShorthandRow { return [PSCustomObject]@{ cells = $cells } } +# Позиционный список ячеек опознаём по наличию хотя бы одного элемента-строки или null: +# маркеры, текст и пропуски бывают только в нём. Список из одних объектов разбирается +# как обычный — для простой строки обе трактовки дают один результат, неоднозначности нет. +function Test-PositionalCells { + param($cells) + if ($null -eq $cells -or -not ($cells -is [array])) { return $false } + foreach ($el in $cells) { + if ($null -eq $el -or $el -is [string]) { return $true } + } + return $false +} + # Карту занятых колонок ведём и по объектным строкам: "|" продолжает ту ячейку, # которая реально стоит выше, независимо от того, какой формой её записали. function Update-OpenByCol { @@ -743,7 +762,14 @@ foreach ($area in $def.areas) { foreach ($row in $area.rows) { $rowIdx++ if ($row -is [array]) { + # Строка целиком массивом — сахар для { cells: [...] }. $expandedRows += Expand-ShorthandRow -row $row -areaName $areaName -rowIdx $rowIdx -openByCol $openByCol -maxCols $areaMaxCols + } elseif (Test-PositionalCells $row.cells) { + # Короткая запись — свойство СПИСКА ЯЧЕЕК, а не строки: свои height и rowStyle + # строка при этом сохраняет. + $expanded = Expand-ShorthandRow -row $row.cells -areaName $areaName -rowIdx $rowIdx -openByCol $openByCol -maxCols $areaMaxCols + $row.cells = $expanded.cells + $expandedRows += $row } else { $expandedRows += $row if ($row.empty) { $openByCol.Clear() } else { Update-OpenByCol -row $row -openByCol $openByCol } diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 8880f1f8..42e85e4f 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.24 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# mxl-compile v1.25 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -674,7 +674,9 @@ def main(): extended = [] # ячейки, чей rowspan уже нарастили в этой строке (span>1 даёт несколько "|") last = None # последняя реальная ячейка слева — цель для ">" - for idx, el in enumerate(row, start=1): + idx = 0 + for el in row: + idx += 1 if idx > max_cols: print(f'Row exceeds \'columns\' ({max_cols}): area "{area_name}",' f' row {row_idx}', file=sys.stderr) @@ -729,6 +731,13 @@ def main(): cells.append(cell) placed[idx] = cell last = cell + # Ячейка занимает СТОЛЬКО позиций, каков её span. У строки со следующими ">" это + # получается само (каждый маркер съедает позицию), а объектный элемент несёт span + # внутри — без этого сдвига всё правее него уезжало влево. + el_span = int(cell.get('span') or 1) + for _ in range(el_span - 1): + idx += 1 + placed[idx] = cell # Колонки, не занятые в этой строке, теряют «ячейку сверху». open_by_col.clear() @@ -738,6 +747,14 @@ def main(): out['cells'] = cells return out + # Позиционный список ячеек опознаём по наличию хотя бы одного элемента-строки или None: + # маркеры, текст и пропуски бывают только в нём. Список из одних объектов разбирается + # как обычный — для простой строки обе трактовки дают один результат, неоднозначности нет. + def is_positional_cells(cells): + if not isinstance(cells, list): + return False + return any(el is None or isinstance(el, str) for el in cells) + # Карту занятых колонок ведём и по объектным строкам: "|" продолжает ту ячейку, # которая реально стоит выше, независимо от того, какой формой её записали. def update_open_by_col(row, open_by_col): @@ -767,7 +784,14 @@ def main(): expanded_rows = [] for row_idx, row in enumerate(area.get('rows', []), start=1): if isinstance(row, list): + # Строка целиком массивом — сахар для { cells: [...] }. expanded_rows.append(expand_shorthand_row(row, area_name, row_idx, open_by_col, area_max_cols)) + elif is_positional_cells(row.get('cells')): + # Короткая запись — свойство СПИСКА ЯЧЕЕК, а не строки: свои height и rowStyle + # строка при этом сохраняет. + expanded = expand_shorthand_row(row['cells'], area_name, row_idx, open_by_col, area_max_cols) + row['cells'] = expanded['cells'] + expanded_rows.append(row) else: expanded_rows.append(row) if row.get('empty'): diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index f2425b6a..dd38e0fb 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.7 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.8 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -480,6 +480,54 @@ function Get-StyleName { return "default" } +# Список признаётся позиционным по тому же правилу, что и в компиляторе: есть элемент-строка +# или пропуск. Список из одних объектов таковым не считаем — он читается как обычный. +function Test-PositionalList { + param($cells) + foreach ($el in $cells) { + if ($null -eq $el -or $el -is [string]) { return $true } + } + return $false +} + +# Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется. +# Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды null +# и станет длиннее объектного. Ячейка, у которой кроме текста или параметра ничего нет, +# пишется строкой; span раскрывается маркерами ">"; всё прочее — объектным элементом без col. +function ConvertTo-PositionalCells { + param($cells) + if ($cells.Count -eq 0) { return @() } + $first = $cells[0] + if ([int]$first["col"] -ne 1) { return $cells } + + $out = @() + $expected = 1 + foreach ($c in $cells) { + $col = [int]$c["col"] + if ($col -lt $expected) { return $cells } # перекрытие — позиционно не выразить + while ($expected -lt $col) { $out += $null; $expected++ } + + $span = if ($c.Contains("span")) { [int]$c["span"] } else { 1 } + $keys = @($c.Keys | Where-Object { $_ -notin @("col", "span") }) + $plainText = ($keys.Count -eq 1 -and $keys[0] -eq "text" -and $c["text"] -is [string]) + $plainParam = ($keys.Count -eq 1 -and $keys[0] -eq "param") + + if ($plainText) { $out += $c["text"] } + elseif ($plainParam) { $out += "{$($c["param"])}" } + else { + $obj = [ordered]@{} + foreach ($k in $c.Keys) { if ($k -ne "col") { $obj[$k] = $c[$k] } } + $out += $obj + } + # span раскрываем маркерами — кроме объектного элемента, он несёт span сам. + if (($plainText -or $plainParam) -and $span -gt 1) { + for ($i = 1; $i -lt $span; $i++) { $out += ">" } + } + $expected = $col + $span + } + return $out +} + # --- 12. Build areas --- # Сетка нарезается на блоки: непересекающиеся области типа Rows задают границы, строки вне @@ -664,8 +712,14 @@ foreach ($area in $blocks) { $dslCells += $dslCell } - if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells } - $areaRows += $dslRow + if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array](ConvertTo-PositionalCells $dslCells) } + # Самая короткая из применимых форм: если у строки нет своих свойств, а список ячеек + # позиционный — строка пишется просто массивом, без ключа cells. + if ($dslRow.Count -eq 1 -and $dslRow.Contains("cells") -and (Test-PositionalList $dslRow["cells"])) { + $areaRows += ,([array]$dslRow["cells"]) + } else { + $areaRows += $dslRow + } } # Compress consecutive empty rows ({}) into { empty = N } @@ -680,7 +734,8 @@ foreach ($area in $blocks) { else { $compressedRows += [ordered]@{ empty = $emptyRun } } $emptyRun = 0 } - $compressedRows += $r + # Запятая обязательна: строка-массив иначе развернётся в список строк. + $compressedRows += ,$r } } if ($emptyRun -gt 0) { @@ -755,8 +810,11 @@ if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) { $usedStyles = @{} foreach ($a in $dslAreas) { foreach ($r in $a.rows) { - if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true } - if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } } + # Строка может быть массивом (короткая форма) — у неё нет свойства cells, и без этой + # ветки стиль, использованный только внутри такой строки, вырезался как «неиспользуемый». + $cellList = if ($r -is [array]) { $r } else { $r.cells } + if ($r -isnot [array] -and $r.rowStyle) { $usedStyles[$r.rowStyle] = $true } + if ($cellList) { foreach ($c in $cellList) { if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } } } } } $toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 0ef1792f..1b7c536c 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.7 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.8 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -667,6 +667,39 @@ def main(): return style_names[key] return "default" + def to_positional_cells(cells): + """Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется. + Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды None + и станет длиннее объектного. Ячейка, у которой кроме текста или параметра ничего нет, + пишется строкой; span раскрывается маркерами ">"; прочее — объектным элементом без col.""" + if not cells or int(cells[0].get("col", 0)) != 1: + return cells + out = [] + expected = 1 + for c in cells: + col = int(c.get("col", 0)) + if col < expected: + return cells # перекрытие — позиционно не выразить + while expected < col: + out.append(None) + expected += 1 + span = int(c.get("span", 1) or 1) + keys = [k for k in c if k not in ("col", "span")] + plain_text = len(keys) == 1 and keys[0] == "text" and isinstance(c["text"], str) + plain_param = len(keys) == 1 and keys[0] == "param" + if plain_text: + out.append(c["text"]) + elif plain_param: + out.append("{%s}" % c["param"]) + else: + obj = OrderedDict((k, v) for k, v in c.items() if k != "col") + out.append(obj) + # span раскрываем маркерами — кроме объектного элемента, он несёт span сам. + if (plain_text or plain_param) and span > 1: + out.extend([">"] * (span - 1)) + expected = col + span + return out + # --- 12. Build areas --- # Сетка нарезается на блоки: непересекающиеся области типа Rows задают границы, строки вне @@ -831,8 +864,14 @@ def main(): dsl_cells.append(dsl_cell) if len(dsl_cells) > 0: - dsl_row["cells"] = dsl_cells - area_rows.append(dsl_row) + dsl_row["cells"] = to_positional_cells(dsl_cells) + # Самая короткая из применимых форм: если у строки нет своих свойств, а список ячеек + # позиционный — строка пишется просто массивом, без ключа cells. + if (len(dsl_row) == 1 and "cells" in dsl_row + and any(el is None or isinstance(el, str) for el in dsl_row["cells"])): + area_rows.append(dsl_row["cells"]) + else: + area_rows.append(dsl_row) # Compress consecutive empty rows ({}) into { empty = N } compressed_rows = [] @@ -933,12 +972,19 @@ def main(): used_styles = set() for a in dsl_areas: for r in a["rows"]: - if "rowStyle" in r: - used_styles.add(r["rowStyle"]) - if "cells" in r: - for c in r["cells"]: - if "style" in c: - used_styles.add(c["style"]) + # Строка может быть массивом (короткая форма) — у неё нет ключа cells, и без этой + # ветки стиль, использованный только внутри такой строки, вырезался как «неиспользуемый». + if isinstance(r, list): + cell_list = r + else: + if "rowStyle" in r: + used_styles.add(r["rowStyle"]) + cell_list = r.get("cells") or [] + # Список ячеек может быть позиционным: строки, None и маркеры стиля не несут, + # стиль бывает только у объектного элемента. + for c in cell_list: + if isinstance(c, dict) and "style" in c: + used_styles.add(c["style"]) to_remove = [s for s in style_defs if s not in used_styles] for s in to_remove: del style_defs[s] diff --git a/tests/skills/cases/mxl-decompile/snapshots/output-absolute-path/back.json b/tests/skills/cases/mxl-decompile/snapshots/output-absolute-path/back.json index 7d36d508..e10c9948 100644 --- a/tests/skills/cases/mxl-decompile/snapshots/output-absolute-path/back.json +++ b/tests/skills/cases/mxl-decompile/snapshots/output-absolute-path/back.json @@ -1 +1 @@ -{ "columns": 2, "defaultWidth": 10, "fonts": { "default": { "face": "Arial", "size": 10 } }, "styles": {}, "areas": [{ "name": "Test", "rows": [{ "cells": [{ "col": 1, "text": "A" }, { "col": 2, "text": "B" }] }] }] } \ No newline at end of file +{ "columns": 2, "defaultWidth": 10, "fonts": { "default": { "face": "Arial", "size": 10 } }, "styles": {}, "areas": [{ "name": "Test", "rows": [["A", "B"]] }] } \ No newline at end of file