feat(mxl-compile): стиль умеет все свойства формата, а не семь

Стиль описывал 7 свойств из 47, которые платформа хранит в <format>. Не было
самого частого тега корпуса — backColor (1.7 млн вхождений), а также textColor,
textPlacement, protection, hidden, indent, borderColor, textOrientation и хвоста.

Теперь ключ стиля — имя тега платформы, без исключений: помнить, какие ключи
названы по-своему, больше не нужно. Прежние align/valign/wrap продолжают
работать молча, как синонимы; там же CSS-имена (background, color,
border-bottom) и русские имена свойств. Таблицы типов значений и допустимых
значений перечислений сняты с корпуса, а не выписаны на глаз.

Рамка: пять плоских ключей (border и четыре стороны) со значением
{ style, width } либо строкой стиля; линия регистрируется в палитре <line>,
которая раньше знала только «тонкую» и «толстую» Solid. Четыре одинаковые
стороны сворачиваются в один <border> — правило проверено на корпусе:
70 265 свёрнутых форматов против 36 783 записанных по сторонам, и среди
вторых нет ни одного с четырьмя совпадающими значениями.

Цвет — нотация самой платформы: #RRGGBB, style:Имя, web:Имя, win:Имя. Первые
две пишутся дословно (префикс style объявлен в корне документа), для web и win
платформа дописывает объявление xmlns прямо на узел — делаем так же.

containsValue / valueType / controlType намеренно не заведены: это свойства
конкретной ячейки, а стиль — сущность общая, один на многие ячейки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 15:34:54 +03:00
co-authored by Claude Opus 5
parent c559696c5a
commit d3c16f56c5
14 changed files with 663 additions and 295 deletions
@@ -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 ---
# Рамка хранится не в формате, а в палитре <line>: формат ссылается на запись индексом.
# Запись — тройка (стиль, ширина, 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 }
# Одинаковые четыре стороны платформа пишет одним <border>. Правило проверено на корпусе:
# 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 — ссылка в палитру <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<line width=`"1`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "`t</line>"
}
if ($hasThickBorders) {
X "`t<line width=`"2`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
foreach ($ln in $script:lineRegistry) {
X "`t<line width=`"$($ln.Width)`" gap=`"$($ln.Gap)`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$($ln.Style)</v8ui:style>"
X "`t</line>"
}
@@ -1346,6 +1463,12 @@ foreach ($key in $formatRegistry.Keys) {
X "`t`t`t`t<v8:content>$(Esc-XmlText $val)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</$tag>"
} 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</$tag>"
} else {
X "`t`t<$tag>$val</$tag>"
}
@@ -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)"
+208 -84
View File
@@ -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 — ссылка в палитру <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 ---
# Рамка хранится не в формате, а в палитре <line>: формат ссылается на запись индексом.
# Запись — тройка (стиль, ширина, 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
# Одинаковые четыре стороны платформа пишет одним <border>. Правило проверено на корпусе:
# 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</namedItem>')
# 7h. Line palette
if has_thin_borders:
lines.append('\t<line width="1" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
if has_thick_borders:
lines.append('\t<line width="2" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
for ln in line_registry:
lines.append(f'\t<line width="{ln["Width"]}" gap="{ln["Gap"]}">')
lines.append(f'\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{ln["Style"]}</v8ui:style>')
lines.append('\t</line>')
# 7i. Font palette
@@ -1313,6 +1430,7 @@ def main():
lines.append('\t<format>')
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<v8:content>{esc_xml_text(val)}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append(f'\t\t</{tag}>')
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}</{tag}>')
else:
lines.append(f'\t\t<{tag}>{val}</{tag}>')
@@ -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)}")
@@ -151,9 +151,6 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
</document>
@@ -101,17 +101,11 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
</document>
@@ -123,25 +123,16 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
<fillType>Parameter</fillType>
<format>
@@ -153,18 +144,12 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Right</horizontalAlignment>
<fillType>Parameter</fillType>
<format>
@@ -159,42 +159,27 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>1</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
</document>
@@ -194,25 +194,16 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Right</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
@@ -174,34 +174,22 @@
</format>
<format>
<font>1</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
</document>
@@ -140,17 +140,11 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
</document>
@@ -548,42 +548,27 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Right</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
@@ -117,10 +117,7 @@
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
</format>
</document>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>4</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Отчёт о движении</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>3</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Слева пунктир</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>4</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Цвет из палитры</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>5</f>
<parameter>Скрытое</parameter>
</c>
</c>
<c>
<c>
<f>6</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Рамка свёрнута</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>2</height>
<vgRows>2</vgRows>
<merge>
<r>0</r>
<c>0</c>
<w>3</w>
</merge>
<namedItem xsi:type="NamedItemCells">
<name>Тело</name>
<area>
<type>Rows</type>
<beginRow>1</beginRow>
<endRow>1</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
</area>
</namedItem>
<namedItem xsi:type="NamedItemCells">
<name>Шапка</name>
<area>
<type>Rows</type>
<beginRow>0</beginRow>
<endRow>0</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
</area>
</namedItem>
<line width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Dotted</v8ui:style>
</line>
<line width="2" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</line>
<line width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">ThinDashed</v8ui:style>
</line>
<line width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</line>
<font faceName="Arial" height="10" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<font faceName="Arial" height="10" bold="true" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<width>20</width>
</format>
<format>
<font>1</font>
<horizontalAlignment>Center</horizontalAlignment>
<verticalAlignment>Center</verticalAlignment>
<textColor>style:FormTextColor</textColor>
<backColor>#EBEBEB</backColor>
<textPlacement>Wrap</textPlacement>
<protection>true</protection>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>1</topBorder>
<bottomBorder>2</bottomBorder>
<borderColor>#C8C8C8</borderColor>
</format>
<format>
<font>0</font>
<textColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/colors/windows">d3p1:ButtonText</textColor>
<backColor xmlns:d3p1="http://v8.1c.ru/8.1/data/ui/colors/web">d3p1:Gainsboro</backColor>
</format>
<format>
<font>0</font>
<fillType>Parameter</fillType>
<hidden>true</hidden>
<textOrientation>900</textOrientation>
<indent>2</indent>
</format>
<format>
<font>0</font>
<border>3</border>
</format>
</document>
@@ -302,42 +302,27 @@
</format>
<format>
<font>1</font>
<leftBorder>1</leftBorder>
<topBorder>1</topBorder>
<rightBorder>1</rightBorder>
<bottomBorder>1</bottomBorder>
<border>1</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Center</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<textPlacement>Wrap</textPlacement>
<fillType>Parameter</fillType>
</format>
<format>
<font>0</font>
<leftBorder>0</leftBorder>
<topBorder>0</topBorder>
<rightBorder>0</rightBorder>
<bottomBorder>0</bottomBorder>
<border>0</border>
<horizontalAlignment>Right</horizontalAlignment>
<fillType>Parameter</fillType>
</format>
@@ -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"] }
}