From c9e490bd25ead959f5233e9bdfb6c7530726d33d Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 11 Aug 2026 16:15:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(mxl-decompile):=20=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8=D0=BA=D0=BE=D0=BC,=20=D0=B0=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=B4=D0=B5=D0=B2=D1=8F=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Декомпилятор снимал из только шрифт, четыре рамки, выравнивания, перенос и форматную строку — остальные тридцать с лишним свойств терялись молча. Компилятор их уже умеет, но до раундтрипа они не доезжали. Теперь формат читается целиком по общей таблице типов — той же, что у компилятора: рамки разворачиваются в описание линии из палитры, булевы и целые приводятся по типу, цвет из web/win-палитры возвращается в привычную запись (префикс узла разрешается в URI пространства имён). Имя стиля осталось читаемой меткой по заметным свойствам: различает стили ключ, имя лишь помогает автору ориентироваться. Co-Authored-By: Claude Opus 5 (1M context) --- .../mxl-decompile/scripts/mxl-decompile.ps1 | 230 ++++++++++------ .../mxl-decompile/scripts/mxl-decompile.py | 257 ++++++++++-------- 2 files changed, 287 insertions(+), 200 deletions(-) diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index e71b80f3..9cd971ca 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.9 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.10 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -29,6 +29,51 @@ $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") $ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") $ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +# Тип значения каждого тега формата — выведен из корпуса, а не выписан на глаз. +# line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: +# ml — многоязычная строка; enum — замкнутый список. +# containsValue / valueType / controlType сюда НЕ входят: это свойства конкретной ячейки, +# а стиль — сущность общая, один на многие ячейки. +function Get-FormatTagKind { + return @{ + 'autoIndent' = 'int'; 'autoMarkIncomplete' = 'bool'; 'autoWidthCalculation' = 'bool' + 'backColor' = 'color'; 'border' = 'line'; 'borderColor' = 'color' + 'bottomBorder' = 'line'; 'bySelectedColumns' = 'bool'; 'columnSizeChange' = 'enum' + 'detailsUse' = 'enum'; 'drawingBorder' = 'int' + 'drawingHaveBottomBorder' = 'bool'; 'drawingHaveLeftBorder' = 'bool' + 'drawingHaveRightBorder' = 'bool'; 'drawingHaveTopBorder' = 'bool' + 'editFormat' = 'ml'; 'fillType' = 'enum'; 'font' = 'int'; 'format' = 'ml' + 'height' = 'int'; 'hidden' = 'bool'; 'horizontalAlignment' = 'enum' + 'hyperLink' = 'bool'; 'indent' = 'int'; 'leftBorder' = 'line' + 'markNegatives' = 'bool'; 'mask' = 'ml'; 'pattern' = 'enum'; 'patternColor' = 'color' + 'picHorizontalAlignment' = 'enum'; 'picIndex' = 'int'; 'picVerticalAlignment' = 'enum' + 'pictureSizeMode' = 'enum'; 'print' = 'bool'; 'protection' = 'bool' + 'rightBorder' = 'line'; 'textColor' = 'color'; 'textOrientation' = 'int' + 'textPlacement' = 'enum'; 'textPosition' = 'enum'; 'topBorder' = 'line' + 'verticalAlignment' = 'enum'; 'width' = 'int'; 'widthWeightFactor' = 'int' + } +} + +# Теги, значение которых — многоязычная строка ( на язык), а не скаляр. +function Get-FormatMlTags { + return @{ 'format' = $true; 'editFormat' = $true; 'mask' = $true } +} + +# Цвет платформа хранит значением с префиксом пространства имён. style: объявлен в корне +# документа и читается как есть; web/win объявлены прямо на узле сгенерированным префиксом +# (d3p1), поэтому его надо разрешить в URI и вернуть привычное web:/win:. +function ConvertTo-DslColor { + param($node, [string]$val) + if ($val -notlike '*:*' -or $val -like 'style:*') { return $val } + $prefix = $val.Substring(0, $val.IndexOf(':')) + $name = $val.Substring($val.IndexOf(':') + 1) + # get_*(): XML-адаптер PowerShell перекрывает .NET-члены одноимёнными атрибутами узла. + $uri = $node.GetNamespaceOfPrefix($prefix) + if ($uri -ceq 'http://v8.1c.ru/8.1/data/ui/colors/web') { return "web:$name" } + if ($uri -ceq 'http://v8.1c.ru/8.1/data/ui/colors/windows') { return "win:$name" } + return $val +} + # --- 2. Extract font palette --- # Размер шрифта бывает дробным (8.3, 11.3 — в корпусе ERP это треть макетов). [int] его @@ -61,50 +106,47 @@ foreach ($fNode in $root.SelectNodes("d:font", $ns)) { $rawLines = @() foreach ($lNode in $root.SelectNodes("d:line", $ns)) { - $rawLines += @{ Width = [int]$lNode.GetAttribute("width") } + $st = $lNode.SelectSingleNode("v8ui:style", $ns) + $rawLines += @{ + Width = [int]$lNode.GetAttribute("width") + Gap = $lNode.GetAttribute("gap") + Style = if ($st) { $st.InnerText } else { "Solid" } + } } # --- 4. Extract format palette --- +# Формат читаем целиком — «тег → значение», а не выборочные девять полей. Тип значения +# берём из общей таблицы, она же у компилятора. +$formatKinds = Get-FormatTagKind +$formatMl = Get-FormatMlTags $rawFormats = @() foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { + # Совместимость с прежними полями: ими пользуются ширина колонки, высота строки и + # признак заполнения — они не часть стиля. $fmt = @{ - FontIdx = -1 - LB = -1; TB = -1; RB = -1; BB = -1 - Width = 0; Height = 0 - HA = ""; VA = "" - Wrap = $false; FillType = ""; DataFormat = "" + FontIdx = -1; Width = 0; Height = 0; FillType = "" + Props = [ordered]@{} } - $n = $fmtNode.SelectSingleNode("d:font", $ns) - if ($n) { $fmt.FontIdx = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:leftBorder", $ns) - if ($n) { $fmt.LB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:topBorder", $ns) - if ($n) { $fmt.TB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:rightBorder", $ns) - if ($n) { $fmt.RB = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns) - if ($n) { $fmt.BB = [int]$n.InnerText } + foreach ($child in $fmtNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + $tag = $child.get_LocalName() + if ($formatMl.ContainsKey($tag)) { + $item = $child.SelectSingleNode("v8:item/v8:content", $ns) + if ($item -and $item.InnerText) { $fmt.Props[$tag] = $item.InnerText } + continue + } + $val = $child.InnerText + if ([string]::IsNullOrWhiteSpace($val)) { continue } + if ($formatKinds[$tag] -ceq 'color') { $val = ConvertTo-DslColor $child $val } + $fmt.Props[$tag] = $val + } - $n = $fmtNode.SelectSingleNode("d:width", $ns) - if ($n) { $fmt.Width = [int]$n.InnerText } - $n = $fmtNode.SelectSingleNode("d:height", $ns) - if ($n) { $fmt.Height = [int]$n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns) - if ($n) { $fmt.HA = $n.InnerText } - $n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns) - if ($n) { $fmt.VA = $n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:textPlacement", $ns) - if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true } - - $n = $fmtNode.SelectSingleNode("d:fillType", $ns) - if ($n) { $fmt.FillType = $n.InnerText } - - $n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns) - if ($n) { $fmt.DataFormat = $n.InnerText } + if ($fmt.Props.Contains('font')) { $fmt.FontIdx = [int]$fmt.Props['font'] } + if ($fmt.Props.Contains('width')) { $fmt.Width = [int]$fmt.Props['width'] } + if ($fmt.Props.Contains('height')) { $fmt.Height = [int]$fmt.Props['height'] } + if ($fmt.Props.Contains('fillType')) { $fmt.FillType = $fmt.Props['fillType'] } $rawFormats += $fmt } @@ -317,43 +359,60 @@ function Get-DslText { # --- 9. Build style key (ignoring fillType) --- -function Get-BorderDesc { +# Свойства формата, которые стилем НЕ являются: ширина принадлежит колонке, высота — +# строке, вид заполнения выводится из того, чем задано содержимое ячейки. +$nonStyleTags = @('width', 'height', 'fillType', 'font') +$borderTags = @('border', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder') + +function ConvertTo-DslLine { + # Ссылку в палитру разворачиваем в описание линии. Ширина 1 подразумевается, поэтому + # обычная линия пишется просто именем стиля. + param($idx) + $i = [int]$idx + if ($i -lt 0 -or $i -ge $rawLines.Count) { return "Solid" } + $ln = $rawLines[$i] + $out = [ordered]@{ style = $ln.Style } + if ($ln.Width -ne 1) { $out['width'] = $ln.Width } + if ($ln.Gap -ceq 'true') { $out['gap'] = $true } + if ($out.Count -eq 1) { return $ln.Style } + return $out +} + +function Get-StyleProps { + # Свойства формата, попадающие в стиль, — в каноническом порядке тегов. param($fmt) - if (-not $fmt) { return @{ Border = "none"; Thick = $false } } - - $lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0 - $rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0 - - if (-not $lb -and -not $tb -and -not $rb -and -not $bb) { - return @{ Border = "none"; Thick = $false } - } - - $thick = $false - foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) { - if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) { - $thick = $true; break + $out = [ordered]@{} + if (-not $fmt) { return $out } + foreach ($tag in $fmt.Props.Keys) { + if ($nonStyleTags -ccontains $tag) { continue } + $val = $fmt.Props[$tag] + if ($borderTags -ccontains $tag) { + $out[$tag] = ConvertTo-DslLine $val + } elseif ($formatKinds[$tag] -ceq 'bool') { + $out[$tag] = ($val -ceq 'true') + } elseif ($formatKinds[$tag] -ceq 'int') { + $out[$tag] = [int]$val + } else { + $out[$tag] = $val } } - - if ($lb -and $tb -and $rb -and $bb) { - return @{ Border = "all"; Thick = $thick } - } - - $sides = @() - if ($tb) { $sides += "top" } - if ($bb) { $sides += "bottom" } - if ($lb) { $sides += "left" } - if ($rb) { $sides += "right" } - - return @{ Border = ($sides -join ","); Thick = $thick } + return $out } function Get-StyleKey { param($fmt) if (-not $fmt) { return "empty" } $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } - $bd = Get-BorderDesc $fmt - return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)" + $parts = @("f=$fi") + $props = Get-StyleProps $fmt + foreach ($tag in $props.Keys) { + $v = $props[$tag] + if ($v -is [System.Collections.IDictionary]) { + $v = (($v.Keys | ForEach-Object { "$_=$($v[$_])" }) -join ',') + } + $parts += "$tag=$v" + } + return ($parts -join '|') } # --- 10. Name fonts --- @@ -435,28 +494,36 @@ foreach ($r in $rowData.Values) { } } +# Имя стиля — читаемая метка по самым заметным свойствам. Полнота тут не нужна: +# различает стили ключ, имя лишь помогает автору ориентироваться. function Name-Style { param($fmt) if (-not $fmt) { return "default" } $parts = @() + $props = Get-StyleProps $fmt $fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 } if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { $parts += $fontNames[$fi] } - $bd = Get-BorderDesc $fmt - if ($bd.Border -ne "none") { - if ($bd.Border -eq "all") { $parts += "bordered" } - else { $parts += "border-$($bd.Border)" } + if ($props.Contains('border')) { + $parts += "bordered" + } else { + $sides = @() + foreach ($s in @('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder')) { + if ($props.Contains($s)) { $sides += $s.Substring(0, $s.Length - 6) } + } + if ($sides.Count -gt 0) { $parts += "border-$($sides -join ',')" } } - if ($fmt.HA -eq "Center") { $parts += "center" } - elseif ($fmt.HA -eq "Right") { $parts += "right" } - if ($fmt.VA -eq "Center") { $parts += "vcenter" } - elseif ($fmt.VA -eq "Top") { $parts += "vtop" } - if ($fmt.Wrap) { $parts += "wrap" } - if ($fmt.DataFormat) { $parts += "fmt" } + if ($props['horizontalAlignment'] -ceq "Center") { $parts += "center" } + elseif ($props['horizontalAlignment'] -ceq "Right") { $parts += "right" } + if ($props['verticalAlignment'] -ceq "Center") { $parts += "vcenter" } + elseif ($props['verticalAlignment'] -ceq "Top") { $parts += "vtop" } + if ($props['textPlacement'] -ceq "Wrap") { $parts += "wrap" } + if ($props.Contains('backColor')) { $parts += "bg" } + if ($props.Contains('format')) { $parts += "fmt" } if ($parts.Count -eq 0) { return "default" } return ($parts -join "-") @@ -479,21 +546,8 @@ foreach ($key in $styleKeys.Keys) { if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") { $sDef["font"] = $fontNames[$fi] } - if ($fmt.HA) { - $a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } } - if ($a) { $sDef["align"] = $a } - } - if ($fmt.VA) { - $a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } } - if ($a) { $sDef["valign"] = $a } - } - $bd = Get-BorderDesc $fmt - if ($bd.Border -ne "none") { - $sDef["border"] = $bd.Border - if ($bd.Thick) { $sDef["borderWidth"] = "thick" } - } - if ($fmt.Wrap) { $sDef["wrap"] = $true } - if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat } + $props = Get-StyleProps $fmt + foreach ($tag in $props.Keys) { $sDef[$tag] = $props[$tag] } $styleDefs[$name] = $sDef } diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 1055b516..8998cea2 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.9 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.10 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -69,6 +69,51 @@ def to_font_size(raw): return int(d) if d == int(d) else d +def format_tag_kind(): + """Тип значения каждого тега — выведен из корпуса, а не выписан на глаз. + line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: + ml — многоязычная строка; enum — замкнутый список (см. format_enum_values). + containsValue / valueType / controlType сюда НЕ входят: это свойства конкретной ячейки, + а стиль — сущность общая, один на многие ячейки.""" + return { + 'autoIndent': 'int', 'autoMarkIncomplete': 'bool', 'autoWidthCalculation': 'bool', + 'backColor': 'color', 'border': 'line', 'borderColor': 'color', + 'bottomBorder': 'line', 'bySelectedColumns': 'bool', 'columnSizeChange': 'enum', + 'detailsUse': 'enum', 'drawingBorder': 'int', + 'drawingHaveBottomBorder': 'bool', 'drawingHaveLeftBorder': 'bool', + 'drawingHaveRightBorder': 'bool', 'drawingHaveTopBorder': 'bool', + 'editFormat': 'ml', 'fillType': 'enum', 'font': 'int', 'format': 'ml', + 'height': 'int', 'hidden': 'bool', 'horizontalAlignment': 'enum', + 'hyperLink': 'bool', 'indent': 'int', 'leftBorder': 'line', + 'markNegatives': 'bool', 'mask': 'ml', 'pattern': 'enum', 'patternColor': 'color', + 'picHorizontalAlignment': 'enum', 'picIndex': 'int', 'picVerticalAlignment': 'enum', + 'pictureSizeMode': 'enum', 'print': 'bool', 'protection': 'bool', + 'rightBorder': 'line', 'textColor': 'color', 'textOrientation': 'int', + 'textPlacement': 'enum', 'textPosition': 'enum', 'topBorder': 'line', + 'verticalAlignment': 'enum', 'width': 'int', 'widthWeightFactor': 'int', + } + + +def format_ml_tags(): + """Теги, значение которых — многоязычная строка ( на язык), а не скаляр.""" + return {'format': True, 'editFormat': True, 'mask': True} + + +def color_to_dsl(node, val): + """Цвет платформа хранит значением с префиксом пространства имён. style: объявлен в корне + документа и читается как есть; web/win объявлены прямо на узле сгенерированным префиксом + (d3p1), поэтому его надо разрешить в URI и вернуть привычное web:/win:.""" + if ':' not in val or val.startswith('style:'): + return val + prefix, name = val.split(':', 1) + uri = node.nsmap.get(prefix) + if uri == 'http://v8.1c.ru/8.1/data/ui/colors/web': + return 'web:' + name + if uri == 'http://v8.1c.ru/8.1/data/ui/colors/windows': + return 'win:' + name + return val + + def int_of(node, default=0): if node is not None and node.text: return int(node.text) @@ -231,61 +276,50 @@ def main(): raw_lines = [] for l_node in findall(root, "d:line"): - raw_lines.append({"Width": int(l_node.get("width", "0"))}) + st = find(l_node, "v8ui:style") + raw_lines.append({ + "Width": int(l_node.get("width", "0")), + "Gap": l_node.get("gap", "false"), + "Style": (text_of(st) or "Solid") if st is not None else "Solid", + }) # --- 4. Extract format palette --- + # Формат читаем целиком — «тег → значение», а не выборочные девять полей. Тип значения + # берём из общей таблицы, она же у компилятора. + kinds = format_tag_kind() + ml_tags = format_ml_tags() + raw_formats = [] for fmt_node in findall(root, "d:format"): + # Совместимость с прежними полями: ими пользуются ширина колонки, высота строки и + # признак заполнения — они не часть стиля. fmt = { - "FontIdx": -1, - "LB": -1, "TB": -1, "RB": -1, "BB": -1, - "Width": 0, "Height": 0, - "HA": "", "VA": "", - "Wrap": False, "FillType": "", "DataFormat": "", + "FontIdx": -1, "Width": 0, "Height": 0, "FillType": "", "Props": OrderedDict(), } + for child in fmt_node: + tag = etree.QName(child).localname + if tag in ml_tags: + item = find(child, "v8:item/v8:content") + val = text_of(item) if item is not None else None + if val: + fmt["Props"][tag] = val + continue + val = (child.text or "").strip() + if not val: + continue + if kinds.get(tag) == "color": + val = color_to_dsl(child, val) + fmt["Props"][tag] = val - n = find(fmt_node, "d:font") - if n is not None and n.text: - fmt["FontIdx"] = int(n.text) - n = find(fmt_node, "d:leftBorder") - if n is not None and n.text: - fmt["LB"] = int(n.text) - n = find(fmt_node, "d:topBorder") - if n is not None and n.text: - fmt["TB"] = int(n.text) - n = find(fmt_node, "d:rightBorder") - if n is not None and n.text: - fmt["RB"] = int(n.text) - n = find(fmt_node, "d:bottomBorder") - if n is not None and n.text: - fmt["BB"] = int(n.text) - - n = find(fmt_node, "d:width") - if n is not None and n.text: - fmt["Width"] = int(n.text) - n = find(fmt_node, "d:height") - if n is not None and n.text: - fmt["Height"] = int(n.text) - - n = find(fmt_node, "d:horizontalAlignment") - if n is not None and n.text: - fmt["HA"] = n.text - n = find(fmt_node, "d:verticalAlignment") - if n is not None and n.text: - fmt["VA"] = n.text - - n = find(fmt_node, "d:textPlacement") - if n is not None and n.text == "Wrap": - fmt["Wrap"] = True - - n = find(fmt_node, "d:fillType") - if n is not None and n.text: - fmt["FillType"] = n.text - - n = find(fmt_node, "d:format/v8:item/v8:content") - if n is not None and n.text: - fmt["DataFormat"] = n.text + if "font" in fmt["Props"]: + fmt["FontIdx"] = int(fmt["Props"]["font"]) + if "width" in fmt["Props"]: + fmt["Width"] = int(fmt["Props"]["width"]) + if "height" in fmt["Props"]: + fmt["Height"] = int(fmt["Props"]["height"]) + if "fillType" in fmt["Props"]: + fmt["FillType"] = fmt["Props"]["fillType"] raw_formats.append(fmt) @@ -493,45 +527,54 @@ def main(): # --- 9. Build style key (ignoring fillType) --- - def get_border_desc(fmt): + # Свойства формата, которые стилем НЕ являются: ширина принадлежит колонке, высота — + # строке, вид заполнения выводится из того, чем задано содержимое ячейки. + NON_STYLE_TAGS = ("width", "height", "fillType", "font") + BORDER_TAGS = ("border", "leftBorder", "topBorder", "rightBorder", "bottomBorder") + + def line_to_dsl(idx): + """Ссылку в палитру разворачиваем в описание линии. Ширина 1 подразумевается, + поэтому обычная линия пишется просто именем стиля.""" + i = int(idx) + if i < 0 or i >= len(raw_lines): + return "Solid" + ln = raw_lines[i] + out = OrderedDict() + out["style"] = ln["Style"] + if ln["Width"] != 1: + out["width"] = ln["Width"] + if ln["Gap"] == "true": + out["gap"] = True + if len(out) == 1: + return out["style"] + return out + + def style_props(fmt): + """Свойства формата, попадающие в стиль, — в каноническом порядке тегов.""" if not fmt: - return {"Border": "none", "Thick": False} - - lb = fmt["LB"] >= 0 - tb = fmt["TB"] >= 0 - rb = fmt["RB"] >= 0 - bb = fmt["BB"] >= 0 - - if not lb and not tb and not rb and not bb: - return {"Border": "none", "Thick": False} - - thick = False - for b_idx in [fmt["LB"], fmt["TB"], fmt["RB"], fmt["BB"]]: - if b_idx >= 0 and b_idx < len(raw_lines) and raw_lines[b_idx]["Width"] >= 2: - thick = True - break - - if lb and tb and rb and bb: - return {"Border": "all", "Thick": thick} - - sides = [] - if tb: - sides.append("top") - if bb: - sides.append("bottom") - if lb: - sides.append("left") - if rb: - sides.append("right") - - return {"Border": ",".join(sides), "Thick": thick} + return OrderedDict() + out = OrderedDict() + for tag, val in fmt["Props"].items(): + if tag in NON_STYLE_TAGS: + continue + if tag in BORDER_TAGS: + out[tag] = line_to_dsl(val) + elif kinds.get(tag) == "bool": + out[tag] = (val == "true") + elif kinds.get(tag) == "int": + out[tag] = int(val) + else: + out[tag] = val + return out def get_style_key(fmt): if not fmt: return "empty" fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0 - bd = get_border_desc(fmt) - return f"f={fi}|b={bd['Border']}|bw={bd['Thick']}|ha={fmt['HA']}|va={fmt['VA']}|wr={fmt['Wrap']}|df={fmt['DataFormat']}" + parts = [f"f={fi}"] + for tag, val in style_props(fmt).items(): + parts.append(f"{tag}={val}") + return "|".join(parts) # --- 10. Name fonts --- @@ -614,32 +657,40 @@ def main(): format_to_style_key[cell["FormatIdx"]] = key def name_style(fmt): + """Имя стиля — читаемая метка по самым заметным свойствам. Полнота тут не нужна: + различает стили ключ, имя лишь помогает автору ориентироваться.""" if not fmt: return "default" parts = [] + props = style_props(fmt) fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0 if fi in font_names and font_names[fi] != "default": parts.append(font_names[fi]) - bd = get_border_desc(fmt) - if bd["Border"] != "none": - if bd["Border"] == "all": - parts.append("bordered") - else: - parts.append(f"border-{bd['Border']}") + if "border" in props: + parts.append("bordered") + else: + sides = [s[:-6] for s in ("leftBorder", "topBorder", "rightBorder", "bottomBorder") + if s in props] + if sides: + parts.append("border-" + ",".join(sides)) - if fmt["HA"] == "Center": + ha = props.get("horizontalAlignment") + if ha == "Center": parts.append("center") - elif fmt["HA"] == "Right": + elif ha == "Right": parts.append("right") - if fmt["VA"] == "Center": + va = props.get("verticalAlignment") + if va == "Center": parts.append("vcenter") - elif fmt["VA"] == "Top": + elif va == "Top": parts.append("vtop") - if fmt["Wrap"]: + if props.get("textPlacement") == "Wrap": parts.append("wrap") - if fmt["DataFormat"]: + if "backColor" in props: + parts.append("bg") + if "format" in props: parts.append("fmt") if len(parts) == 0: @@ -665,25 +716,7 @@ def main(): fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0 if fi in font_names and font_names[fi] != "default": s_def["font"] = font_names[fi] - if fmt["HA"]: - a_map = {"Left": "left", "Center": "center", "Right": "right"} - a = a_map.get(fmt["HA"]) - if a: - s_def["align"] = a - if fmt["VA"]: - va_map = {"Top": "top", "Center": "center"} - a = va_map.get(fmt["VA"]) - if a: - s_def["valign"] = a - bd = get_border_desc(fmt) - if bd["Border"] != "none": - s_def["border"] = bd["Border"] - if bd["Thick"]: - s_def["borderWidth"] = "thick" - if fmt["Wrap"]: - s_def["wrap"] = True - if fmt["DataFormat"]: - s_def["format"] = fmt["DataFormat"] + s_def.update(style_props(fmt)) style_defs[name] = s_def