From 4e389d0ef122c1f492da5091be382e0c39526d65 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 11 Aug 2026 16:57:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(mxl-compile,mxl-decompile):=20columnStyles?= =?UTF-8?q?=20=E2=80=94=20=D0=BE=D1=84=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit У ячейки есть style, у строки rowStyle, у колонки не было ничего — при том что колонка ссылается в ту же палитру и несёт те же свойства. В корпусе ERP колонки используют шрифт (3006 форматов), выравнивание, рамки, скрытие и прочее; всё это терялось при раундтрипе. Колонка получает такой же именованный стиль: columnStyles рядом с columnWidths, ключи той же грамматики диапазонов ("1", "2-8", "5,7,9"), значение — имя из styles. Внутри columnSets тот же ключ. Формат колонки собирается из ширины и свойств стиля: запись в палитре одна. Шрифт по умолчанию колонке не навязываем — формат колонки без оформления это ровно , как пишет платформа. Попутно два дефекта декомпилятора: стиль колонки не учитывался при отсечении неиспользуемых стилей и пропадал целиком, а набор из одних неприметных свойств (отступ, защита) получал зарезервированное имя default и тоже терялся. На пилоте потери в категории colset[].col[].formatIndex упали с 86 до 39. Co-Authored-By: Claude Opus 5 (1M context) --- .../mxl-compile/scripts/mxl-compile.ps1 | 42 ++++- .../skills/mxl-compile/scripts/mxl-compile.py | 37 ++++- .../mxl-decompile/scripts/mxl-decompile.ps1 | 46 +++++- .../mxl-decompile/scripts/mxl-decompile.py | 42 ++++- .../cases/mxl-compile/column-styles.json | 47 ++++++ .../snapshots/column-styles/Template.xml | 156 ++++++++++++++++++ 6 files changed, 350 insertions(+), 20 deletions(-) create mode 100644 tests/skills/cases/mxl-compile/column-styles.json create mode 100644 tests/skills/cases/mxl-compile/snapshots/column-styles/Template.xml diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index c82eed49..a917a5ed 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.29 — Compile 1C spreadsheet from JSON +# mxl-compile v1.30 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -382,6 +382,21 @@ function Build-ColWidthMap { $colWidthMap = Build-ColWidthMap $def.columnWidths +# Стиль колонки — тот же именованный стиль, что у ячейки и строки: колонка третий владелец +# формата, и своих свойств у неё нет. Ключи те же, что у columnWidths. +function Build-ColStyleMap { + param($styles) + $map = @{} + if ($styles) { + foreach ($prop in $styles.PSObject.Properties) { + foreach ($c in (Parse-ColumnSpec $prop.Name)) { $map[$c] = "$($prop.Value)" } + } + } + return $map +} + +$colStyleMap = Build-ColStyleMap $def.columnStyles + # Колоночные раскладки: документные columns/columnWidths — раскладка по умолчанию (в XML # элемент БЕЗ , он всегда идёт первым). Дополнительные объявляются в # columnSets, ключ — идентификатор, на него ссылается область ключом columnSet. @@ -407,7 +422,7 @@ function ConvertTo-LayoutId { } $columnLayouts = @() -$columnLayouts += @{ Id = $null; Name = $null; Size = $totalColumns; Widths = $colWidthMap } +$columnLayouts += @{ Id = $null; Name = $null; Size = $totalColumns; Widths = $colWidthMap; Styles = $colStyleMap } if ($def.columnSets) { foreach ($prop in $def.columnSets.PSObject.Properties) { $cs = $prop.Value @@ -417,6 +432,7 @@ if ($def.columnSets) { Name = $prop.Name Size = $size Widths = Build-ColWidthMap $cs.columnWidths + Styles = Build-ColStyleMap $cs.columnStyles } } } @@ -455,11 +471,12 @@ function Get-ColorNamespace { } function Resolve-Style { - param([string]$styleName, [string]$fillType) + param([string]$styleName, [string]$fillType, [switch]$noDefaultFont) # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. - $props = @{ font = $fontMap["default"] } + $props = @{} + if (-not $noDefaultFont) { $props['font'] = $fontMap["default"] } if ($styleName -and $def.styles) { $style = $def.styles.$styleName @@ -677,12 +694,21 @@ function Register-Format { # 6a. Default width format $defaultFormatIndex = Register-Format @{ width = $defaultWidth } -# 6b. Column width formats — по одной карте на каждую колоночную раскладку +# 6b. Column formats — по одной карте на каждую колоночную раскладку. +# У колонки бывает и ширина, и оформление: формат один, свойства складываются. foreach ($layout in $columnLayouts) { $map = @{} # 1-based col -> format index - foreach ($col in ($layout.Widths.Keys | Sort-Object)) { - $w = $layout.Widths[$col] - $map[[int]$col] = Register-Format @{ width = $w } + $cols = @($layout.Widths.Keys) + @($layout.Styles.Keys) | ForEach-Object { [int]$_ } | + Select-Object -Unique | Sort-Object + foreach ($col in $cols) { + $props = @{} + if ($layout.Styles.ContainsKey($col)) { + # Шрифт по умолчанию колонке не навязываем: формат колонки без оформления — + # это ровно , как пишет платформа. + $props = Resolve-Style -styleName $layout.Styles[$col] -fillType "" -noDefaultFont + } + if ($layout.Widths.ContainsKey($col)) { $props['width'] = $layout.Widths[$col] } + $map[$col] = Register-Format $props } $layout.FormatMap = $map } diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index eaf6959f..3bb31b7f 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.29 — Compile 1C spreadsheet from JSON +# mxl-compile v1.30 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -595,6 +595,18 @@ def main(): col_width_map = build_col_width_map(defn.get('columnWidths')) + # Стиль колонки — тот же именованный стиль, что у ячейки и строки: колонка третий + # владелец формата, и своих свойств у неё нет. Ключи те же, что у columnWidths. + def build_col_style_map(styles): + out = {} + if styles: + for prop_name, prop_value in styles.items(): + for c in parse_column_spec(prop_name): + out[c] = str(prop_value) + return out + + col_style_map = build_col_style_map(defn.get('columnStyles')) + # Колоночные раскладки: документные columns/columnWidths — раскладка по умолчанию (в XML # элемент БЕЗ , он всегда идёт первым). Дополнительные объявляются в # columnSets, ключ — идентификатор, на него ссылается область ключом columnSet. @@ -616,7 +628,8 @@ def main(): h = b.hex() return f'{h[0:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}' - column_layouts = [{'Id': None, 'Name': None, 'Size': total_columns, 'Widths': col_width_map}] + column_layouts = [{'Id': None, 'Name': None, 'Size': total_columns, + 'Widths': col_width_map, 'Styles': col_style_map}] for set_name, cs in (defn.get('columnSets') or {}).items(): size = int(cs['columns']) if cs.get('columns') is not None else total_columns column_layouts.append({ @@ -624,6 +637,7 @@ def main(): 'Name': set_name, 'Size': size, 'Widths': build_col_width_map(cs.get('columnWidths')), + 'Styles': build_col_style_map(cs.get('columnStyles')), }) # --- 5. Style resolver --- @@ -651,10 +665,10 @@ def main(): sys.exit(1) return {'Style': canon, 'Width': width, 'Gap': gap} - def resolve_style(style_name, fill_type): + def resolve_style(style_name, fill_type, no_default_font=False): # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. - props = {'font': font_map.get('default', 0)} + props = {} if no_default_font else {'font': font_map.get('default', 0)} if style_name and defn.get('styles'): style = defn['styles'].get(style_name) @@ -759,12 +773,19 @@ def main(): # 6a. Default width format default_format_index = register_format({'width': default_width}) - # 6b. Column width formats — по одной карте на каждую колоночную раскладку + # 6b. Column 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({'width': w}) + for col in sorted({int(c) for c in list(layout['Widths']) + list(layout['Styles'])}): + props = {} + if col in layout['Styles']: + # Шрифт по умолчанию колонке не навязываем: формат колонки без оформления — + # это ровно , как пишет платформа. + props = resolve_style(layout['Styles'][col], '', no_default_font=True) + if col in layout['Widths']: + props['width'] = layout['Widths'][col] + fmap[col] = register_format(props) layout['FormatMap'] = fmap col_format_map = column_layouts[0]['FormatMap'] diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 9cd971ca..a1df39bb 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.10 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.11 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -191,12 +191,17 @@ function Read-ColumnSet { $widths.Add([string]($col0 + 1), $fmt.Width) } } + # Формат колонки несёт не только ширину — имя стиля подставим ниже, когда стили + # будут поименованы. + $fmtIdx = [ordered]@{} + foreach ($col0 in ($byIdx.Keys | Sort-Object)) { $fmtIdx.Add([string]($col0 + 1), $byIdx[$col0]) } $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 + FmtIdx = $fmtIdx } } @@ -494,6 +499,18 @@ foreach ($r in $rowData.Values) { } } +# Колонка — третий владелец формата, её оформление тоже становится именованным стилем. +foreach ($cs in $columnSets) { + foreach ($fi in $cs.FmtIdx.Values) { + $fmt = Get-Format $fi + if (-not $fmt) { continue } + if ((Get-StyleProps $fmt).Count -eq 0) { continue } + $key = Get-StyleKey $fmt + if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } + $formatToStyleKey[$fi] = $key + } +} + # Имя стиля — читаемая метка по самым заметным свойствам. Полнота тут не нужна: # различает стили ключ, имя лишь помогает автору ориентироваться. function Name-Style { @@ -525,7 +542,10 @@ function Name-Style { if ($props.Contains('backColor')) { $parts += "bg" } if ($props.Contains('format')) { $parts += "fmt" } - if ($parts.Count -eq 0) { return "default" } + # Имя "default" зарезервировано за стилем БЕЗ свойств: под ним компилятор понимает + # отсутствие оформления. Набор из одних неприметных свойств (отступ, защита) обязан + # получить своё имя, иначе он потеряется. + if ($parts.Count -eq 0) { return $(if ($props.Count -eq 0) { "default" } else { "style" }) } return ($parts -join "-") } @@ -559,6 +579,18 @@ function Get-StyleName { return "default" } +# Колонки раскладки, у которых формат несёт не только ширину, — «колонка → стиль». +function Get-ColumnStyles { + param($cs) + $out = [ordered]@{} + foreach ($col in $cs.FmtIdx.Keys) { + $fi = $cs.FmtIdx[$col] + $fmt = Get-Format $fi + if ($fmt -and (Get-StyleProps $fmt).Count -gt 0) { $out[$col] = Get-StyleName $fi } + } + return $out +} + # Список признаётся позиционным по тому же правилу, что и в компиляторе: есть элемент-строка # или пропуск. Список из одних объектов таковым не считаем — он читается как обычный. function Test-PositionalList { @@ -880,6 +912,10 @@ $result = [ordered]@{ defaultWidth = $defaultWidth } if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths } +if ($defaultSet) { + $defaultColStyles = Get-ColumnStyles $defaultSet + if ($defaultColStyles.Count -gt 0) { $result["columnStyles"] = $defaultColStyles } +} # Набор языков объявляем, только если он отличается от умолчания компилятора (один ru). if ($textLanguages.Count -gt 0 -and -not ($textLanguages.Count -eq 1 -and $textLanguages[0] -ceq 'ru')) { $result["textLanguages"] = [array]$textLanguages @@ -900,6 +936,10 @@ foreach ($a in $dslAreas) { if ($cellList) { foreach ($c in $cellList) { if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } } } } } +# Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. +foreach ($cs in $columnSets) { + foreach ($name in (Get-ColumnStyles $cs).Values) { $usedStyles[$name] = $true } +} $toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) foreach ($s in $toRemove) { $styleDefs.Remove($s) } @@ -916,6 +956,8 @@ if ($extraSets.Count -gt 0) { foreach ($cs in $extraSets) { $entry = [ordered]@{ columns = $cs.Size } if ($cs.Widths.Count -gt 0) { $entry["columnWidths"] = $cs.Widths } + $csStyles = Get-ColumnStyles $cs + if ($csStyles.Count -gt 0) { $entry["columnStyles"] = $csStyles } $setsOut[$cs.Id] = $entry } $result["columnSets"] = $setsOut diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 8998cea2..39733cd0 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.10 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.11 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -363,6 +363,9 @@ def main(): "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, + # Формат колонки несёт не только ширину — имя стиля подставим ниже, когда стили + # будут поименованы. + "FmtIdx": {str(c0 + 1): by_idx[c0] for c0 in sorted(by_idx.keys())}, } column_sets = [read_column_set(cn) for cn in findall(root, "d:columns")] @@ -656,6 +659,17 @@ def main(): style_keys[key] = fmt format_to_style_key[cell["FormatIdx"]] = key + # Колонка — третий владелец формата, её оформление тоже становится именованным стилем. + for cs in column_sets: + for fi in cs["FmtIdx"].values(): + fmt = get_format(fi) + if not fmt or not style_props(fmt): + continue + key = get_style_key(fmt) + if key not in style_keys: + style_keys[key] = fmt + format_to_style_key[fi] = key + def name_style(fmt): """Имя стиля — читаемая метка по самым заметным свойствам. Полнота тут не нужна: различает стили ключ, имя лишь помогает автору ориентироваться.""" @@ -694,7 +708,10 @@ def main(): parts.append("fmt") if len(parts) == 0: - return "default" + # Имя "default" зарезервировано за стилем БЕЗ свойств: под ним компилятор + # понимает отсутствие оформления. Набор из одних неприметных свойств + # (отступ, защита) обязан получить своё имя, иначе он потеряется. + return "default" if not props else "style" return "-".join(parts) style_names = OrderedDict() @@ -726,6 +743,15 @@ def main(): return style_names[key] return "default" + def column_styles_of(cs): + """Колонки раскладки, у которых формат несёт не только ширину, — «колонка → стиль».""" + out = OrderedDict() + for col, fi in cs["FmtIdx"].items(): + fmt = get_format(fi) + if fmt and style_props(fmt): + out[col] = get_style_name(fi) + return out + def to_positional_cells(cells): """Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется. Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды None @@ -1022,6 +1048,10 @@ def main(): result["defaultWidth"] = default_width if len(compressed_widths) > 0: result["columnWidths"] = compressed_widths + if default_set: + default_col_styles = column_styles_of(default_set) + if default_col_styles: + result["columnStyles"] = default_col_styles # Набор языков объявляем, только если он отличается от умолчания компилятора (один ru). if text_languages and text_languages != ['ru']: result["textLanguages"] = text_languages @@ -1047,6 +1077,11 @@ def main(): for c in cell_list: if isinstance(c, dict) and "style" in c: used_styles.add(c["style"]) + # Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. + for src in [result.get("columnStyles")] + [ + (cs or {}).get("columnStyles") for cs in (result.get("columnSets") or {}).values()]: + for name in (src or {}).values(): + used_styles.add(name) to_remove = [s for s in style_defs if s not in used_styles] for s in to_remove: del style_defs[s] @@ -1064,6 +1099,9 @@ def main(): entry = OrderedDict([("columns", cs["Size"])]) if cs["Widths"]: entry["columnWidths"] = cs["Widths"] + styles_out = column_styles_of(cs) + if styles_out: + entry["columnStyles"] = styles_out sets_out[cs["Id"]] = entry result["columnSets"] = sets_out diff --git a/tests/skills/cases/mxl-compile/column-styles.json b/tests/skills/cases/mxl-compile/column-styles.json new file mode 100644 index 00000000..48f42e0d --- /dev/null +++ b/tests/skills/cases/mxl-compile/column-styles.json @@ -0,0 +1,47 @@ +{ + "name": "Стиль колонки — в документной раскладке и в columnSets", + "input": { + "columns": 5, + "defaultWidth": 20, + "fonts": { + "default": { "face": "Arial", "size": 10 }, + "bold": { "face": "Arial", "size": 10, "bold": true } + }, + "styles": { + "по-центру": { "horizontalAlignment": "Center" }, + "скрытая": { "hidden": true }, + "жирная-с-фоном": { "font": "bold", "backColor": "#EBEBEB" } + }, + "columnWidths": { "1": 30, "2-3": 15 }, + "columnStyles": { "1": "по-центру", "4": "скрытая" }, + "columnSets": { + "таблица": { + "columns": 3, + "columnWidths": { "1": 12 }, + "columnStyles": { "2-3": "жирная-с-фоном" } + } + }, + "areas": [ + { + "name": "Шапка", + "rows": [ + { "cells": [ { "col": 1, "span": 5, "text": "Заголовок" } ] } + ] + }, + { + "name": "Таблица", + "columnSet": "таблица", + "rows": [ + { "cells": [ + { "col": 1, "param": "Код" }, + { "col": 2, "param": "Имя" }, + { "col": 3, "param": "Сумма" } + ]} + ] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "validatePath": "Template.xml", + "expect": { "files": ["Template.xml"] } +} diff --git a/tests/skills/cases/mxl-compile/snapshots/column-styles/Template.xml b/tests/skills/cases/mxl-compile/snapshots/column-styles/Template.xml new file mode 100644 index 00000000..00bea868 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/column-styles/Template.xml @@ -0,0 +1,156 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 5 + + 0 + + 2 + + + + 1 + + 3 + + + + 2 + + 3 + + + + 3 + + 4 + + + + + 01f06d2d-2103-3169-8cc8-32872f100129 + 3 + + 0 + + 5 + + + + 1 + + 6 + + + + 2 + + 6 + + + + + 0 + + + + 1 + + + ru + Заголовок + + + + + + + + 1 + + 01f06d2d-2103-3169-8cc8-32872f100129 + + + 7 + Код + + + + + 7 + Имя + + + + + 7 + Сумма + + + + + true + 1 + 2 + 2 + + 0 + 0 + 4 + + + Таблица + + Rows + 1 + 1 + -1 + -1 + + + + Шапка + + Rows + 0 + 0 + -1 + -1 + + + + + + 20 + + + 30 + Center + + + 15 + + + true + + + 12 + + + 1 + #EBEBEB + + + 0 + Parameter + + \ No newline at end of file