refactor(mxl-compile): формат — таблица тегов вместо двенадцати полей

Формат был фиксированной записью из 12 полей, выписанной руками в четырёх
местах: ключ дедупликации, набор свойств, резолв стиля и эмиссия палитры.
Добавление свойства стоило восьми правок в двух портах, а свойств у формата
в выгрузке 47.

Теперь формат — набор «тег платформы → значение», а порядок эмиссии задаёт
один канонический список тегов. Список снят с корпуса ERP: 766 960 форматов,
ни один не нарушает эту последовательность.

Вывод не меняется: пилот из 40 макетов скомпилировался побайтово так же, как
до правки, обоими портами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 14:45:30 +03:00
co-authored by Claude Opus 5
parent c5a65ef620
commit c559696c5a
2 changed files with 145 additions and 165 deletions
@@ -1,4 +1,4 @@
# mxl-compile v1.27 — Compile 1C spreadsheet from JSON
# mxl-compile v1.28 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -480,14 +480,19 @@ function Resolve-Style {
}
}
return @{
FontIdx = $fontIdx
LB = $lb; TB = $tb; RB = $rb; BB = $bb
HA = $ha; VA = $va
Wrap = $wrap
FillType = $fillType
NumberFormat = $nf
}
# Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки
# роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов.
$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 }
return $props
}
# --- 6. Format palette builder ---
@@ -495,22 +500,45 @@ function Resolve-Style {
$formatRegistry = [ordered]@{} # key -> hashtable with properties
$formatOrder = @() # ordered keys for index assignment
function Get-FormatKey {
param(
[int]$fontIdx = -1,
[int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1,
[string]$ha = "", [string]$va = "",
[bool]$wrap = $false,
[string]$fillType = "",
[string]$numberFormat = "",
[int]$width = -1,
[int]$height = -1
# Канонический порядок тегов внутри <format>. Снят с корпуса: 766 960 форматов, ни один
# его не нарушает — платформа пишет теги строго в этой последовательности, и от неё
# зависит побайтовое совпадение с выгрузкой.
function Get-FormatTagOrder {
return @(
'print', 'drawingBorder',
'drawingHaveLeftBorder', 'drawingHaveTopBorder', 'drawingHaveRightBorder', 'drawingHaveBottomBorder',
'font', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder', 'border',
'height', 'borderColor', 'width', 'autoWidthCalculation', 'widthWeightFactor',
'horizontalAlignment', 'verticalAlignment', 'textColor', 'backColor',
'patternColor', 'pattern', 'textPlacement', 'fillType', 'protection', 'hidden',
'textOrientation', 'detailsUse', 'bySelectedColumns', 'markNegatives',
'containsValue', 'valueType', 'format', 'controlType', 'hyperLink',
'autoMarkIncomplete', 'indent', 'autoIndent', 'editFormat', 'columnSizeChange',
'mask', 'picIndex', 'pictureSizeMode', 'picHorizontalAlignment',
'picVerticalAlignment', 'textPosition'
)
return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|nf=$numberFormat|w=$width|h=$height"
}
# Теги, значение которых — многоязычная строка (<v8:item> на язык), а не скаляр.
function Get-FormatMlTags {
return @{ 'format' = $true; 'editFormat' = $true; 'mask' = $true }
}
$script:formatTagOrder = Get-FormatTagOrder
$script:formatMlTags = Get-FormatMlTags
function Get-FormatKey {
param([hashtable]$props)
$parts = @()
foreach ($tag in $script:formatTagOrder) {
if ($props.ContainsKey($tag)) { $parts += "$tag=$($props[$tag])" }
}
return ($parts -join '|')
}
function Register-Format {
param([string]$key, [hashtable]$props)
param([hashtable]$props)
$key = Get-FormatKey $props
if (-not $script:formatRegistry.Contains($key)) {
$script:formatRegistry[$key] = $props
$script:formatOrder += $key
@@ -525,16 +553,14 @@ function Register-Format {
}
# 6a. Default width format
$defaultFormatKey = Get-FormatKey -width $defaultWidth
$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth }
$defaultFormatIndex = Register-Format @{ width = $defaultWidth }
# 6b. Column width formats — по одной карте на каждую колоночную раскладку
foreach ($layout in $columnLayouts) {
$map = @{} # 1-based col -> format index
foreach ($col in ($layout.Widths.Keys | Sort-Object)) {
$w = $layout.Widths[$col]
$key = Get-FormatKey -width $w
$map[[int]$col] = Register-Format -key $key -props @{ Width = $w }
$map[[int]$col] = Register-Format @{ width = $w }
}
$layout.FormatMap = $map
}
@@ -606,27 +632,10 @@ function Register-CellFormat {
# платформа: в её палитре у неоформленного макета один формат (ширина колонки), и все
# ячейки указывают на него. Мы же заводили каждой свой формат с <font>0</font>, где ноль
# означает «шрифт не задан», то есть формат был пуст по смыслу.
if ($resolved.FontIdx -eq $fontMap["default"] -and
$resolved.LB -lt 0 -and $resolved.TB -lt 0 -and $resolved.RB -lt 0 -and $resolved.BB -lt 0 -and
-not $resolved.HA -and -not $resolved.VA -and -not $resolved.Wrap -and
-not $resolved.FillType -and -not $resolved.NumberFormat) {
if ($resolved.Count -eq 1 -and $resolved['font'] -eq $fontMap["default"]) {
return $script:defaultFormatIndex
}
$key = Get-FormatKey -fontIdx $resolved.FontIdx `
-lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB `
-ha $resolved.HA -va $resolved.VA `
-wrap $resolved.Wrap -fillType $resolved.FillType `
-numberFormat $resolved.NumberFormat
$props = @{
FontIdx = $resolved.FontIdx
LB = $resolved.LB; TB = $resolved.TB
RB = $resolved.RB; BB = $resolved.BB
HA = $resolved.HA; VA = $resolved.VA
Wrap = $resolved.Wrap
FillType = $resolved.FillType
NumberFormat = $resolved.NumberFormat
}
return Register-Format -key $key -props $props
return Register-Format $resolved
}
# --- 5.5. Шорткат строк: строка-массив ячеек ---
@@ -792,8 +801,7 @@ foreach ($area in $def.areas) {
# Row height format
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null
Register-Format @{ height = [int]$row.height } | Out-Null
}
# rowStyle gap-fill format (no content → no fillType)
@@ -955,13 +963,7 @@ foreach ($area in $def.areas) {
# Determine row height format
$rowFormatIdx = 0
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
# Find format index for this key
$rIdx = 0
foreach ($k in $formatRegistry.Keys) {
$rIdx++
if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break }
}
$rowFormatIdx = Register-Format @{ height = [int]$row.height }
}
if ($row.cells -and $row.cells.Count -gt 0) {
@@ -1334,46 +1336,19 @@ foreach ($key in $formatRegistry.Keys) {
$fmt = $formatRegistry[$key]
X "`t<format>"
if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) {
X "`t`t<font>$($fmt.FontIdx)</font>"
}
if ($fmt.LB -ne $null -and $fmt.LB -ge 0) {
X "`t`t<leftBorder>$($fmt.LB)</leftBorder>"
}
if ($fmt.TB -ne $null -and $fmt.TB -ge 0) {
X "`t`t<topBorder>$($fmt.TB)</topBorder>"
}
if ($fmt.RB -ne $null -and $fmt.RB -ge 0) {
X "`t`t<rightBorder>$($fmt.RB)</rightBorder>"
}
if ($fmt.BB -ne $null -and $fmt.BB -ge 0) {
X "`t`t<bottomBorder>$($fmt.BB)</bottomBorder>"
}
if ($fmt.Width) {
X "`t`t<width>$($fmt.Width)</width>"
}
if ($fmt.Height) {
X "`t`t<height>$($fmt.Height)</height>"
}
if ($fmt.HA) {
X "`t`t<horizontalAlignment>$($fmt.HA)</horizontalAlignment>"
}
if ($fmt.VA) {
X "`t`t<verticalAlignment>$($fmt.VA)</verticalAlignment>"
}
if ($fmt.Wrap -eq $true) {
X "`t`t<textPlacement>Wrap</textPlacement>"
}
if ($fmt.FillType) {
X "`t`t<fillType>$($fmt.FillType)</fillType>"
}
if ($fmt.NumberFormat) {
X "`t`t<format>"
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-XmlText $fmt.NumberFormat)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</format>"
foreach ($tag in $script:formatTagOrder) {
if (-not $fmt.ContainsKey($tag)) { continue }
$val = $fmt[$tag]
if ($script:formatMlTags.ContainsKey($tag)) {
X "`t`t<$tag>"
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-XmlText $val)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</$tag>"
} else {
X "`t`t<$tag>$val</$tag>"
}
}
X "`t</format>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.27 — Compile 1C spreadsheet from JSON
# mxl-compile v1.28 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -326,6 +326,38 @@ def parse_col_value(val):
return None
def format_tag_order():
"""Канонический порядок тегов внутри <format>. Снят с корпуса: 766 960 форматов, ни один
его не нарушает — платформа пишет теги строго в этой последовательности, и от неё
зависит побайтовое совпадение с выгрузкой."""
return [
'print', 'drawingBorder',
'drawingHaveLeftBorder', 'drawingHaveTopBorder', 'drawingHaveRightBorder', 'drawingHaveBottomBorder',
'font', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder', 'border',
'height', 'borderColor', 'width', 'autoWidthCalculation', 'widthWeightFactor',
'horizontalAlignment', 'verticalAlignment', 'textColor', 'backColor',
'patternColor', 'pattern', 'textPlacement', 'fillType', 'protection', 'hidden',
'textOrientation', 'detailsUse', 'bySelectedColumns', 'markNegatives',
'containsValue', 'valueType', 'format', 'controlType', 'hyperLink',
'autoMarkIncomplete', 'indent', 'autoIndent', 'editFormat', 'columnSizeChange',
'mask', 'picIndex', 'pictureSizeMode', 'picHorizontalAlignment',
'picVerticalAlignment', 'textPosition',
]
def format_ml_tags():
"""Теги, значение которых — многоязычная строка (<v8:item> на язык), а не скаляр."""
return {'format': True, 'editFormat': True, 'mask': True}
def get_format_key(props):
parts = []
for tag in format_tag_order():
if tag in props:
parts.append(f'{tag}={props[tag]}')
return '|'.join(parts)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -568,24 +600,35 @@ def main():
if style.get('format'):
nf = style['format']
return {
'FontIdx': font_idx,
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
'HA': ha, 'VA': va,
'Wrap': wrap,
'FillType': fill_type,
'NumberFormat': nf,
}
# Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки
# роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов.
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
return props
# --- 6. Format palette builder ---
format_registry = {} # key -> props
format_order = [] # ordered keys for index assignment
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
wrap=False, fill_type='', number_format='', width=-1, height=-1):
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
def register_format(key, props):
def register_format(props):
key = get_format_key(props)
if key not in format_registry:
format_registry[key] = props
format_order.append(key)
@@ -593,15 +636,14 @@ def main():
return format_order.index(key) + 1
# 6a. Default width format
default_format_key = get_format_key(width=default_width)
default_format_index = register_format(default_format_key, {'Width': default_width})
default_format_index = register_format({'width': default_width})
# 6b. Column width formats — по одной карте на каждую колоночную раскладку
for layout in column_layouts:
fmap = {} # 1-based col -> format index
for col in sorted(layout['Widths']):
w = layout['Widths'][col]
fmap[int(col)] = register_format(get_format_key(width=w), {'Width': w})
fmap[int(col)] = register_format({'width': w})
layout['FormatMap'] = fmap
col_format_map = column_layouts[0]['FormatMap']
@@ -644,28 +686,9 @@ def main():
# платформа: в её палитре у неоформленного макета один формат (ширина колонки), и все
# ячейки указывают на него. Мы же заводили каждой свой формат с <font>0</font>, где ноль
# означает «шрифт не задан», то есть формат был пуст по смыслу.
if (resolved['FontIdx'] == font_map.get('default', 0)
and resolved['LB'] < 0 and resolved['TB'] < 0
and resolved['RB'] < 0 and resolved['BB'] < 0
and not resolved['HA'] and not resolved['VA'] and not resolved['Wrap']
and not resolved['FillType'] and not resolved['NumberFormat']):
if len(resolved) == 1 and resolved.get('font') == font_map.get('default', 0):
return default_format_index
key = get_format_key(
font_idx=resolved['FontIdx'],
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
ha=resolved['HA'], va=resolved['VA'],
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
number_format=resolved['NumberFormat'])
props = {
'FontIdx': resolved['FontIdx'],
'LB': resolved['LB'], 'TB': resolved['TB'],
'RB': resolved['RB'], 'BB': resolved['BB'],
'HA': resolved['HA'], 'VA': resolved['VA'],
'Wrap': resolved['Wrap'],
'FillType': resolved['FillType'],
'NumberFormat': resolved['NumberFormat'],
}
return register_format(key, props)
return register_format(resolved)
# --- 5.5. Шорткат строк: строка-массив ячеек ---
# Та же форма, что у макетов СКД (skd-compile): позиция ячейки = индекс в массиве,
@@ -814,8 +837,7 @@ def main():
# Row height format
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
register_format(h_key, {'Height': int(row['height'])})
register_format({'height': int(row['height'])})
# rowStyle gap-fill format
if row.get('rowStyle'):
@@ -962,9 +984,7 @@ def main():
# Determine row height format
row_format_idx = 0
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
if h_key in format_registry:
row_format_idx = format_order.index(h_key) + 1
row_format_idx = register_format({'height': int(row['height'])})
if row.get('cells') and len(row['cells']) > 0:
row_has_content = True
@@ -1292,35 +1312,20 @@ def main():
fmt = format_registry[key]
lines.append('\t<format>')
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
if fmt.get('Width'):
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
if fmt.get('Height'):
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
if fmt.get('HA'):
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
if fmt.get('VA'):
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
if fmt.get('Wrap') is True:
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
if fmt.get('FillType'):
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
if fmt.get('NumberFormat'):
lines.append('\t\t<format>')
lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t<v8:content>{esc_xml_text(fmt["NumberFormat"])}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append('\t\t</format>')
ml_tags = format_ml_tags()
for tag in format_tag_order():
if tag not in fmt:
continue
val = fmt[tag]
if tag in ml_tags:
lines.append(f'\t\t<{tag}>')
lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
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}>')
else:
lines.append(f'\t\t<{tag}>{val}</{tag}>')
lines.append('\t</format>')