feat(mxl-compile,mxl-decompile): значение ячейки-поля ввода и настройки элемента управления

Ключ ячейки value несёт значение, набранное в поле ввода, ключ control —
сериализованные настройки самого элемента управления. Оба живут у ячейки,
а не в палитре формата: у двух ячеек с одинаковым оформлением значения
разные.

Тип значения выражается литералом JSON и приведения к объявленному типу
НЕ делается. Так пишет платформа: ссылочный и составной тип она хранит
строкой, а при смене типа ячейки прежнее значение не переписывает — на
корпусе 560 ячеек объявлены числом, но несут строку. Приведение здесь
означало бы переписывать данные при пересборке. Дата литералом JSON не
выражается, поэтому едет строкой и читается как дата только у ячейки,
объявленной датой.

Настройки элемента управления возим дословно: 26 370 вхождений корпуса
дают всего 163 различных блоба, разбирать структуру незачем. Переводы
строк внутри блоба приводим к LF на чтении — XML-парсеры двух портов
нормализуют их по-разному, и без этого порты давали разный JSON на одном
файле.

Стенд СЗначениями снова собирается байт в байт обоими портами: фикстура
обновлена до версии со значениями, флажком и блобом настроек.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-15 13:32:40 +03:00
co-authored by Claude Opus 5
parent 66bb285636
commit 43d2f660da
14 changed files with 951 additions and 17 deletions
@@ -1,4 +1,4 @@
# mxl-compile v1.41 — Compile 1C spreadsheet from JSON
# mxl-compile v1.42 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -854,6 +854,14 @@ function Get-CellValueProps {
[Console]::Error.WriteLine("Cell 'controlType' requires 'valueType' ($where)")
exit 1
}
if ($null -ne $cell.PSObject.Properties['value'] -and $null -ne $cell.value) {
[Console]::Error.WriteLine("Cell 'value' requires 'valueType' ($where)")
exit 1
}
if ($null -ne $cell.control -and "$($cell.control)" -ne '') {
[Console]::Error.WriteLine("Cell 'control' requires 'valueType' ($where)")
exit 1
}
return $props
}
# Ячейка, содержащая значение, текста не несёт: платформа этого не допускает, и в корпусе
@@ -885,6 +893,46 @@ function Get-CellValueProps {
return $props
}
# Значение ячейки-поля ввода — тег САМОЙ ячейки (<v>), а не запись палитры: у двух ячеек
# с одинаковым оформлением значения разные, и в дедупликацию формата оно не входит.
# Тип значения берём из литерала JSON и приведения НЕ делаем: платформа хранит значение с его
# собственным типом, и совпадать с объявленным он не обязан (ссылочный и составной тип она
# держит строкой, а при смене типа ячейки прежнее значение не переписывает).
function Get-CellValue {
param($cell, [string]$canonType, [string]$where)
$prop = $cell.PSObject.Properties['value']
if ($null -eq $prop -or $null -eq $prop.Value) { return $null }
$v = $prop.Value
if ($v -is [bool]) {
return @{ Type = 'xs:boolean'; Text = $(if ($v) { 'true' } else { 'false' }) }
}
if ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) {
return @{ Type = 'xs:decimal'; Text = (Format-Num $v) }
}
$s = "$v"
if ($s -ne '') {
# Дата литералом JSON не выражается, поэтому едет строкой. Читаем её как дату только
# у ячейки, объявленной датой: там платформа других значений не держит.
if (($canonType -ceq 'Date' -or $canonType -ceq 'DateTime' -or $canonType -ceq 'Time') -and
$s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') {
return @{ Type = 'xs:dateTime'; Text = $s }
}
return @{ Type = 'xs:string'; Text = $s }
}
# Пустая строка — пустое значение ОБЪЯВЛЕННОГО типа: платформа так и пишет, когда текст
# ячейки к типу не привёлся. Ссылочный и составной тип пустого значения своего вида не
# имеют — там остаётся пустая строка.
if ($canonType -ceq 'Boolean') { return @{ Type = 'xs:boolean'; Text = 'false' } }
if ($canonType -like 'Number(*') { return @{ Type = 'xs:decimal'; Text = '0' } }
if ($canonType -ceq 'Date' -or $canonType -ceq 'DateTime' -or $canonType -ceq 'Time') {
return @{ Type = 'xs:dateTime'; Text = '0001-01-01T00:00:00' }
}
return @{ Type = 'xs:string'; Text = '' }
}
# --- 6. Format palette builder ---
$formatRegistry = [ordered]@{} # key -> hashtable with properties
@@ -1140,7 +1188,7 @@ function Set-CellProp {
function Test-CellObject {
param($el)
$cellKeys = @('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template',
'valueType', 'controlType')
'valueType', 'controlType', 'value', 'control')
foreach ($p in $el.PSObject.Properties) {
if ($cellKeys -contains $p.Name) { return $true }
}
@@ -1613,6 +1661,8 @@ foreach ($area in $def.areas) {
Detail = $cell.detail
Text = $cell.text
Template = $cell.template
Value = $(if ($vp.Count -gt 0) { Get-CellValue $cell "$($vp['valueType'])" "area `"$areaName`", row $($localRow + 1)" } else { $null })
Control = $(if ($vp.Count -gt 0) { $cell.control } else { $null })
}
$rowCells += $cellInfo
@@ -1719,6 +1769,22 @@ foreach ($area in $def.areas) {
if ($null -ne $cellInfo.Template) { Emit-CellText $cellInfo.Template }
if ($null -ne $cellInfo.Value) {
X "`t`t`t`t`t<v xsi:type=`"$($cellInfo.Value.Type)`">$(Esc-XmlText $cellInfo.Value.Text)</v>"
}
# Настройки элемента управления платформа сериализует в base64 и держит у самой
# ячейки. Структуру не разбираем — возим дословно, иначе она теряется.
if ($null -ne $cellInfo.Control -and "$($cellInfo.Control)" -ne '') {
# Блоб платформа переносит по строкам, а X дописывает перевод строки сам —
# поэтому каждую строку блоба пишем отдельным вызовом, иначе перевод строки
# внутри значения разошёлся бы с остальным файлом.
$parts = @("$($cellInfo.Control)" -split "`r?`n")
$parts[0] = "`t`t`t`t`t<control xsi:type=`"xs:base64Binary`">$($parts[0])"
$parts[-1] = "$($parts[-1])</control>"
foreach ($p in $parts) { X $p }
}
# Расшифровка от параметра заполнения не зависит: на корпусе 20 404 ячейки несут
# её БЕЗ параметра (у текста, у пустой ячейки, у поля ввода) против 8 582 с ним.
# Пока она стояла внутри ветки параметра, две трети расшифровок терялись молча.
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.41 — Compile 1C spreadsheet from JSON
# mxl-compile v1.42 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -644,6 +644,45 @@ def emit_value_type_content(lines, indent, canon_type):
lines.extend(quals[q])
def cell_value(cell, canon_type, where):
"""Значение ячейки-поля ввода — тег САМОЙ ячейки (<v>), а не запись палитры: у двух ячеек
с одинаковым оформлением значения разные, и в дедупликацию формата оно не входит.
Тип значения берём из литерала JSON и приведения НЕ делаем: платформа хранит значение с его
собственным типом, и совпадать с объявленным он не обязан (ссылочный и составной тип она
держит строкой, а при смене типа ячейки прежнее значение не переписывает)."""
if 'value' not in cell:
return None
v = cell.get('value')
if v is None:
return None
if isinstance(v, bool):
return {'Type': 'xs:boolean', 'Text': 'true' if v else 'false'}
if isinstance(v, (int, float)):
# 8.0 обязано дать "8", как в PS-порту: там Convert.ToString инвариантной культурой
# хвостовой .0 не пишет, а str() в py — пишет.
text = str(int(v)) if (isinstance(v, float) and v.is_integer()) else str(v)
return {'Type': 'xs:decimal', 'Text': text}
s = str(v)
if s != '':
# Дата литералом JSON не выражается, поэтому едет строкой. Читаем её как дату только
# у ячейки, объявленной датой: там платформа других значений не держит.
if canon_type in ('Date', 'DateTime', 'Time') and re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$', s):
return {'Type': 'xs:dateTime', 'Text': s}
return {'Type': 'xs:string', 'Text': s}
# Пустая строка — пустое значение ОБЪЯВЛЕННОГО типа: платформа так и пишет, когда текст
# ячейки к типу не привёлся. Ссылочный и составной тип пустого значения своего вида не
# имеют — там остаётся пустая строка.
if canon_type == 'Boolean':
return {'Type': 'xs:boolean', 'Text': 'false'}
if canon_type.startswith('Number('):
return {'Type': 'xs:decimal', 'Text': '0'}
if canon_type in ('Date', 'DateTime', 'Time'):
return {'Type': 'xs:dateTime', 'Text': '0001-01-01T00:00:00'}
return {'Type': 'xs:string', 'Text': ''}
def cell_value_props(cell, where):
"""Свойства значения ячейки — пустой набор, если ячейка обычная. Считаются ОДНОЙ функцией:
пре-проход регистрации палитры и генерация обязаны получить один и тот же набор, иначе
@@ -655,6 +694,12 @@ def cell_value_props(cell, where):
if ctl is not None and str(ctl) != '':
print(f"Cell 'controlType' requires 'valueType' ({where})", file=sys.stderr)
sys.exit(1)
if cell.get('value') is not None:
print(f"Cell 'value' requires 'valueType' ({where})", file=sys.stderr)
sys.exit(1)
if cell.get('control'):
print(f"Cell 'control' requires 'valueType' ({where})", file=sys.stderr)
sys.exit(1)
return props
# Ячейка, содержащая значение, текста не несёт: платформа этого не допускает, и в корпусе
# нет ни одной такой ячейки из 370 197.
@@ -1154,7 +1199,7 @@ def main():
ключей есть ключ схемы ячейки, во втором ключи — идентификаторы языков. Пересечений
нет: в корпусе это ru, en, ru1, Русский."""
cell_keys = ('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template',
'valueType', 'controlType')
'valueType', 'controlType', 'value', 'control')
return any(k in el for k in cell_keys)
def expand_shorthand_row(row, area_name, row_idx, open_by_col, max_cols):
@@ -1570,6 +1615,9 @@ def main():
'Detail': cell.get('detail'),
'Text': cell.get('text'),
'Template': cell.get('template'),
'Value': (cell_value(cell, vp.get('valueType', ''),
f'area "{area_name}", row {local_row + 1}') if vp else None),
'Control': (cell.get('control') if vp else None),
}
row_cells.append(cell_info)
@@ -1667,6 +1715,21 @@ def main():
if cell_info['Template'] is not None:
emit_cell_text(lines, cell_info['Template'])
if cell_info.get('Value') is not None:
val = cell_info['Value']
lines.append(f'\t\t\t\t\t<v xsi:type="{val["Type"]}">{esc_xml_text(val["Text"])}</v>')
# Настройки элемента управления платформа сериализует в base64 и держит у самой
# ячейки. Структуру не разбираем — возим дословно, иначе она теряется.
if cell_info.get('Control'):
# Блоб платформа переносит по строкам, а строки документа склеиваются
# позже — поэтому каждую строку блоба кладём отдельным элементом списка,
# иначе перевод строки внутри значения разошёлся бы с остальным файлом.
parts = str(cell_info['Control']).replace('\r\n', '\n').split('\n')
parts[0] = f'\t\t\t\t\t<control xsi:type="xs:base64Binary">{parts[0]}'
parts[-1] = f'{parts[-1]}</control>'
lines.extend(parts)
# Расшифровка от параметра заполнения не зависит: на корпусе 20 404 ячейки несут
# её БЕЗ параметра (у текста, у пустой ячейки, у поля ввода) против 8 582 с ним.
# Пока она стояла внутри ветки параметра, две трети расшифровок терялись молча.