diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 4b693183..c82eed49 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.28 — Compile 1C spreadsheet from JSON +# mxl-compile v1.29 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -268,33 +268,29 @@ if (-not $hasDefault) { Add-Font -name "default" -fontDef $defaultDef } -# --- 3. Determine line palette --- +# --- 3. Line palette --- +# Рамка хранится не в формате, а в палитре : формат ссылается на запись индексом. +# Запись — тройка (стиль, ширина, gap); в корпусе gap всегда false, но тег платформа пишет. +$script:lineRegistry = @() -$hasThinBorders = $false -$hasThickBorders = $false +function Get-LineKey { + param($ln) + return "$($ln.Style)|$($ln.Width)|$($ln.Gap)" +} -# Scan styles for border usage -if ($def.styles) { - foreach ($prop in $def.styles.PSObject.Properties) { - $s = $prop.Value - if ($s.border -and $s.border -ne "none") { - if ($s.borderWidth -eq "thick") { - $hasThickBorders = $true - } else { - $hasThinBorders = $true - } - } +function Register-Line { + param($ln) + $key = Get-LineKey $ln + for ($i = 0; $i -lt $script:lineRegistry.Count; $i++) { + if ((Get-LineKey $script:lineRegistry[$i]) -ceq $key) { return $i } } + $script:lineRegistry += $ln + return $script:lineRegistry.Count - 1 } -$thinLineIndex = -1 -$thickLineIndex = -1 -$lineCount = 0 -if ($hasThinBorders) { - $thinLineIndex = $lineCount; $lineCount++ -} -if ($hasThickBorders) { - $thickLineIndex = $lineCount; $lineCount++ +function Get-LineStyles { + # Стили линии, встречающиеся в палитрах корпуса. + return @('Solid', 'None', 'Dotted', 'ThinDashed', 'LargeDashed', 'ThickDashed', 'Double') } # --- 4. Parse column width specs --- @@ -427,71 +423,131 @@ if ($def.columnSets) { # --- 5. Style resolver --- +# Значение рамки: строка стиля ("Dotted") либо объект { style, width, gap }. +# Прежняя запись borderWidth: thin/thick — это ширина 1 и 2. +function ConvertTo-LineValue { + param($val, [string]$where) + $style = 'Solid'; $width = 1; $gap = 'false' + if ($val -is [string] -or $val -is [int]) { + $style = "$val" + } else { + if ($null -ne $val.style) { $style = "$($val.style)" } + if ($null -ne $val.width) { $width = "$($val.width)" } + if ($null -ne $val.gap) { $gap = if ($val.gap -eq $true) { 'true' } else { 'false' } } + } + if ($style -ceq 'thin') { $style = 'Solid'; $width = 1 } + elseif ($style -ceq 'thick') { $style = 'Solid'; $width = 2 } + $canon = Get-LineStyles | Where-Object { $_ -eq $style } | Select-Object -First 1 + if (-not $canon) { + [Console]::Error.WriteLine("Unknown border style `"$style`" ($where). Allowed: $((Get-LineStyles) -join ', ')") + exit 1 + } + return @{ Style = $canon; Width = $width; Gap = $gap } +} + +# Цвет — значение с префиксом пространства имён (нотация платформы). style: объявлен в корне +# документа, web/win — нет, поэтому платформа дописывает объявление прямо на узел. +function Get-ColorNamespace { + param([string]$val) + if ($val -like 'web:*') { return 'http://v8.1c.ru/8.1/data/ui/colors/web' } + if ($val -like 'win:*') { return 'http://v8.1c.ru/8.1/data/ui/colors/windows' } + return '' +} + function Resolve-Style { param([string]$styleName, [string]$fillType) - $fontIdx = $fontMap["default"] - $lb = -1; $tb = -1; $rb = -1; $bb = -1 - $ha = ""; $va = ""; $nf = "" - $wrap = $false + # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки + # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. + $props = @{ font = $fontMap["default"] } if ($styleName -and $def.styles) { $style = $def.styles.$styleName if ($style) { - # Font - if ($style.font -and $fontMap.Contains($style.font)) { - $fontIdx = $fontMap[$style.font] - } + $kinds = Get-FormatTagKind + $enums = Get-FormatEnumValues + $synonyms = Get-StyleKeySynonyms + $where = "style `"$styleName`"" - # Borders - if ($style.border -and $style.border -ne "none") { - $lineIdx = if ($style.borderWidth -eq "thick") { $thickLineIndex } else { $thinLineIndex } - foreach ($side in ($style.border -split ',')) { - switch ($side.Trim()) { - "all" { $lb = $lineIdx; $tb = $lineIdx; $rb = $lineIdx; $bb = $lineIdx } - "left" { $lb = $lineIdx } - "top" { $tb = $lineIdx } - "right" { $rb = $lineIdx } - "bottom" { $bb = $lineIdx } + # Прежняя запись рамки: стороны строкой + borderWidth. Разворачиваем в посторонние + # ключи ДО общего разбора, чтобы дальше был один путь. + $sideKeys = @{ left = 'leftBorder'; top = 'topBorder'; right = 'rightBorder'; bottom = 'bottomBorder' } + $legacyBorder = $style.border + if ($legacyBorder -is [string] -and $legacyBorder -and (Get-LineStyles | Where-Object { $_ -eq $legacyBorder }).Count -eq 0) { + $ln = ConvertTo-LineValue @{ style = 'Solid'; width = $(if ("$($style.borderWidth)" -ceq 'thick') { 2 } else { 1 }) } $where + $idx = Register-Line $ln + foreach ($side in ($legacyBorder -split ',')) { + $s = $side.Trim().ToLower() + if ($s -eq 'none') { continue } + if ($s -eq 'all') { + foreach ($k in $sideKeys.Values) { $props[$k] = $idx } + } elseif ($sideKeys.ContainsKey($s)) { + $props[$sideKeys[$s]] = $idx + } else { + [Console]::Error.WriteLine("Unknown border side `"$($side.Trim())`" ($where). Allowed: all, left, top, right, bottom, none") + exit 1 } } } - # Alignment - if ($style.align) { - switch ($style.align) { - "left" { $ha = "Left" } - "center" { $ha = "Center" } - "right" { $ha = "Right" } + foreach ($p in $style.PSObject.Properties) { + $raw = $p.Name + # Синонимы: канон побеждает, сравнение без регистра и пробелов. + $norm = ($raw -replace '\s', '').ToLower() + $tag = if ($synonyms.ContainsKey($norm)) { $synonyms[$norm] } else { $raw } + if ($tag -ceq 'borderWidth') { continue } + if ($tag -ceq 'border' -and $legacyBorder -is [string] -and + (Get-LineStyles | Where-Object { $_ -eq $legacyBorder }).Count -eq 0) { continue } + + $val = $p.Value + if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } + + if ($tag -ceq 'font') { + if ($fontMap.Contains("$val")) { $props['font'] = $fontMap["$val"] } + continue + } + # wrap — не имя, а сокращение: булево вместо перечисления textPlacement. + if ($tag -ceq 'wrap') { + if ($val -eq $true -or "$val" -eq 'true') { $props['textPlacement'] = 'Wrap' } + continue + } + if (-not $kinds.ContainsKey($tag)) { + Write-Warning "Unknown style key '$raw' ($where) — ignored." + continue + } + switch ($kinds[$tag]) { + 'line' { $props[$tag] = Register-Line (ConvertTo-LineValue $val $where) } + 'bool' { $props[$tag] = if ($val -eq $true -or "$val" -eq 'true') { 'true' } else { 'false' } } + 'int' { $props[$tag] = [int]$val } + 'enum' { + $allowed = $enums[$tag] + $canon = $allowed | Where-Object { $_ -eq "$val" } | Select-Object -First 1 + if (-not $canon) { + [Console]::Error.WriteLine("Unknown '$tag' value `"$val`" ($where). Allowed: $($allowed -join ', ')") + exit 1 + } + $props[$tag] = $canon + } + default { $props[$tag] = "$val" } } } - if ($style.valign) { - switch ($style.valign) { - "top" { $va = "Top" } - "center" { $va = "Center" } - } - } - - # Wrap - if ($style.wrap -eq $true) { $wrap = $true } - - # Number format - if ($style.format) { $nf = $style.format } } } - # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки - # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. - $props = @{ font = $fontIdx } - if ($lb -ge 0) { $props['leftBorder'] = $lb } - if ($tb -ge 0) { $props['topBorder'] = $tb } - if ($rb -ge 0) { $props['rightBorder'] = $rb } - if ($bb -ge 0) { $props['bottomBorder'] = $bb } - if ($ha) { $props['horizontalAlignment'] = $ha } - if ($va) { $props['verticalAlignment'] = $va } - if ($wrap) { $props['textPlacement'] = 'Wrap' } if ($fillType) { $props['fillType'] = $fillType } - if ($nf) { $props['format'] = $nf } + + # Одинаковые четыре стороны платформа пишет одним . Правило проверено на корпусе: + # 70 265 свёрнутых форматов, 36 783 записанных по сторонам — и среди вторых нет ни одного + # с четырьмя совпадающими значениями. + $sides = @('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder') + $present = @($sides | Where-Object { $props.ContainsKey($_) }) + if ($present.Count -eq 4) { + $vals = @($sides | ForEach-Object { $props[$_] } | Select-Object -Unique) + if ($vals.Count -eq 1) { + foreach ($s in $sides) { $props.Remove($s) } + $props['border'] = $vals[0] + } + } return $props } @@ -524,8 +580,74 @@ function Get-FormatMlTags { return @{ 'format' = $true; 'editFormat' = $true; 'mask' = $true } } +# Тип значения каждого тега — выведен из корпуса, а не выписан на глаз. +# line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: +# ml — многоязычная строка; enum — замкнутый список (см. Get-FormatEnumValues) +# 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-FormatEnumValues { + return @{ + 'columnSizeChange' = @('Normal', 'QuickChange') + 'detailsUse' = @('Cell', 'Row', 'WithoutProcessing') + 'fillType' = @('Parameter', 'Template', 'Text') + 'horizontalAlignment' = @('Auto', 'Center', 'Justify', 'Left', 'Right') + 'pattern' = @('Pattern7', 'Pattern10', 'Pattern12', 'Pattern13', 'Pattern14', 'Pattern16', 'Solid', 'WithoutPattern') + 'picHorizontalAlignment' = @('Auto', 'Center', 'Left', 'Right') + 'picVerticalAlignment' = @('Bottom', 'Center', 'Top') + 'pictureSizeMode' = @('AutoSize', 'Proportionally', 'RealSize') + 'textPlacement' = @('Auto', 'Block', 'Cut', 'Wrap') + 'textPosition' = @('Auto', 'Bottom', 'Right', 'Top') + 'verticalAlignment' = @('Bottom', 'Center', 'Top') + } +} + +# Прощающий ввод: ключ стиля, написанный иначе, чем тег платформы. Канон побеждает — +# если заданы оба, синоним отбрасывается. Ключи карты нормализованы (lower, без пробелов). +# Инвертированных синонимов (visible для hidden) НЕ заводим — это баг семантики, не удобство. +function Get-StyleKeySynonyms { + return @{ + 'align' = 'horizontalAlignment'; 'textalign' = 'horizontalAlignment' + 'halign' = 'horizontalAlignment'; 'горизонтальноеположение' = 'horizontalAlignment' + 'valign' = 'verticalAlignment'; 'verticalalign' = 'verticalAlignment' + 'вертикальноеположение' = 'verticalAlignment' + 'background' = 'backColor'; 'bgcolor' = 'backColor'; 'цветфона' = 'backColor' + 'color' = 'textColor'; 'forecolor' = 'textColor'; 'цветтекста' = 'textColor' + 'цветрамки' = 'borderColor'; 'цветузора' = 'patternColor' + 'borderleft' = 'leftBorder'; 'border-left' = 'leftBorder' + 'bordertop' = 'topBorder'; 'border-top' = 'topBorder' + 'borderright' = 'rightBorder'; 'border-right' = 'rightBorder' + 'borderbottom' = 'bottomBorder'; 'border-bottom' = 'bottomBorder' + 'protected' = 'protection'; 'защита' = 'protection' + 'отступ' = 'indent'; 'узор' = 'pattern'; 'гиперссылка' = 'hyperLink' + 'ориентациятекста' = 'textOrientation'; 'размещениетекста' = 'textPlacement' + 'переноспословам' = 'wrap' + } +} + $script:formatTagOrder = Get-FormatTagOrder $script:formatMlTags = Get-FormatMlTags +$script:formatTagKind = Get-FormatTagKind function Get-FormatKey { param([hashtable]$props) @@ -1315,14 +1437,9 @@ foreach ($ni in $sortedNamedItems) { } # 7h. Line palette -if ($hasThinBorders) { - X "`t" - X "`t`tSolid" - X "`t" -} -if ($hasThickBorders) { - X "`t" - X "`t`tSolid" +foreach ($ln in $script:lineRegistry) { + X "`t" + X "`t`t$($ln.Style)" X "`t" } @@ -1346,6 +1463,12 @@ foreach ($key in $formatRegistry.Keys) { X "`t`t`t`t$(Esc-XmlText $val)" X "`t`t`t" X "`t`t" + } elseif ($script:formatTagKind[$tag] -ceq 'color' -and (Get-ColorNamespace "$val")) { + # web/win-палитры в корне документа не объявлены — платформа дописывает объявление + # прямо на узел и пишет значение с этим префиксом. + $ns = Get-ColorNamespace "$val" + $name = "$val".Substring("$val".IndexOf(':') + 1) + X "`t`t<$tag xmlns:d3p1=`"$ns`">d3p1:$name" } else { X "`t`t<$tag>$val" } @@ -1376,5 +1499,5 @@ if ($def.page) { Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth" } Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns" -Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)" +Write-Host " Fonts: $($fontEntries.Count), Lines: $($script:lineRegistry.Count), Formats: $($formatRegistry.Count)" Write-Host " Merges: $($merges.Count)" diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 212f94d3..eaf6959f 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.28 — Compile 1C spreadsheet from JSON +# mxl-compile v1.29 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -350,6 +350,87 @@ def format_ml_tags(): return {'format': True, 'editFormat': True, 'mask': True} +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_enum_values(): + """Допустимые значения перечислений — тоже сняты с корпуса.""" + return { + 'columnSizeChange': ['Normal', 'QuickChange'], + 'detailsUse': ['Cell', 'Row', 'WithoutProcessing'], + 'fillType': ['Parameter', 'Template', 'Text'], + 'horizontalAlignment': ['Auto', 'Center', 'Justify', 'Left', 'Right'], + 'pattern': ['Pattern7', 'Pattern10', 'Pattern12', 'Pattern13', 'Pattern14', + 'Pattern16', 'Solid', 'WithoutPattern'], + 'picHorizontalAlignment': ['Auto', 'Center', 'Left', 'Right'], + 'picVerticalAlignment': ['Bottom', 'Center', 'Top'], + 'pictureSizeMode': ['AutoSize', 'Proportionally', 'RealSize'], + 'textPlacement': ['Auto', 'Block', 'Cut', 'Wrap'], + 'textPosition': ['Auto', 'Bottom', 'Right', 'Top'], + 'verticalAlignment': ['Bottom', 'Center', 'Top'], + } + + +def style_key_synonyms(): + """Прощающий ввод: ключ стиля, написанный иначе, чем тег платформы. Канон побеждает — + если заданы оба, синоним отбрасывается. Ключи карты нормализованы (lower, без пробелов). + Инвертированных синонимов (visible для hidden) НЕ заводим — это баг семантики, не удобство.""" + return { + 'align': 'horizontalAlignment', 'textalign': 'horizontalAlignment', + 'halign': 'horizontalAlignment', 'горизонтальноеположение': 'horizontalAlignment', + 'valign': 'verticalAlignment', 'verticalalign': 'verticalAlignment', + 'вертикальноеположение': 'verticalAlignment', + 'background': 'backColor', 'bgcolor': 'backColor', 'цветфона': 'backColor', + 'color': 'textColor', 'forecolor': 'textColor', 'цветтекста': 'textColor', + 'цветрамки': 'borderColor', 'цветузора': 'patternColor', + 'borderleft': 'leftBorder', 'border-left': 'leftBorder', + 'bordertop': 'topBorder', 'border-top': 'topBorder', + 'borderright': 'rightBorder', 'border-right': 'rightBorder', + 'borderbottom': 'bottomBorder', 'border-bottom': 'bottomBorder', + 'protected': 'protection', 'защита': 'protection', + 'отступ': 'indent', 'узор': 'pattern', 'гиперссылка': 'hyperLink', + 'ориентациятекста': 'textOrientation', 'размещениетекста': 'textPlacement', + 'переноспословам': 'wrap', + } + + +def line_styles(): + """Стили линии, встречающиеся в палитрах корпуса.""" + return ['Solid', 'None', 'Dotted', 'ThinDashed', 'LargeDashed', 'ThickDashed', 'Double'] + + +def color_namespace(val): + """Цвет — значение с префиксом пространства имён (нотация платформы). style: объявлен + в корне документа, web/win — нет, поэтому платформа дописывает объявление прямо на узел.""" + if val.startswith('web:'): + return 'http://v8.1c.ru/8.1/data/ui/colors/web' + if val.startswith('win:'): + return 'http://v8.1c.ru/8.1/data/ui/colors/windows' + return '' + + def get_format_key(props): parts = [] for tag in format_tag_order(): @@ -430,27 +511,18 @@ def main(): if not has_default: add_font('default', {'face': 'Arial', 'size': 10}) - # --- 3. Determine line palette --- - has_thin_borders = False - has_thick_borders = False + # --- 3. Line palette --- + # Рамка хранится не в формате, а в палитре : формат ссылается на запись индексом. + # Запись — тройка (стиль, ширина, gap); в корпусе gap всегда false, но тег платформа пишет. + line_registry = [] - if defn.get('styles'): - for sname, sval in defn['styles'].items(): - if sval.get('border') and sval['border'] != 'none': - if sval.get('borderWidth') == 'thick': - has_thick_borders = True - else: - has_thin_borders = True - - thin_line_index = -1 - thick_line_index = -1 - line_count = 0 - if has_thin_borders: - thin_line_index = line_count - line_count += 1 - if has_thick_borders: - thick_line_index = line_count - line_count += 1 + def register_line(ln): + key = (ln['Style'], str(ln['Width']), ln['Gap']) + for i, existing in enumerate(line_registry): + if (existing['Style'], str(existing['Width']), existing['Gap']) == key: + return i + line_registry.append(ln) + return len(line_registry) - 1 # --- 4. Parse column width specs --- def parse_column_spec(spec): @@ -555,72 +627,121 @@ def main(): }) # --- 5. Style resolver --- + def to_line_value(val, where): + """Значение рамки: строка стиля ("Dotted") либо объект { style, width, gap }. + Прежняя запись borderWidth: thin/thick — это ширина 1 и 2.""" + style, width, gap = 'Solid', 1, 'false' + if isinstance(val, dict): + if val.get('style') is not None: + style = str(val['style']) + if val.get('width') is not None: + width = val['width'] + if val.get('gap') is not None: + gap = 'true' if val['gap'] is True else 'false' + else: + style = str(val) + if style == 'thin': + style, width = 'Solid', 1 + elif style == 'thick': + style, width = 'Solid', 2 + canon = next((s for s in line_styles() if s.lower() == style.lower()), None) + if not canon: + print(f'Unknown border style "{style}" ({where}). Allowed: {", ".join(line_styles())}', + file=sys.stderr) + sys.exit(1) + return {'Style': canon, 'Width': width, 'Gap': gap} + def resolve_style(style_name, fill_type): - font_idx = font_map.get('default', 0) - lb = -1; tb = -1; rb = -1; bb = -1 - ha = ''; va = ''; nf = '' - wrap = False + # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки + # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. + props = {'font': font_map.get('default', 0)} if style_name and defn.get('styles'): style = defn['styles'].get(style_name) if style: - # Font - if style.get('font') and style['font'] in font_map: - font_idx = font_map[style['font']] + kinds = format_tag_kind() + enums = format_enum_values() + synonyms = style_key_synonyms() + where = f'style "{style_name}"' + side_keys = {'left': 'leftBorder', 'top': 'topBorder', + 'right': 'rightBorder', 'bottom': 'bottomBorder'} - # Borders - if style.get('border') and style['border'] != 'none': - line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index - for side in style['border'].split(','): - side = side.strip() - if side == 'all': - lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx - elif side == 'left': - lb = line_idx - elif side == 'top': - tb = line_idx - elif side == 'right': - rb = line_idx - elif side == 'bottom': - bb = line_idx + # Прежняя запись рамки: стороны строкой + borderWidth. Разворачиваем в + # посторонние ключи ДО общего разбора, чтобы дальше был один путь. + legacy = style.get('border') + legacy_sides = (isinstance(legacy, str) and legacy + and not any(s.lower() == legacy.lower() for s in line_styles())) + if legacy_sides: + idx = register_line(to_line_value( + {'style': 'Solid', 'width': 2 if str(style.get('borderWidth')) == 'thick' else 1}, + where)) + for side in legacy.split(','): + s = side.strip().lower() + if s == 'none': + continue + if s == 'all': + for k in side_keys.values(): + props[k] = idx + elif s in side_keys: + props[side_keys[s]] = idx + else: + print(f'Unknown border side "{side.strip()}" ({where}).' + f' Allowed: all, left, top, right, bottom, none', file=sys.stderr) + sys.exit(1) - # Alignment - if style.get('align'): - align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'} - ha = align_map.get(style['align'], '') - if style.get('valign'): - valign_map = {'top': 'Top', 'center': 'Center'} - va = valign_map.get(style['valign'], '') + for raw, val in style.items(): + # Синонимы: канон побеждает, сравнение без регистра и пробелов. + norm = re.sub(r'\s', '', raw).lower() + tag = synonyms.get(norm, raw) + if tag == 'borderWidth': + continue + if tag == 'border' and legacy_sides: + continue + if val is None or (isinstance(val, str) and val == ''): + continue - # Wrap - if style.get('wrap') is True: - wrap = True + if tag == 'font': + if str(val) in font_map: + props['font'] = font_map[str(val)] + continue + # wrap — не имя, а сокращение: булево вместо перечисления textPlacement. + if tag == 'wrap': + if val is True or str(val) == 'true': + props['textPlacement'] = 'Wrap' + continue + if tag not in kinds: + print(f"Warning: unknown style key '{raw}' ({where}) — ignored.", file=sys.stderr) + continue + kind = kinds[tag] + if kind == 'line': + props[tag] = register_line(to_line_value(val, where)) + elif kind == 'bool': + props[tag] = 'true' if (val is True or str(val) == 'true') else 'false' + elif kind == 'int': + props[tag] = int(val) + elif kind == 'enum': + allowed = enums[tag] + canon = next((a for a in allowed if a.lower() == str(val).lower()), None) + if not canon: + print(f'Unknown \'{tag}\' value "{val}" ({where}).' + f' Allowed: {", ".join(allowed)}', file=sys.stderr) + sys.exit(1) + props[tag] = canon + else: + props[tag] = str(val) - # Number format - if style.get('format'): - nf = style['format'] - - # Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки - # роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов. - props = {'font': font_idx} - if lb >= 0: - props['leftBorder'] = lb - if tb >= 0: - props['topBorder'] = tb - if rb >= 0: - props['rightBorder'] = rb - if bb >= 0: - props['bottomBorder'] = bb - if ha: - props['horizontalAlignment'] = ha - if va: - props['verticalAlignment'] = va - if wrap: - props['textPlacement'] = 'Wrap' if fill_type: props['fillType'] = fill_type - if nf: - props['format'] = nf + + # Одинаковые четыре стороны платформа пишет одним . Правило проверено на корпусе: + # 70 265 свёрнутых форматов, 36 783 записанных по сторонам — и среди вторых нет ни одного + # с четырьмя совпадающими значениями. + sides = ['leftBorder', 'topBorder', 'rightBorder', 'bottomBorder'] + if all(s in props for s in sides) and len({props[s] for s in sides}) == 1: + val = props[sides[0]] + for s in sides: + del props[s] + props['border'] = val return props # --- 6. Format palette builder --- @@ -1294,13 +1415,9 @@ def main(): lines.append('\t') # 7h. Line palette - if has_thin_borders: - lines.append('\t') - lines.append('\t\tSolid') - lines.append('\t') - if has_thick_borders: - lines.append('\t') - lines.append('\t\tSolid') + for ln in line_registry: + lines.append(f'\t') + lines.append(f'\t\t{ln["Style"]}') lines.append('\t') # 7i. Font palette @@ -1313,6 +1430,7 @@ def main(): lines.append('\t') ml_tags = format_ml_tags() + kinds = format_tag_kind() for tag in format_tag_order(): if tag not in fmt: continue @@ -1324,6 +1442,12 @@ def main(): lines.append(f'\t\t\t\t{esc_xml_text(val)}') lines.append('\t\t\t') lines.append(f'\t\t') + elif kinds.get(tag) == 'color' and color_namespace(str(val)): + # web/win-палитры в корне документа не объявлены — платформа дописывает + # объявление прямо на узел и пишет значение с этим префиксом. + ns = color_namespace(str(val)) + name = str(val)[str(val).index(':') + 1:] + lines.append(f'\t\t<{tag} xmlns:d3p1="{ns}">d3p1:{name}') else: lines.append(f'\t\t<{tag}>{val}') @@ -1351,7 +1475,7 @@ def main(): if defn.get('page'): print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}") print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}") - print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}") + print(f" Fonts: {len(font_entries)}, Lines: {len(line_registry)}, Formats: {len(format_registry)}") print(f" Merges: {len(merges)}") 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 845bd39d..48797436 100644 --- a/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/column-widths/Template.xml @@ -151,9 +151,6 @@ 0 - 0 - 0 - 0 - 0 + 0 \ No newline at end of file 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 2da85557..62fa5f7f 100644 --- a/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/empty-rows/Template.xml @@ -101,17 +101,11 @@ 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Parameter \ No newline at end of file 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 008edd6b..84d01015 100644 --- a/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/format-strings/Template.xml @@ -123,25 +123,16 @@ 0 - 0 - 0 - 0 - 0 + 0 Center 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Center Parameter @@ -153,18 +144,12 @@ 0 - 0 - 0 - 0 - 0 + 0 Parameter 0 - 0 - 0 - 0 - 0 + 0 Right Parameter 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 350e36d1..e73e88fd 100644 --- a/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/merged-cells/Template.xml @@ -159,42 +159,27 @@ 0 - 0 - 0 - 0 - 0 + 0 1 - 0 - 0 - 0 - 0 + 0 Center 0 - 0 - 0 - 0 - 0 + 0 Center 0 - 0 - 0 - 0 - 0 + 0 Center Parameter 0 - 0 - 0 - 0 - 0 + 0 Parameter \ No newline at end of file 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 f420b0b1..825bf5c1 100644 --- a/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/multiple-areas/Template.xml @@ -194,25 +194,16 @@ 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Parameter 0 - 0 - 0 - 0 - 0 + 0 Right Parameter 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 342e4ac8..4f2e2485 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 @@ -174,34 +174,22 @@ 1 - 0 - 0 - 0 - 0 + 0 Center 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Center Parameter 0 - 0 - 0 - 0 - 0 + 0 Parameter \ No newline at end of file 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 bcc193b4..56fa7e27 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 @@ -140,17 +140,11 @@ 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Parameter \ No newline at end of file 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 e5e92593..21ffffe2 100644 --- a/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/print-form/Template.xml @@ -548,42 +548,27 @@ 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Center 0 - 0 - 0 - 0 - 0 + 0 Center Parameter 0 - 0 - 0 - 0 - 0 + 0 Parameter 0 - 0 - 0 - 0 - 0 + 0 Right Parameter diff --git a/tests/skills/cases/mxl-compile/snapshots/shorthand-rows/Template.xml b/tests/skills/cases/mxl-compile/snapshots/shorthand-rows/Template.xml index 1d4a7e4f..eb891bd0 100644 --- a/tests/skills/cases/mxl-compile/snapshots/shorthand-rows/Template.xml +++ b/tests/skills/cases/mxl-compile/snapshots/shorthand-rows/Template.xml @@ -117,10 +117,7 @@ 0 - 0 - 0 - 0 - 0 + 0 Center \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/style-full-properties/Template.xml b/tests/skills/cases/mxl-compile/snapshots/style-full-properties/Template.xml new file mode 100644 index 00000000..9954db26 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/style-full-properties/Template.xml @@ -0,0 +1,153 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 2 + + + ru + Отчёт о движении + + + + + + + + 1 + + + + 3 + + + ru + Слева пунктир + + + + + + + 4 + + + ru + Цвет из палитры + + + + + + + 5 + Скрытое + + + + + 6 + + + ru + Рамка свёрнута + + + + + + + true + 1 + 2 + 2 + + 0 + 0 + 3 + + + Тело + + Rows + 1 + 1 + -1 + -1 + + + + Шапка + + Rows + 0 + 0 + -1 + -1 + + + + Dotted + + + Solid + + + ThinDashed + + + Solid + + + + + 20 + + + 1 + Center + Center + style:FormTextColor + #EBEBEB + Wrap + true + + + 0 + 0 + 1 + 2 + #C8C8C8 + + + 0 + d3p1:ButtonText + d3p1:Gainsboro + + + 0 + Parameter + true + 900 + 2 + + + 0 + 3 + + \ 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 84a9ecba..922602fb 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 @@ -302,42 +302,27 @@ 1 - 1 - 1 - 1 - 1 + 1 0 - 0 - 0 - 0 - 0 + 0 0 - 0 - 0 - 0 - 0 + 0 Center Parameter 0 - 0 - 0 - 0 - 0 + 0 Wrap Parameter 0 - 0 - 0 - 0 - 0 + 0 Right Parameter diff --git a/tests/skills/cases/mxl-compile/style-full-properties.json b/tests/skills/cases/mxl-compile/style-full-properties.json new file mode 100644 index 00000000..8741fc59 --- /dev/null +++ b/tests/skills/cases/mxl-compile/style-full-properties.json @@ -0,0 +1,67 @@ +{ + "name": "Стиль: свойства формата платформенными именами, цвета, посторонние рамки", + "input": { + "columns": 4, + "defaultWidth": 20, + "fonts": { + "default": { "face": "Arial", "size": 10 }, + "bold": { "face": "Arial", "size": 10, "bold": true } + }, + "styles": { + "шапка": { + "font": "bold", + "backColor": "#EBEBEB", + "textColor": "style:FormTextColor", + "horizontalAlignment": "Center", + "verticalAlignment": "Center", + "textPlacement": "Wrap", + "protection": true + }, + "рамка-по-сторонам": { + "leftBorder": "Dotted", + "topBorder": { "style": "Solid", "width": 2 }, + "bottomBorder": { "style": "ThinDashed" }, + "borderColor": "#C8C8C8" + }, + "web-цвет": { + "backColor": "web:Gainsboro", + "textColor": "win:ButtonText" + }, + "скрытая": { + "hidden": true, + "indent": 2, + "textOrientation": 900 + }, + "рамка-вокруг": { + "leftBorder": "Solid", + "topBorder": "Solid", + "rightBorder": "Solid", + "bottomBorder": "Solid" + } + }, + "areas": [ + { + "name": "Шапка", + "rows": [ + { "cells": [ + { "col": 1, "span": 4, "style": "шапка", "text": "Отчёт о движении" } + ]} + ] + }, + { + "name": "Тело", + "rows": [ + { "cells": [ + { "col": 1, "style": "рамка-по-сторонам", "text": "Слева пунктир" }, + { "col": 2, "style": "web-цвет", "text": "Цвет из палитры" }, + { "col": 3, "style": "скрытая", "param": "Скрытое" }, + { "col": 4, "style": "рамка-вокруг", "text": "Рамка свёрнута" } + ]} + ] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "validatePath": "Template.xml", + "expect": { "files": ["Template.xml"] } +}