feat(mxl-compile,mxl-decompile): языки макета и шрифт как у платформы

Контролируемый стенд (upload/epf/МакетТабличныйДокумент) вскрыл четыре
системных расхождения подряд. После правок 7 макетов стенда из 10 проходят
раундтрип БАЙТ В БАЙТ обоими портами — до этого ни один.

1. Объявление языков макета жёстко писалось как «только русский». Это не то же
   самое, что textLanguages: почти во всех макетах ERP объявлен один ru, а текст
   лежит и под ru, и под en, поэтому выводить одно из другого нельзя. Заведены
   недокументированные ключи languages / currentLanguage / defaultLanguage —
   автору они не нужны, нужны раундтрипу. defaultLanguage несём, а не выводим:
   «всегда ru» — наблюдение на четырёх русских типовых, а не правило формата.

2. <font> писался в каждом формате. Платформа пишет его только когда шрифт
   задан: треть форматов корпуса (23 003 из 69 581) обходится без него.

3. Декомпилятор считал шрифт с индексом 0 «обычным» и не писал его в стиль.
   Нулевой шрифт вовсе не обязан быть обычным — в макетах стенда он курсивный
   и жирный, и начертание терялось.

4. Формат, несущий ТОЛЬКО шрифт, выглядел пустым: у ячейки он получал
   зарезервированное имя default и не записывался, у строки терялся целиком.

Остаток на стенде — три макета, и все три про одно: ссылка ячейки на запись
ширины, оставшуюся от прежнего состояния документа. Это не воспроизводится
принципиально, разобрано в docs/1c-spreadsheet-spec.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 20:08:28 +03:00
co-authored by Claude Opus 5
parent f72dd544dd
commit fc792bfee6
33 changed files with 356 additions and 379 deletions
@@ -1,4 +1,4 @@
# mxl-compile v1.35 — Compile 1C spreadsheet from JSON
# mxl-compile v1.36 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -502,7 +502,7 @@ function Get-RowFormatProps {
$props = @{}
$spec = Get-RowStyleSpec $row.rowStyle "row"
if ($spec.Name -and $spec.Apply -cne 'cells') {
$props = Resolve-Style -styleName $spec.Name -fillType "" -noDefaultFont
$props = Resolve-Style -styleName $spec.Name -fillType ""
}
if ($row.height) { $props['height'] = [int]$row.height }
if ($row.hidden -eq $true) { $props['hidden'] = 'true' }
@@ -510,12 +510,13 @@ function Get-RowFormatProps {
}
function Resolve-Style {
param([string]$styleName, [string]$fillType, [switch]$noDefaultFont)
param([string]$styleName, [string]$fillType)
# Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки
# роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов.
# <font> пишем только когда стиль задал шрифт: треть форматов корпуса (23 003 из 69 581)
# обходится без него, а мы раньше подставляли шрифт по умолчанию всегда.
$props = @{}
if (-not $noDefaultFont) { $props['font'] = $fontMap["default"] }
if ($styleName -and $def.styles) {
$style = $def.styles.$styleName
@@ -753,7 +754,7 @@ foreach ($layout in $columnLayouts) {
if ($styleName) {
# Шрифт по умолчанию колонке не навязываем: формат колонки без оформления —
# это ровно <width>, как пишет платформа.
$props = Resolve-Style -styleName $styleName -fillType "" -noDefaultFont
$props = Resolve-Style -styleName $styleName -fillType ""
}
if ($layout.Widths.ContainsKey($col)) { $props['width'] = $layout.Widths[$col] }
$map[$col] = Register-Format $props
@@ -827,7 +828,7 @@ function Register-CellFormat {
# У ячейки без собственного оформления формата НЕТ вовсе: <f>0</f>, где ноль — не индекс
# записи, а «формата нет». В корпусе так у 170 710 ячеек против 50 635, ссылающихся на
# формат по умолчанию; <f>0</f> встречается в 71% макетов.
if ($resolved.Count -eq 1 -and $resolved['font'] -eq $fontMap["default"]) {
if ($resolved.Count -eq 0) {
return 0
}
return Register-Format $resolved
@@ -1065,14 +1066,50 @@ X '<?xml version="1.0" encoding="UTF-8"?>'
X "<document $docNsDecl>"
# 7b. Language settings
# Языки МАКЕТА — не то же, что textLanguages: там языки, на которые разворачивается текст
# ячейки, здесь объявление в шапке. В 98% макетов ERP объявлен один ru, а текст лежит и под
# ru, и под en, поэтому выводить одно из другого нельзя. Ключи недокументированные: автору
# они не нужны, нужны раундтрипу.
$langCodes = @{ 'ru' = 'Русский'; 'en' = 'Английский' }
$langList = @()
if ($def.languages) {
foreach ($l in @($def.languages)) {
if ($l -is [string]) {
$code = if ($langCodes.ContainsKey("$l")) { $langCodes["$l"] } else { "$l" }
$langList += @{ Id = "$l"; Code = $code; Description = $code }
} else {
# description отсутствует и пустая строка — одно и то же: тег есть всегда, но
# бывает самозакрывающимся (77 записей корпуса против 11 017 со значением).
$langList += @{
Id = "$($l.id)"
Code = "$($l.code)"
Description = if ($null -eq $l.description) { '' } else { "$($l.description)" }
}
}
}
}
if ($langList.Count -eq 0) {
$langList = @(@{ Id = 'ru'; Code = 'Русский'; Description = 'Русский' })
}
$currentLang = if ($def.PSObject.Properties['currentLanguage']) { $def.currentLanguage } else { 'ru' }
$defaultLang = if ($def.defaultLanguage) { "$($def.defaultLanguage)" } else { 'ru' }
X "`t<languageSettings>"
X "`t`t<currentLanguage>ru</currentLanguage>"
X "`t`t<defaultLanguage>ru</defaultLanguage>"
X "`t`t<languageInfo>"
X "`t`t`t<id>ru</id>"
X "`t`t`t<code>Русский</code>"
X "`t`t`t<description>Русский</description>"
X "`t`t</languageInfo>"
if ($null -ne $currentLang -and "$currentLang" -ne '') {
X "`t`t<currentLanguage>$(Esc-XmlText "$currentLang")</currentLanguage>"
}
X "`t`t<defaultLanguage>$(Esc-XmlText $defaultLang)</defaultLanguage>"
foreach ($l in $langList) {
X "`t`t<languageInfo>"
X "`t`t`t<id>$(Esc-XmlText $l.Id)</id>"
X "`t`t`t<code>$(Esc-XmlText $l.Code)</code>"
if ($l.Description -eq '') {
X "`t`t`t<description/>"
} else {
X "`t`t`t<description>$(Esc-XmlText $l.Description)</description>"
}
X "`t`t</languageInfo>"
}
X "`t</languageSettings>"
# 7c. Columns
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.35 — Compile 1C spreadsheet from JSON
# mxl-compile v1.36 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -695,17 +695,19 @@ def main():
props = {}
name, apply_to = row_style_spec(row.get('rowStyle'), 'row')
if name and apply_to != 'cells':
props = resolve_style(name, '', no_default_font=True)
props = resolve_style(name, '')
if row.get('height'):
props['height'] = int(row['height'])
if row.get('hidden') is True:
props['hidden'] = 'true'
return props
def resolve_style(style_name, fill_type, no_default_font=False):
def resolve_style(style_name, fill_type):
# Набор свойств формата — «тег платформы → значение», только заданные. Порядок вставки
# роли не играет: и ключ дедупликации, и эмиссия идут по каноническому порядку тегов.
props = {} if no_default_font else {'font': font_map.get('default', 0)}
# <font> пишем только когда стиль задал шрифт: треть форматов корпуса (23 003 из
# 69 581) обходится без него, а мы раньше подставляли шрифт по умолчанию всегда.
props = {}
if style_name and defn.get('styles'):
style = defn['styles'].get(style_name)
@@ -827,7 +829,7 @@ def main():
if style_name:
# Шрифт по умолчанию колонке не навязываем: формат колонки без оформления —
# это ровно <width>, как пишет платформа.
props = resolve_style(style_name, '', no_default_font=True)
props = resolve_style(style_name, '')
if col in layout['Widths']:
props['width'] = layout['Widths'][col]
fmap[col] = register_format(props)
@@ -872,7 +874,7 @@ def main():
# У ячейки без собственного оформления формата НЕТ вовсе: <f>0</f>, где ноль — не
# индекс записи, а «формата нет». В корпусе так у 170 710 ячеек против 50 635,
# ссылающихся на формат по умолчанию; <f>0</f> встречается в 71% макетов.
if len(resolved) == 1 and resolved.get('font') == font_map.get('default', 0):
if not resolved:
return 0
return register_format(resolved)
@@ -1077,15 +1079,43 @@ def main():
lines.append(f'<document {doc_ns_decl}>')
# 7b. Language settings
lines.append('\t<languageSettings>')
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
lines.append('\t\t<languageInfo>')
lines.append('\t\t\t<id>ru</id>')
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
lines.append('\t\t</languageInfo>')
lines.append('\t</languageSettings>')
# Языки МАКЕТА — не то же, что textLanguages: там языки, на которые разворачивается текст
# ячейки, здесь объявление в шапке. Почти во всех макетах ERP объявлен один ru, а текст
# лежит и под ru, и под en, поэтому выводить одно из другого нельзя. Ключи
# недокументированные: автору они не нужны, нужны раундтрипу.
RU = '\u0420\u0443\u0441\u0441\u043a\u0438\u0439'
lang_codes = {'ru': RU, 'en': '\u0410\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u0438\u0439'}
lang_list = []
for lang in (defn.get('languages') or []):
if isinstance(lang, dict):
# description отсутствует и пустая строка — одно и то же: тег есть всегда, но
# бывает самозакрывающимся (77 записей корпуса против 11 017 со значением).
lang_list.append({'Id': str(lang.get('id') or ''),
'Code': str(lang.get('code') or ''),
'Description': '' if lang.get('description') is None
else str(lang['description'])})
else:
code = lang_codes.get(str(lang), str(lang))
lang_list.append({'Id': str(lang), 'Code': code, 'Description': code})
if not lang_list:
lang_list = [{'Id': 'ru', 'Code': RU, 'Description': RU}]
current_lang = defn['currentLanguage'] if 'currentLanguage' in defn else 'ru'
default_lang = str(defn.get('defaultLanguage') or 'ru')
lines.append(' <languageSettings>')
if current_lang is not None and str(current_lang) != '':
lines.append(f' <currentLanguage>{esc_xml_text(str(current_lang))}</currentLanguage>')
lines.append(f' <defaultLanguage>{esc_xml_text(default_lang)}</defaultLanguage>')
for lang in lang_list:
lines.append(' <languageInfo>')
lines.append(f' <id>{esc_xml_text(lang["Id"])}</id>')
lines.append(f' <code>{esc_xml_text(lang["Code"])}</code>')
if lang['Description'] == '':
lines.append(' <description/>')
else:
lines.append(f' <description>{esc_xml_text(lang["Description"])}</description>')
lines.append(' </languageInfo>')
lines.append(' </languageSettings>')
# 7c. Columns
# Раскладка по умолчанию идёт первой и без <id> — так их хранит платформа.