From ba25aa4a003174ea29cde18e368b1660f0957ee5 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 11 Aug 2026 18:36:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(mxl-compile,mxl-decompile):=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=82=20=D1=81=D0=B0=D0=BC=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit У строки есть собственный формат: платформа хранит в нём скрытие (17 423 вхождения в корпусе), шрифт (9 216), фон, выравнивания, защиту. Мы писали туда только высоту, всё остальное теряли. Разбор на контролируемом стенде показал, что это два разных случая, а не один. Оформление, применённое к строке целиком, платформа пишет И строке, И каждой ячейке (backColor: 13 899 ячеек повторяют против 96). Скрытие — только строке (24 026 против 62 856). Поэтому одного правила «rowStyle красит ячейки» мало. Теперь rowStyle — стиль строки: по умолчанию ложится и на строку, и на ячейки, как это делает платформа. Объектная форма { style, apply } задаёт исключения: "row" — только строке, "cells" — только ячейкам. Модификатор нужен раундтрипу, в описании DSL его нет. Скрытие и высота — собственные свойства строки, ключами рядом: у ячейки таких свойств не бывает, и в её оформление они не попадают. На пилоте категория row[].formatIndex упала с 1964 до 201 потерянного факта. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/mxl-compile/SKILL.md | 5 +- .../skills/mxl-compile/reference/dsl-spec.md | 14 +- .../mxl-compile/scripts/mxl-compile.ps1 | 74 ++++++--- .../skills/mxl-compile/scripts/mxl-compile.py | 71 +++++++-- .../mxl-decompile/scripts/mxl-decompile.ps1 | 63 +++++++- .../mxl-decompile/scripts/mxl-decompile.py | 65 ++++++-- docs/mxl-dsl-spec.md | 14 +- .../cases/mxl-compile/row-own-format.json | 38 +++++ .../snapshots/column-widths/Template.xml | 16 +- .../snapshots/empty-rows/Template.xml | 10 +- .../snapshots/format-strings/Template.xml | 25 ++- .../snapshots/merged-cells/Template.xml | 24 +-- .../snapshots/multiple-areas/Template.xml | 25 +-- .../snapshots/page-a4-landscape/Template.xml | 15 +- .../parameters-and-templates/Template.xml | 10 +- .../snapshots/print-form/Template.xml | 45 +++--- .../snapshots/row-own-format/Template.xml | 142 ++++++++++++++++++ .../styles-fonts-borders/Template.xml | 21 ++- .../roundtrip-merged-cells/Template.xml | 13 +- .../roundtrip-multiple-areas/Template.xml | 16 +- .../snapshots/areas-and-params/Template.xml | 18 ++- .../snapshots/detail-params/Template.xml | 10 +- .../snapshots/valid-complex/Template.xml | 18 ++- .../snapshots/valid-print-form/Template.xml | 18 ++- .../valid-with-detailed/Template.xml | 8 +- 25 files changed, 611 insertions(+), 167 deletions(-) create mode 100644 tests/skills/cases/mxl-compile/row-own-format.json create mode 100644 tests/skills/cases/mxl-compile/snapshots/row-own-format/Template.xml diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index b8fd7c37..ce2d473f 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -58,7 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J fonts: { name: { face, size, bold, italic, underline, strikeout } }, styles: { name: { font, horizontalAlignment, verticalAlignment, textPlacement, backColor, textColor, border, borderColor, format, hidden } }, - areas: [{ name, columnSet, rows: [{ height, rowStyle, cells: [ + areas: [{ name, columnSet, rows: [{ height, hidden, rowStyle, cells: [ { col, span, rowspan, style, param, detail, text, template } ]}]}], namedAreas: [{ name, rows, cols }], @@ -73,7 +73,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J - `columnSet` у области — ссылка на раскладку из `columnSets`, когда группе строк нужны свои ширины колонок; без него действует документная раскладка - Ключ стиля — имя свойства как в выгрузке; `columnStyles` вешает стиль на колонку так же, как `style` на ячейку - Рамка — `border` (все стороны) или `leftBorder`/`topBorder`/`rightBorder`/`bottomBorder`; значение `"Solid"` либо `{ style, width }` -- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) +- `rowStyle` — стиль строки: ложится и на строку, и на все её колонки, заполняя пустоты (рамки по всей ширине) +- `height` и `hidden` — собственные свойства строки, у ячейки таких нет - `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`) - Строку можно писать массивом ячеек — позиция из порядка, `col` не нужен: `"текст"`, `"{Имя}"` — параметр, `">"` — продолжить ячейку слева, `"|"` — сверху, `null` — пропуск колонки - `col` — 1-based позиция колонки diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index e4f13e31..20405775 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -127,12 +127,16 @@ | Поле | По умолч. | Описание | |------|-----------|----------| | `height` | — | Высота строки (если не задана, используется авто) | -| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) | +| `hidden` | `false` | Скрыть строку | +| `rowStyle` | — | Стиль строки: ложится и на саму строку, и на ВСЕ её колонки (заполняет пустоты рамками) | | `cells` | `[]` | Массив ячеек | | `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) | Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`. +`height` и `hidden` — собственные свойства строки: у ячейки таких нет, и в её оформление они +не попадают. Всё остальное оформление строки задаётся через `rowStyle`. + ### Короткая форма: строка массивом Вместо объекта строка может быть массивом ячеек — позиция определяется порядком, `col` не указывается. @@ -203,9 +207,11 @@ Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста. -## `rowStyle` — автозаполнение +## `rowStyle` — оформление строки -Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. +Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. + +Стиль конкретной ячейки (`style`) перекрывает `rowStyle` для этой ячейки. Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются. @@ -216,8 +222,6 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - ячейки-поля ввода (`containsValue` / `valueType` / `controlType`); -- оформление СТРОКИ помимо высоты: у строки задаётся только `height`, а скрытие, - шрифт и прочее платформа хранит и у неё тоже; - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 46766fd7..41e39c5a 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.32 — Compile 1C spreadsheet from JSON +# mxl-compile v1.33 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -474,6 +474,41 @@ function Get-ColorNamespace { return '' } +# Стиль строки: имя строкой либо объект { style, apply }. apply — куда лёг стиль: +# "both" (умолчание) — и в формат строки, и в форматы ячеек, как пишет платформа, когда +# автор оформляет строку целиком; "row" — только строке (так хранится, например, скрытие); +# "cells" — только ячейкам. Модификатор нужен раундтрипу, в описании DSL его нет. +function Get-RowStyleSpec { + param($val, [string]$where) + if ($null -eq $val) { return @{ Name = $null; Apply = 'both' } } + if ($val -is [string]) { return @{ Name = $val; Apply = 'both' } } + $name = "$($val.style)" + $apply = if ($val.apply) { "$($val.apply)".ToLower() } else { 'both' } + if ($apply -notin @('both', 'row', 'cells')) { + [Console]::Error.WriteLine("Unknown 'apply' value `"$($val.apply)`" ($where). Allowed: both, row, cells") + exit 1 + } + if (-not $name) { + [Console]::Error.WriteLine("rowStyle object requires 'style' ($where)") + exit 1 + } + return @{ Name = $name; Apply = $apply } +} + +# Формат самой строки: собственные свойства строки (высота, скрытие) плюс стиль строки, +# если он ложится на строку. Пустой набор = у строки формата нет. +function Get-RowFormatProps { + param($row) + $props = @{} + $spec = Get-RowStyleSpec $row.rowStyle "row" + if ($spec.Name -and $spec.Apply -cne 'cells') { + $props = Resolve-Style -styleName $spec.Name -fillType "" -noDefaultFont + } + if ($row.height) { $props['height'] = [int]$row.height } + if ($row.hidden -eq $true) { $props['hidden'] = 'true' } + return $props +} + function Resolve-Style { param([string]$styleName, [string]$fillType, [switch]$noDefaultFont) @@ -957,20 +992,23 @@ foreach ($area in $def.areas) { # Skip empty row placeholder if ($row.empty) { continue } - # Row height format - if ($row.height) { - Register-Format @{ height = [int]$row.height } | Out-Null - } + # Формат САМОЙ строки: высота и скрытие — её собственные свойства (у ячейки таких + # нет), плюс стиль строки, если он ложится на строку. + $rowProps = Get-RowFormatProps $row + if ($rowProps.Count -gt 0) { Register-Format $rowProps | Out-Null } + + $spec = Get-RowStyleSpec $row.rowStyle "row" + $cellsStyle = if ($spec.Apply -ceq 'row') { $null } else { $spec.Name } # rowStyle gap-fill format (no content → no fillType) - if ($row.rowStyle) { - Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null + if ($cellsStyle) { + Register-CellFormat -styleName $cellsStyle -fillType "" | Out-Null } # Explicit cell formats if ($row.cells) { foreach ($cell in $row.cells) { - $cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" } + $cellStyle = if ($cell.style) { $cell.style } elseif ($cellsStyle) { $cellsStyle } else { "default" } $ft = Get-FillType $cell Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null } @@ -1118,11 +1156,13 @@ foreach ($area in $def.areas) { $rowHasContent = $false $rowCells = @() # array of { Col(0-based), FormatIdx, Content } - # Determine row height format + # Формат самой строки — высота, скрытие и стиль строки, если он ложится на строку $rowFormatIdx = 0 - if ($row.height) { - $rowFormatIdx = Register-Format @{ height = [int]$row.height } - } + $rowProps = Get-RowFormatProps $row + if ($rowProps.Count -gt 0) { $rowFormatIdx = Register-Format $rowProps } + + $spec = Get-RowStyleSpec $row.rowStyle "row" + $cellsStyle = if ($spec.Apply -ceq 'row') { $null } else { $spec.Name } if ($row.cells -and $row.cells.Count -gt 0) { $rowHasContent = $true @@ -1186,7 +1226,7 @@ foreach ($area in $def.areas) { $colStart = [int]$cell.col $colSpan = if ($cell.span) { [int]$cell.span } else { 1 } $rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 } - $cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" } + $cellStyle = if ($cell.style) { $cell.style } elseif ($cellsStyle) { $cellsStyle } else { "default" } $ft = Get-FillType $cell $fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft @@ -1219,8 +1259,8 @@ foreach ($area in $def.areas) { } # Generate gap-fill cells for rowStyle - if ($row.rowStyle) { - $gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType "" + if ($cellsStyle) { + $gapFmtIdx = Register-CellFormat -styleName $cellsStyle -fillType "" for ($c = 1; $c -le $totalColumns; $c++) { if (-not $occupiedCols.ContainsKey($c)) { $rowCells += @{ @@ -1238,10 +1278,10 @@ foreach ($area in $def.areas) { # Sort cells by column $rowCells = $rowCells | Sort-Object { $_.Col } - } elseif ($row.rowStyle) { + } elseif ($cellsStyle) { # Row with only rowStyle, no explicit cells — fill non-rowspan columns $rowHasContent = $true - $gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType "" + $gapFmtIdx = Register-CellFormat -styleName $cellsStyle -fillType "" for ($c = 1; $c -le $totalColumns; $c++) { if ($rowspanOccupied.ContainsKey($c)) { continue } $rowCells += @{ diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 34268487..255a4a38 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.32 — Compile 1C spreadsheet from JSON +# mxl-compile v1.33 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -669,6 +669,39 @@ def main(): sys.exit(1) return {'Style': canon, 'Width': width, 'Gap': gap} + def row_style_spec(val, where): + """Стиль строки: имя строкой либо объект { style, apply }. apply — куда лёг стиль: + "both" (умолчание) — и в формат строки, и в форматы ячеек, как пишет платформа, когда + автор оформляет строку целиком; "row" — только строке (так хранится, например, + скрытие); "cells" — только ячейкам. Модификатор нужен раундтрипу, в описании DSL его нет.""" + if val is None: + return None, 'both' + if not isinstance(val, dict): + return str(val), 'both' + name = str(val.get('style') or '') + apply_to = str(val.get('apply') or 'both').lower() + if apply_to not in ('both', 'row', 'cells'): + print(f"Unknown 'apply' value \"{val.get('apply')}\" ({where})." + f" Allowed: both, row, cells", file=sys.stderr) + sys.exit(1) + if not name: + print(f"rowStyle object requires 'style' ({where})", file=sys.stderr) + sys.exit(1) + return name, apply_to + + def row_format_props(row): + """Формат самой строки: собственные свойства строки (высота, скрытие) плюс стиль + строки, если он ложится на строку. Пустой набор = у строки формата нет.""" + props = {} + name, apply_to = row_style_spec(row.get('rowStyle'), 'row') + if name and apply_to != 'cells': + props = resolve_style(name, '', no_default_font=True) + if row.get('height'): + props['height'] = int(row['height']) + if row.get('hidden') is True: + props['hidden'] = 'true' + return props + def resolve_style(style_name, fill_type, no_default_font=False): # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. @@ -986,18 +1019,23 @@ def main(): if row.get('empty'): continue - # Row height format - if row.get('height'): - register_format({'height': int(row['height'])}) + # Формат САМОЙ строки: высота и скрытие — её собственные свойства (у ячейки + # таких нет), плюс стиль строки, если он ложится на строку. + row_props = row_format_props(row) + if row_props: + register_format(row_props) + + rs_name, rs_apply = row_style_spec(row.get('rowStyle'), 'row') + cells_style = None if rs_apply == 'row' else rs_name # rowStyle gap-fill format - if row.get('rowStyle'): - register_cell_format(row['rowStyle'], '') + if cells_style: + register_cell_format(cells_style, '') # Explicit cell formats if row.get('cells'): for cell in row['cells']: - cell_style = cell.get('style') or row.get('rowStyle') or 'default' + cell_style = cell.get('style') or cells_style or 'default' ft = get_fill_type(cell) register_cell_format(cell_style, ft) @@ -1133,9 +1171,14 @@ def main(): row_cells = [] # Determine row height format + # Формат самой строки — высота, скрытие и стиль строки, если он ложится на строку row_format_idx = 0 - if row.get('height'): - row_format_idx = register_format({'height': int(row['height'])}) + row_props = row_format_props(row) + if row_props: + row_format_idx = register_format(row_props) + + rs_name, rs_apply = row_style_spec(row.get('rowStyle'), 'row') + cells_style = None if rs_apply == 'row' else rs_name if row.get('cells') and len(row['cells']) > 0: row_has_content = True @@ -1185,7 +1228,7 @@ def main(): col_start = int(cell['col']) col_span = int(cell.get('span', 1)) rowspan = int(cell.get('rowspan', 1)) - cell_style = cell.get('style') or row.get('rowStyle') or 'default' + cell_style = cell.get('style') or cells_style or 'default' ft = get_fill_type(cell) fmt_idx = register_cell_format(cell_style, ft) @@ -1216,8 +1259,8 @@ def main(): merges.append(merge) # Generate gap-fill cells for rowStyle - if row.get('rowStyle'): - gap_fmt_idx = register_cell_format(row['rowStyle'], '') + if cells_style: + gap_fmt_idx = register_cell_format(cells_style, '') for c in range(1, total_columns + 1): if c not in occupied_cols: row_cells.append({ @@ -1232,10 +1275,10 @@ def main(): # Sort cells by column row_cells.sort(key=lambda x: x['Col']) - elif row.get('rowStyle'): + elif cells_style: # Row with only rowStyle, no explicit cells row_has_content = True - gap_fmt_idx = register_cell_format(row['rowStyle'], '') + gap_fmt_idx = register_cell_format(cells_style, '') for c in range(1, total_columns + 1): if c in rowspan_occupied: continue diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index b782e59d..36f0a6ac 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.12 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.13 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -499,6 +499,30 @@ foreach ($r in $rowData.Values) { } } +# Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, +# поэтому в именованный стиль попадать не должно. +function Get-RowStyleFmt { + param($fmt) + if (-not $fmt) { return $null } + $props = Get-StyleProps $fmt + $props.Remove('hidden') | Out-Null + if ($props.Count -eq 0) { return $null } + $reduced = [ordered]@{} + foreach ($tag in $fmt.Props.Keys) { + if ($tag -cne 'hidden') { $reduced[$tag] = $fmt.Props[$tag] } + } + return @{ FontIdx = $fmt.FontIdx; Width = 0; Height = 0; FillType = ""; Props = $reduced } +} + +# Строка — тоже владелец формата: её оформление становится именованным стилем. +foreach ($rd in $rowData.Values) { + if ($rd.FormatIdx -le 0) { continue } + $rf = Get-RowStyleFmt (Get-Format $rd.FormatIdx) + if (-not $rf) { continue } + $key = Get-StyleKey $rf + if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $rf } +} + # Колонка — третий владелец формата, её оформление тоже становится именованным стилем. foreach ($cs in $columnSets) { foreach ($fi in $cs.FmtIdx.Values) { @@ -740,11 +764,13 @@ foreach ($area in $blocks) { $dslRow = [ordered]@{} - # Row height - if ($rd.FormatIdx -gt 0) { - $rowFmt = Get-Format $rd.FormatIdx - if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } - } + # Формат самой строки: высота и скрытие — её собственные свойства (у ячейки таких + # нет), остальное — оформление, оно же может лежать и на ячейках. + $rowFmt = if ($rd.FormatIdx -gt 0) { Get-Format $rd.FormatIdx } else { $null } + $rowOwn = if ($rowFmt) { Get-StyleProps $rowFmt } else { [ordered]@{} } + $rowHidden = ($rowOwn['hidden'] -eq $true) + if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height } + if ($rowHidden) { $dslRow["hidden"] = $true } # Separate content cells from gap-fill cells $contentCells = @() @@ -780,7 +806,25 @@ foreach ($area in $blocks) { } } - if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName } + # Стиль строки и стиль ячеек — независимые вещи: платформа их часто, но не всегда + # пишет одинаковыми. Один ключ с модификатором apply покрывает все три случая. + $ownFmt = Get-RowStyleFmt $rowFmt + $ownKey = if ($ownFmt) { Get-StyleKey $ownFmt } else { $null } + $ownName = if ($ownKey -and $styleNames.Contains($ownKey)) { $styleNames[$ownKey] } else { $null } + $cellsName = if ($rowStyleName -and $rowStyleName -ne "default") { $rowStyleName } else { $null } + + if ($ownName -and $cellsName -and $ownKey -ceq $rowStyleKey) { + $dslRow["rowStyle"] = $ownName + } elseif ($ownName -and -not $cellsName) { + $dslRow["rowStyle"] = [ordered]@{ style = $ownName; apply = "row" } + } elseif ($cellsName -and -not $ownName) { + $dslRow["rowStyle"] = [ordered]@{ style = $cellsName; apply = "cells" } + } elseif ($ownName -and $cellsName) { + # Стили разные — строке своё, ячейкам своё; ячейкам стиль раздаётся поячеечно. + $dslRow["rowStyle"] = [ordered]@{ style = $ownName; apply = "row" } + $rowStyleName = $null + $rowStyleKey = $null + } # Build cell list $dslCells = @() @@ -938,7 +982,10 @@ foreach ($a in $dslAreas) { # Строка может быть массивом (короткая форма) — у неё нет свойства cells, и без этой # ветки стиль, использованный только внутри такой строки, вырезался как «неиспользуемый». $cellList = if ($r -is [array]) { $r } else { $r.cells } - if ($r -isnot [array] -and $r.rowStyle) { $usedStyles[$r.rowStyle] = $true } + if ($r -isnot [array] -and $r.rowStyle) { + $rs = $r.rowStyle + $usedStyles[$(if ($rs -is [System.Collections.IDictionary]) { $rs['style'] } else { $rs })] = $true + } if ($cellList) { foreach ($c in $cellList) { if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } } } } } diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 3069322c..0678db4f 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.12 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.13 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -659,6 +659,31 @@ def main(): style_keys[key] = fmt format_to_style_key[cell["FormatIdx"]] = key + def row_style_fmt(fmt): + """Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, + поэтому в именованный стиль попадать не должно.""" + if not fmt: + return None + props = style_props(fmt) + props.pop("hidden", None) + if not props: + return None + reduced = OrderedDict() + for tag, raw in fmt["Props"].items(): + if tag != "hidden": + reduced[tag] = raw + return {"FontIdx": fmt["FontIdx"], "Width": 0, "Height": 0, + "FillType": "", "Props": reduced} + + # Строка — тоже владелец формата: её оформление становится именованным стилем. + for rd in row_data.values(): + rf = row_style_fmt(get_format(rd["FormatIdx"])) if rd["FormatIdx"] > 0 else None + if not rf: + continue + key = get_style_key(rf) + if key not in style_keys: + style_keys[key] = rf + # Колонка — третий владелец формата, её оформление тоже становится именованным стилем. for cs in column_sets: for fi in cs["FmtIdx"].values(): @@ -870,11 +895,15 @@ def main(): dsl_row = OrderedDict() - # Row height - if rd["FormatIdx"] > 0: - row_fmt = get_format(rd["FormatIdx"]) - if row_fmt and row_fmt["Height"] > 0: - dsl_row["height"] = row_fmt["Height"] + # Формат самой строки: высота и скрытие — её собственные свойства (у ячейки + # таких нет), остальное — оформление, оно же может лежать и на ячейках. + row_fmt = get_format(rd["FormatIdx"]) if rd["FormatIdx"] > 0 else None + row_own = style_props(row_fmt) if row_fmt else OrderedDict() + row_hidden = row_own.pop("hidden", False) + if row_fmt and row_fmt["Height"] > 0: + dsl_row["height"] = row_fmt["Height"] + if row_hidden: + dsl_row["hidden"] = True # Separate content cells from gap-fill cells content_cells = [] @@ -904,8 +933,25 @@ def main(): if row_style_key in style_names: row_style_name = style_names[row_style_key] - if row_style_name and row_style_name != "default": - dsl_row["rowStyle"] = row_style_name + # Стиль строки и стиль ячеек — независимые вещи: платформа их часто, но не всегда + # пишет одинаковыми. Один ключ с модификатором apply покрывает все три случая. + own_fmt = row_style_fmt(row_fmt) + own_key = get_style_key(own_fmt) if own_fmt else None + own_name = style_names.get(own_key) if own_key else None + cells_name = row_style_name if row_style_name and row_style_name != "default" else None + + if own_name and cells_name and own_key == row_style_key: + dsl_row["rowStyle"] = own_name + elif own_name and not cells_name: + dsl_row["rowStyle"] = OrderedDict([("style", own_name), ("apply", "row")]) + elif cells_name and not own_name: + dsl_row["rowStyle"] = OrderedDict([("style", cells_name), ("apply", "cells")]) + elif own_name and cells_name: + # Стили разные — строке своё, ячейкам своё; ячейкам стиль раздаётся ниже + # поячеечно, поэтому здесь пишем только строку. + dsl_row["rowStyle"] = OrderedDict([("style", own_name), ("apply", "row")]) + row_style_name = None + row_style_key = None # Build cell list dsl_cells = [] @@ -1075,7 +1121,8 @@ def main(): cell_list = r else: if "rowStyle" in r: - used_styles.add(r["rowStyle"]) + rs = r["rowStyle"] + used_styles.add(rs["style"] if isinstance(rs, dict) else rs) cell_list = r.get("cells") or [] # Список ячеек может быть позиционным: строки, None и маркеры стиля не несут, # стиль бывает только у объектного элемента. diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index bbb64a76..e21198cc 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -127,12 +127,16 @@ | Поле | По умолч. | Описание | |------|-----------|----------| | `height` | — | Высота строки (если не задана, используется авто) | -| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) | +| `hidden` | `false` | Скрыть строку | +| `rowStyle` | — | Стиль строки: ложится и на саму строку, и на ВСЕ её колонки (заполняет пустоты рамками) | | `cells` | `[]` | Массив ячеек | | `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) | Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`. +`height` и `hidden` — собственные свойства строки: у ячейки таких нет, и в её оформление они +не попадают. Всё остальное оформление строки задаётся через `rowStyle`. + ### Короткая форма: строка массивом Вместо объекта строка может быть массивом ячеек — позиция определяется порядком, `col` не указывается. @@ -203,9 +207,11 @@ Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста. -## `rowStyle` — автозаполнение +## `rowStyle` — оформление строки -Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. +Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. + +Стиль конкретной ячейки (`style`) перекрывает `rowStyle` для этой ячейки. Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются. @@ -216,8 +222,6 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - ячейки-поля ввода (`containsValue` / `valueType` / `controlType`); -- оформление СТРОКИ помимо высоты: у строки задаётся только `height`, а скрытие, - шрифт и прочее платформа хранит и у неё тоже; - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/tests/skills/cases/mxl-compile/row-own-format.json b/tests/skills/cases/mxl-compile/row-own-format.json new file mode 100644 index 00000000..85b3a8a7 --- /dev/null +++ b/tests/skills/cases/mxl-compile/row-own-format.json @@ -0,0 +1,38 @@ +{ + "name": "Формат строки: скрытие, высота и режимы применения стиля", + "input": { + "columns": 3, + "defaultWidth": 20, + "fonts": { + "default": { "face": "Arial", "size": 10 }, + "bold": { "face": "Arial", "size": 10, "bold": true } + }, + "styles": { + "фон": { "backColor": "#EBEBEB" }, + "рамка": { "border": "Solid" }, + "жирный": { "font": "bold" } + }, + "areas": [ + { + "name": "Тело", + "rows": [ + { "height": 20, "hidden": true, "cells": [ + { "col": 1, "text": "скрытая строка" } + ]}, + { "rowStyle": "фон", "cells": [ + { "col": 1, "text": "и строке, и ячейкам" } + ]}, + { "rowStyle": { "style": "жирный", "apply": "row" }, "cells": [ + { "col": 1, "text": "только строке" } + ]}, + { "rowStyle": { "style": "рамка", "apply": "cells" }, "cells": [ + { "col": 1, "text": "только ячейкам" } + ]} + ] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "validatePath": "Template.xml", + "expect": { "files": ["Template.xml"] } +} diff --git a/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml b/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml index 48797436..e89989af 100644 --- a/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml @@ -51,9 +51,10 @@ 0 + 5 - 5 + 6 ru @@ -64,7 +65,7 @@ - 5 + 6 ru @@ -75,7 +76,7 @@ - 5 + 6 ru @@ -86,7 +87,7 @@ - 5 + 6 ru @@ -97,7 +98,7 @@ - 5 + 6 ru @@ -108,7 +109,7 @@ - 5 + 6 ru @@ -149,6 +150,9 @@ 30 + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml b/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml index 62fa5f7f..01cf1a4f 100644 --- a/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml @@ -38,21 +38,22 @@ 3 + 3 - 4 + 5 Код - 4 + 5 Имя - 4 + 5 Значение @@ -99,6 +100,9 @@ 1 Center + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml b/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml index 84d01015..ad386d14 100644 --- a/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml @@ -15,9 +15,10 @@ 0 + 2 - 2 + 3 ru @@ -28,7 +29,7 @@ - 2 + 3 ru @@ -39,7 +40,7 @@ - 2 + 3 ru @@ -50,7 +51,7 @@ - 2 + 3 ru @@ -64,27 +65,28 @@ 1 + 4 - 4 + 6 Дата - 5 + 7 Товар - 6 + 8 Количество - 6 + 8 Цена @@ -121,11 +123,18 @@ 10 + + 0 + Center + 0 0 Center + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml b/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml index e73e88fd..e061c767 100644 --- a/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml @@ -15,9 +15,10 @@ 0 + 2 - 3 + 4 ru @@ -28,7 +29,7 @@ - 3 + 4 ru @@ -40,7 +41,7 @@ 3 - 3 + 4 ru @@ -54,10 +55,11 @@ 1 + 2 1 - 4 + 5 ru @@ -68,7 +70,7 @@ - 4 + 5 ru @@ -82,27 +84,28 @@ 2 + 2 - 5 + 6 Номер - 6 + 7 Наименование - 6 + 7 Артикул - 6 + 7 Итого @@ -157,6 +160,9 @@ 10 + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml b/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml index 825bf5c1..147c8f28 100644 --- a/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml @@ -27,9 +27,10 @@ 1 + 4 - 4 + 5 ru @@ -40,7 +41,7 @@ - 4 + 5 ru @@ -51,7 +52,7 @@ - 4 + 5 ru @@ -62,7 +63,7 @@ - 4 + 5 ru @@ -76,28 +77,29 @@ 2 + 4 - 5 + 6 НомерСтроки - 5 + 6 Товар Номенклатура - 6 + 7 Количество - 6 + 7 Сумма @@ -109,7 +111,7 @@ 2 - 7 + 8 ru @@ -120,7 +122,7 @@ - 8 + 9 Всего @@ -192,6 +194,9 @@ Center Parameter + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/page-a4-landscape/Template.xml b/tests/skills/cases/mxl-compile/snapshots/page-a4-landscape/Template.xml index 4f2e2485..3ce62301 100644 --- a/tests/skills/cases/mxl-compile/snapshots/page-a4-landscape/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/page-a4-landscape/Template.xml @@ -45,6 +45,7 @@ 0 + 3 3 @@ -105,33 +106,34 @@ 1 + 4 - 5 + 6 Номер - 6 + 7 Описание - 5 + 6 ЕдИзм - 5 + 6 Количество - 6 + 7 Сумма @@ -177,6 +179,9 @@ 0 Center + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/parameters-and-templates/Template.xml b/tests/skills/cases/mxl-compile/snapshots/parameters-and-templates/Template.xml index 56fa7e27..46ec87d3 100644 --- a/tests/skills/cases/mxl-compile/snapshots/parameters-and-templates/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/parameters-and-templates/Template.xml @@ -69,21 +69,22 @@ 2 + 4 - 5 + 6 Позиция - 5 + 6 Товар - 5 + 6 Цена @@ -138,6 +139,9 @@ 0 Template + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml b/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml index 21ffffe2..fd40ade9 100644 --- a/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml @@ -139,9 +139,10 @@ 4 + 6 - 7 + 8 ru @@ -152,7 +153,7 @@ - 7 + 8 ru @@ -164,7 +165,7 @@ 6 - 7 + 8 ru @@ -175,7 +176,7 @@ - 7 + 8 ru @@ -186,7 +187,7 @@ - 7 + 8 ru @@ -197,7 +198,7 @@ - 7 + 8 ru @@ -211,15 +212,16 @@ 5 + 6 - 8 + 9 НомерСтроки - 9 + 10 Товар Номенклатура @@ -227,25 +229,25 @@ 6 - 8 + 9 ЕдИзм - 10 + 11 Количество - 10 + 11 Цена - 10 + 11 Сумма @@ -257,7 +259,7 @@ 7 - 11 + 12 ru @@ -269,7 +271,7 @@ 9 - 12 + 13 ИтогоСумма @@ -281,7 +283,7 @@ 7 - 11 + 12 ru @@ -293,7 +295,7 @@ 9 - 12 + 13 ИтогоНДС @@ -323,7 +325,7 @@ 3 - 13 + 14 Отпустил @@ -335,7 +337,7 @@ 3 - 14 + 15 ru @@ -369,7 +371,7 @@ 3 - 13 + 14 Получил @@ -381,7 +383,7 @@ 3 - 14 + 15 ru @@ -546,6 +548,9 @@ 0 Parameter + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-compile/snapshots/row-own-format/Template.xml b/tests/skills/cases/mxl-compile/snapshots/row-own-format/Template.xml new file mode 100644 index 00000000..ca52436f --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/row-own-format/Template.xml @@ -0,0 +1,142 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + 2 + + + 0 + + + ru + скрытая строка + + + + + + + + 1 + + 3 + + + 4 + + + ru + и строке, и ячейкам + + + + + + + 4 + + + + + 4 + + + + + + 2 + + 5 + + + 0 + + + ru + только строке + + + + + + + + 3 + + + + 6 + + + ru + только ячейкам + + + + + + + 6 + + + + + 6 + + + + + true + 1 + 4 + 4 + + Тело + + Rows + 0 + 3 + -1 + -1 + + + + Solid + + + + + 20 + + + 20 + true + + + #EBEBEB + + + 0 + #EBEBEB + + + 1 + + + 0 + 0 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/styles-fonts-borders/Template.xml b/tests/skills/cases/mxl-compile/snapshots/styles-fonts-borders/Template.xml index 922602fb..68298ccd 100644 --- a/tests/skills/cases/mxl-compile/snapshots/styles-fonts-borders/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/styles-fonts-borders/Template.xml @@ -55,6 +55,7 @@ 2 + 6 6 @@ -115,33 +116,34 @@ 3 + 7 - 8 + 9 НомерСтроки - 9 + 10 Товар - 10 + 11 Количество - 10 + 11 Цена - 10 + 11 Сумма @@ -153,7 +155,7 @@ 3 - 11 + 12 ru @@ -164,7 +166,7 @@ - 12 + 13 Всего @@ -175,7 +177,7 @@ - 13 + 14 ru @@ -304,6 +306,9 @@ 1 1 + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-merged-cells/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-merged-cells/Template.xml index b4b934e2..e0e3f088 100644 --- a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-merged-cells/Template.xml +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-merged-cells/Template.xml @@ -15,9 +15,10 @@ 0 + 2 - 3 + 4 ru @@ -28,7 +29,7 @@ - 3 + 4 ru @@ -42,10 +43,11 @@ 1 + 2 1 - 2 + 3 ru @@ -56,7 +58,7 @@ - 2 + 3 ru @@ -100,6 +102,9 @@ 10 + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-multiple-areas/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-multiple-areas/Template.xml index 2ab37bca..5aadb3bf 100644 --- a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-multiple-areas/Template.xml +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-multiple-areas/Template.xml @@ -27,27 +27,28 @@ 1 + 4 - 5 + 6 Номер - 5 + 6 Товар - 6 + 7 Количество - 6 + 7 Сумма @@ -59,7 +60,7 @@ 2 - 7 + 8 ru @@ -70,7 +71,7 @@ - 8 + 9 Всего @@ -130,6 +131,9 @@ 0 Parameter + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-info/snapshots/areas-and-params/Template.xml b/tests/skills/cases/mxl-info/snapshots/areas-and-params/Template.xml index 020667c4..95d06fee 100644 --- a/tests/skills/cases/mxl-info/snapshots/areas-and-params/Template.xml +++ b/tests/skills/cases/mxl-info/snapshots/areas-and-params/Template.xml @@ -50,34 +50,35 @@ 2 + 3 - 4 + 5 НомерСтроки - 4 + 5 Товар Номенклатура - 5 + 6 Количество - 5 + 6 Цена - 5 + 6 Сумма @@ -89,7 +90,7 @@ 3 - 6 + 7 ru @@ -100,7 +101,7 @@ - 7 + 8 Всего @@ -177,6 +178,9 @@ 0 Parameter + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-info/snapshots/detail-params/Template.xml b/tests/skills/cases/mxl-info/snapshots/detail-params/Template.xml index 9b667ee4..9c20df2e 100644 --- a/tests/skills/cases/mxl-info/snapshots/detail-params/Template.xml +++ b/tests/skills/cases/mxl-info/snapshots/detail-params/Template.xml @@ -15,22 +15,23 @@ 0 + 2 - 3 + 4 НомерСтроки - 3 + 4 Товар Номенклатура - 3 + 4 Сумма @@ -57,6 +58,9 @@ 10 + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-validate/snapshots/valid-complex/Template.xml b/tests/skills/cases/mxl-validate/snapshots/valid-complex/Template.xml index 6e81ecf6..aa941671 100644 --- a/tests/skills/cases/mxl-validate/snapshots/valid-complex/Template.xml +++ b/tests/skills/cases/mxl-validate/snapshots/valid-complex/Template.xml @@ -26,6 +26,7 @@ 1 + 3 3 @@ -65,10 +66,11 @@ 2 + 4 1 - 4 + 5 ru @@ -79,7 +81,7 @@ - 4 + 5 ru @@ -93,27 +95,28 @@ 3 + 4 - 5 + 6 Номер - 5 + 6 Имя - 5 + 6 Код - 5 + 6 Сумма @@ -194,6 +197,9 @@ 0 Center + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-validate/snapshots/valid-print-form/Template.xml b/tests/skills/cases/mxl-validate/snapshots/valid-print-form/Template.xml index eeb3c9dc..029c0e94 100644 --- a/tests/skills/cases/mxl-validate/snapshots/valid-print-form/Template.xml +++ b/tests/skills/cases/mxl-validate/snapshots/valid-print-form/Template.xml @@ -57,33 +57,34 @@ 1 + 5 - 6 + 7 Номер - 6 + 7 Товар - 6 + 7 ЕдИзм - 7 + 8 Кол - 7 + 8 Сумма @@ -95,7 +96,7 @@ 3 - 8 + 9 ru @@ -106,7 +107,7 @@ - 9 + 10 Всего @@ -171,6 +172,9 @@ Center Parameter + + 0 + 0 0 diff --git a/tests/skills/cases/mxl-validate/snapshots/valid-with-detailed/Template.xml b/tests/skills/cases/mxl-validate/snapshots/valid-with-detailed/Template.xml index 67a1cf39..ae9e2cd7 100644 --- a/tests/skills/cases/mxl-validate/snapshots/valid-with-detailed/Template.xml +++ b/tests/skills/cases/mxl-validate/snapshots/valid-with-detailed/Template.xml @@ -15,15 +15,16 @@ 0 + 2 - 3 + 4 Ключ - 3 + 4 Значение @@ -50,6 +51,9 @@ 10 + + 0 + 0 0