From f926acd4f475e390705e163aa303f9e506e30219 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 11 Aug 2026 20:59:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(mxl-decompile):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B8=D0=B7=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B5=D1=81=D1=81=D0=BC=D1=8B=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20style=20=D0=B8=20=D0=BB=D0=B8=D1=88?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=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 Две правки читаемости DSL, обе замечены на живом выводе. Ячейка получала "style": "default" — ссылку на стиль, которого в styles нет вовсе. Условие сверялось с обнаруженным стилем пустых колонок, а он бывает равен "default": строка такой стиль не пишет, а ячейка получала ключ на пустое место. Теперь сверяемся со стилем, который РЕАЛЬНО раздаётся ячейкам. Позиционная запись строки отбрасывалась, как только первая ячейка стояла не в первой колонке. Но один-два null впереди обычно короче объектной записи с col: строка вида { "cells": [{ "col": 2, "text": "…" }] } сворачивается в [null, "…"]. Выбираем ту форму, которая короче. Правка не должна менять скомпилированный XML — и не меняет: пилот из 40 макетов собрался побайтово так же, как до неё, при том что JSON изменился у 31 макета. Стенд по-прежнему 7 из 10 байт в байт. Попутно блок JSON-сериализатора в ps1 перенесён выше первого использования: в PowerShell функция должна быть объявлена до вызова. Co-Authored-By: Claude Opus 5 (1M context) --- .../mxl-decompile/scripts/mxl-decompile.ps1 | 265 +++++++++--------- .../mxl-decompile/scripts/mxl-decompile.py | 29 +- 2 files changed, 155 insertions(+), 139 deletions(-) diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index cdfd6d3c..2b6974d7 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.14 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.15 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -645,11 +645,138 @@ function Test-PositionalList { # Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды null # и станет длиннее объектного. Ячейка, у которой кроме текста или параметра ничего нет, # пишется строкой; span раскрывается маркерами ">"; всё прочее — объектным элементом без col. +# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline. +# В отличие от ConvertTo-Json (PS5.1): +# - не выравнивает ключи объекта по самому длинному +# - не разворачивает массивы примитивов на отдельные строки +# - кириллица в UTF-8 (без \uXXXX-escapes) +function Convert-StringToJsonLiteral { + param([string]$s) + if ($null -eq $s) { return 'null' } + $sb = New-Object System.Text.StringBuilder + [void]$sb.Append('"') + foreach ($ch in $s.ToCharArray()) { + $code = [int]$ch + if ($code -eq 0x22) { [void]$sb.Append('\"') } + elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } + elseif ($code -eq 0x08) { [void]$sb.Append('\b') } + elseif ($code -eq 0x09) { [void]$sb.Append('\t') } + elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } + elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } + elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } + elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } + else { [void]$sb.Append($ch) } + } + [void]$sb.Append('"') + return $sb.ToString() +} + +# Попробовать сериализовать значение полностью inline (одна строка). +# Возвращает строку либо $null, если содержимое не помещается. +function Try-InlineJson { + param($obj) + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $obj.Keys) { + $v = Try-InlineJson $obj[$k] + if ($null -eq $v) { return $null } + $parts += "$(Convert-StringToJsonLiteral "$k"): $v" + } + return '{ ' + ($parts -join ', ') + ' }' + } + if ($obj -is [System.Management.Automation.PSCustomObject]) { + $props = @($obj.PSObject.Properties) + if ($props.Count -eq 0) { return '{}' } + $parts = @() + foreach ($p in $props) { + $v = Try-InlineJson $p.Value + if ($null -eq $v) { return $null } + $parts += "$(Convert-StringToJsonLiteral "$($p.Name)"): $v" + } + return '{ ' + ($parts -join ', ') + ' }' + } + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @() + foreach ($it in $items) { + $v = Try-InlineJson $it + if ($null -eq $v) { return $null } + $parts += $v + } + return '[' + ($parts -join ', ') + ']' + } + return $null +} + +function ConvertTo-CompactJson { + param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 400) + $indent = $indentUnit * $depth + $childIndent = $indentUnit * ($depth + 1) + + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + + # Try inline для объектов и массивов с объектами — если помещается в lineLimit с учётом текущего indent. + $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [System.Management.Automation.PSCustomObject]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) + if ($isContainer) { + $inlineAttempt = Try-InlineJson $obj + if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { + return $inlineAttempt + } + } + + # Hashtable / OrderedDictionary — объект multi-line + if ($obj -is [System.Collections.IDictionary]) { + $keys = @($obj.Keys) + if ($keys.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $keys) { + $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit + $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" + } + return "{`n" + ($parts -join ",`n") + "`n$indent}" + } + if ($obj -is [System.Management.Automation.PSCustomObject]) { + $props = @($obj.PSObject.Properties) + if ($props.Count -eq 0) { return '{}' } + $parts = @() + foreach ($p in $props) { + $val = ConvertTo-CompactJson -obj $p.Value -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit + $parts += "$childIndent$(Convert-StringToJsonLiteral "$($p.Name)"): $val" + } + return "{`n" + ($parts -join ",`n") + "`n$indent}" + } + # Array / IList multi-line + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) + return "[`n" + ($parts -join ",`n") + "`n$indent]" + } + # Fallback + return (Convert-StringToJsonLiteral "$obj") +} + function ConvertTo-PositionalCells { param($cells) + # Пропуск колонки — null. Выбираем ту запись, которая КОРОЧЕ: раньше позиционная форма + # отбрасывалась, как только первая ячейка стояла не в первой колонке, хотя один-два null + # впереди обычно короче объектной записи с `col`. if ($cells.Count -eq 0) { return @() } - $first = $cells[0] - if ([int]$first["col"] -ne 1) { return $cells } $out = @() $expected = 1 @@ -676,6 +803,10 @@ function ConvertTo-PositionalCells { } $expected = $col + $span } + # Позиционная форма ценна компактностью: если она длиннее объектной, смысла в ней нет. + $a = Try-InlineJson $out + $b = Try-InlineJson $cells + if ($null -ne $a -and $null -ne $b -and $a.Length -gt $b.Length) { return $cells } return $out } @@ -834,6 +965,7 @@ foreach ($area in $blocks) { $dslRow["rowStyle"] = [ordered]@{ style = $ownName; apply = "row" } $rowStyleName = $null $rowStyleKey = $null + $cellsName = $null } # Build cell list @@ -863,7 +995,7 @@ foreach ($area in $blocks) { # `-not $rowStyleName`, и такая ячейка теряла стиль вовсе — при обратной сборке # она наследовала rowStyle. $sn = Get-StyleName $cell.FormatIdx - if ($sn -ne "default" -or $rowStyleName) { + if ($sn -ne "default" -or $cellsName) { $dslCell["style"] = $sn } } @@ -1074,131 +1206,6 @@ if ($overlayAreas.Count -gt 0) { # --- 16. Convert to JSON --- -# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline. -# В отличие от ConvertTo-Json (PS5.1): -# - не выравнивает ключи объекта по самому длинному -# - не разворачивает массивы примитивов на отдельные строки -# - кириллица в UTF-8 (без \uXXXX-escapes) -function Convert-StringToJsonLiteral { - param([string]$s) - if ($null -eq $s) { return 'null' } - $sb = New-Object System.Text.StringBuilder - [void]$sb.Append('"') - foreach ($ch in $s.ToCharArray()) { - $code = [int]$ch - if ($code -eq 0x22) { [void]$sb.Append('\"') } - elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } - elseif ($code -eq 0x08) { [void]$sb.Append('\b') } - elseif ($code -eq 0x09) { [void]$sb.Append('\t') } - elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } - elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } - elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } - elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } - else { [void]$sb.Append($ch) } - } - [void]$sb.Append('"') - return $sb.ToString() -} - -# Попробовать сериализовать значение полностью inline (одна строка). -# Возвращает строку либо $null, если содержимое не помещается. -function Try-InlineJson { - param($obj) - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - if ($obj -is [System.Collections.IDictionary]) { - if ($obj.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $obj.Keys) { - $v = Try-InlineJson $obj[$k] - if ($null -eq $v) { return $null } - $parts += "$(Convert-StringToJsonLiteral "$k"): $v" - } - return '{ ' + ($parts -join ', ') + ' }' - } - if ($obj -is [System.Management.Automation.PSCustomObject]) { - $props = @($obj.PSObject.Properties) - if ($props.Count -eq 0) { return '{}' } - $parts = @() - foreach ($p in $props) { - $v = Try-InlineJson $p.Value - if ($null -eq $v) { return $null } - $parts += "$(Convert-StringToJsonLiteral "$($p.Name)"): $v" - } - return '{ ' + ($parts -join ', ') + ' }' - } - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @() - foreach ($it in $items) { - $v = Try-InlineJson $it - if ($null -eq $v) { return $null } - $parts += $v - } - return '[' + ($parts -join ', ') + ']' - } - return $null -} - -function ConvertTo-CompactJson { - param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 400) - $indent = $indentUnit * $depth - $childIndent = $indentUnit * ($depth + 1) - - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - - # Try inline для объектов и массивов с объектами — если помещается в lineLimit с учётом текущего indent. - $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [System.Management.Automation.PSCustomObject]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) - if ($isContainer) { - $inlineAttempt = Try-InlineJson $obj - if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { - return $inlineAttempt - } - } - - # Hashtable / OrderedDictionary — объект multi-line - if ($obj -is [System.Collections.IDictionary]) { - $keys = @($obj.Keys) - if ($keys.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $keys) { - $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit - $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" - } - return "{`n" + ($parts -join ",`n") + "`n$indent}" - } - if ($obj -is [System.Management.Automation.PSCustomObject]) { - $props = @($obj.PSObject.Properties) - if ($props.Count -eq 0) { return '{}' } - $parts = @() - foreach ($p in $props) { - $val = ConvertTo-CompactJson -obj $p.Value -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit - $parts += "$childIndent$(Convert-StringToJsonLiteral "$($p.Name)"): $val" - } - return "{`n" + ($parts -join ",`n") + "`n$indent}" - } - # Array / IList multi-line - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) - return "[`n" + ($parts -join ",`n") + "`n$indent]" - } - # Fallback - return (Convert-StringToJsonLiteral "$obj") -} $json = ConvertTo-CompactJson $result diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 2e61accb..4b36ac69 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.14 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.15 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -791,10 +791,12 @@ def main(): def to_positional_cells(cells): """Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется. - Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды None - и станет длиннее объектного. Ячейка, у которой кроме текста или параметра ничего нет, - пишется строкой; span раскрывается маркерами ">"; прочее — объектным элементом без col.""" - if not cells or int(cells[0].get("col", 0)) != 1: + Ячейка, у которой кроме текста или параметра ничего нет, пишется строкой; span + раскрывается маркерами ">"; прочее — объектным элементом без col. Пропуск колонки — + null. Выбираем ту запись, которая КОРОЧЕ: раньше позиционная форма отбрасывалась, + как только первая ячейка стояла не в первой колонке, хотя один-два null впереди + обычно короче объектной записи с `col`.""" + if not cells: return cells out = [] expected = 1 @@ -820,6 +822,10 @@ def main(): if (plain_text or plain_param) and span > 1: out.extend([">"] * (span - 1)) expected = col + span + # Позиционная форма ценна компактностью: если она длиннее объектной, смысла в ней нет. + a, b = try_inline_json(out), try_inline_json(cells) + if a is not None and b is not None and len(a) > len(b): + return cells return out # --- 12. Build areas --- @@ -959,6 +965,7 @@ def main(): dsl_row["rowStyle"] = OrderedDict([("style", own_name), ("apply", "row")]) row_style_name = None row_style_key = None + cells_name = None # Build cell list dsl_cells = [] @@ -984,12 +991,14 @@ def main(): pass # Inherits rowStyle else: # Стиль пишем, только когда он отличается от того, что подставит компилятор: - # без rowStyle умолчание и есть "default", поэтому такой ключ — шум (56% ячеек). - # А при заданном rowStyle стиль "default" писать ОБЯЗАТЕЛЬНО: раньше здесь было - # `not row_style_name`, и такая ячейка теряла стиль вовсе — при обратной сборке - # она наследовала rowStyle. + # без стиля у ячеек умолчание и есть "default", поэтому такой ключ — шум + # (56% ячеек). А когда стиль ячейкам раздаётся, "default" писать ОБЯЗАТЕЛЬНО: + # иначе ячейка при обратной сборке унаследовала бы его. + # Сверяемся с cells_name — стилем, который РЕАЛЬНО раздаётся ячейкам. Раньше + # здесь стоял row_style_name, а он бывает равен "default": строка такой стиль + # не пишет, а ячейка получала бессмысленный ключ на несуществующий стиль. sn = get_style_name(cell["FormatIdx"]) - if sn != "default" or row_style_name: + if sn != "default" or cells_name: dsl_cell["style"] = sn # Content