feat(mxl-compile,mxl-decompile): формат самой строки

У строки есть собственный формат: платформа хранит в нём скрытие (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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 18:36:41 +03:00
co-authored by Claude Opus 5
parent ccbd488d63
commit ba25aa4a00
25 changed files with 611 additions and 167 deletions
@@ -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 } } }
}
}
@@ -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 и маркеры стиля не несут,
# стиль бывает только у объектного элемента.