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

В позиционной записи строки элемент — это значение содержимого ячейки, а значение
текста по общей конвенции бывает строкой либо объектом «язык → текст». Значит
объект {ru, en} там законен так же, как строка, и новой формы это не заводит.

Раньше ячейка с многоязычным текстом не считалась «простой», поэтому строка
двуязычного макета оставалась объектной:

  { "cells": [{ "text": { "ru": "Пусто", "en": "Empty" } }, { … }] }
  [{ "ru": "Пусто", "en": "Empty" }, { … }]

Объект-элемент читается как ячейка, если несёт хоть один её ключ, и как текст
в противном случае: идентификаторы языков с ключами ячейки не пересекаются
(в корпусе это ru, en, ru1, Русский).

Попутно исправлен признак «список позиционный»: он искал элемент-строку, поэтому
строка из одних многоязычных текстов позиционной не признавалась и ключ cells
у неё оставался.

Правка не должна менять скомпилированный XML — и не меняет: пилот из 40 макетов
собрался побайтово так же, JSON при этом изменился у 13. Стенд 7 из 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-11 21:13:32 +03:00
co-authored by Claude Opus 5
parent f926acd4f4
commit b5fef09844
6 changed files with 62 additions and 17 deletions
@@ -144,12 +144,16 @@
| Элемент | Значение |
|---------|----------|
| `"текст"` | Статический текст (`text`) |
| `{ "ru": "…", "en": "…" }` | Тот же текст на нескольких языках |
| `"{Имя}"` | Параметр (`param`) |
| `">"` | Продолжение ячейки слева — увеличивает её `span` |
| `"|"` | Продолжение ячейки сверху — увеличивает её `rowspan` |
| `null` | Пустая колонка: позиция занята, ячейка не создаётся |
| `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` |
Объект-элемент читается как ячейка, если несёт хоть один её ключ (`span`, `rowspan`, `style`,
`param`, `detail`, `text`, `template`), и как многоязычный текст в противном случае.
```json
"rows": [
["Вид", "Остаток", ">", "Итог"],
@@ -1,4 +1,4 @@
# mxl-compile v1.36 — Compile 1C spreadsheet from JSON
# mxl-compile v1.37 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -845,6 +845,17 @@ function Set-CellProp {
$cell | Add-Member -NotePropertyName $name -NotePropertyValue $value -Force
}
# Ячейка это или многоязычный текст: у ячейки есть хоть один ключ её схемы, у ML-значения
# ключи — идентификаторы языков. Пересечений нет: в корпусе это ru, en, ru1, Русский.
function Test-CellObject {
param($el)
$cellKeys = @('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template')
foreach ($p in $el.PSObject.Properties) {
if ($cellKeys -contains $p.Name) { return $true }
}
return $false
}
function Expand-ShorthandRow {
param($row, [string]$areaName, [int]$rowIdx, $openByCol, [int]$maxCols)
@@ -897,6 +908,12 @@ function Expand-ShorthandRow {
$m = [regex]::Match($el, '^\{(.+)\}$')
if ($m.Success) { Set-CellProp $cell 'param' $m.Groups[1].Value }
else { Set-CellProp $cell 'text' $el }
} elseif (-not (Test-CellObject $el)) {
# Объект без единого ключа ячейки — это многоязычный ТЕКСТ: в позиционной записи
# элемент и есть значение текста, а значение текста по общей конвенции бывает
# строкой либо объектом «язык → текст».
$cell = [PSCustomObject]@{ col = $idx; span = 1 }
Set-CellProp $cell 'text' $el
} else {
# Объектный элемент — обычная ячейка mxl, позиция берётся из индекса.
if ($el.PSObject.Properties['col']) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.36 — Compile 1C spreadsheet from JSON
# mxl-compile v1.37 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -884,6 +884,13 @@ def main():
# "{Имя}" — параметр. Разворачиваем в обычную строку с явными col/span/rowspan,
# поэтому весь код ниже про шорткат не знает.
def is_cell_object(el):
"""Ячейка это или многоязычный текст: у ячейки есть хоть один ключ её схемы,
у ML-значения ключи — идентификаторы языков. Пересечений нет: в корпусе это
ru, en, ru1, Русский."""
cell_keys = ('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template')
return any(k in el for k in cell_keys)
def expand_shorthand_row(row, area_name, row_idx, open_by_col, max_cols):
cells = []
placed = {} # 1-based col -> ячейка, занимающая колонку в ЭТОЙ строке
@@ -933,6 +940,11 @@ def main():
cell['param'] = m.group(1)
else:
cell['text'] = el
elif not is_cell_object(el):
# Объект без единого ключа ячейки — это многоязычный ТЕКСТ: в позиционной
# записи элемент и есть значение текста, а значение текста по общей конвенции
# бывает строкой либо объектом «язык → текст».
cell = CIDict({'col': idx, 'span': 1, 'text': el})
else:
# Объектный элемент — обычная ячейка mxl, позиция берётся из индекса.
if 'col' in el:
@@ -1,4 +1,4 @@
# mxl-decompile v1.15 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.16 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -631,20 +631,17 @@ function Get-ColumnStyles {
return $out
}
# Список признаётся позиционным по тому же правилу, что и в компиляторе: есть элемент-строка
# или пропуск. Список из одних объектов таковым не считаем — он читается как обычный.
# Список позиционный, если позиция в нём НЕ записана: ни у одного элемента нет col.
# Прежняя проверка искала элемент-строку, поэтому строка из одних многоязычных текстов
# позиционной не признавалась и оставалась объектной.
function Test-PositionalList {
param($cells)
foreach ($el in $cells) {
if ($null -eq $el -or $el -is [string]) { return $true }
if ($el -is [System.Collections.IDictionary] -and $el.Contains('col')) { return $false }
}
return $false
return $true
}
# Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется.
# Применяем, когда первая ячейка стоит в колонке 1 — иначе список начнётся с череды null
# и станет длиннее объектного. Ячейка, у которой кроме текста или параметра ничего нет,
# пишется строкой; span раскрывается маркерами ">"; всё прочее — объектным элементом без col.
# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline.
# В отличие от ConvertTo-Json (PS5.1):
# - не выравнивает ключи объекта по самому длинному
@@ -771,6 +768,10 @@ function ConvertTo-CompactJson {
return (Convert-StringToJsonLiteral "$obj")
}
# Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется.
# Ячейка, у которой кроме текста или параметра ничего нет, пишется значением; span
# раскрывается маркерами ">"; пропуск колонки — null; всё прочее — объектным элементом
# без col. Выбираем ту запись, которая КОРОЧЕ.
function ConvertTo-PositionalCells {
param($cells)
# Пропуск колонки — null. Выбираем ту запись, которая КОРОЧЕ: раньше позиционная форма
@@ -787,10 +788,12 @@ function ConvertTo-PositionalCells {
$span = if ($c.Contains("span")) { [int]$c["span"] } else { 1 }
$keys = @($c.Keys | Where-Object { $_ -notin @("col", "span") })
$plainText = ($keys.Count -eq 1 -and $keys[0] -eq "text" -and $c["text"] -is [string])
# Текст — единственное содержимое: пишем значением. Оно бывает строкой либо
# объектом «язык → текст», и в позиционной записи элемент И ЕСТЬ это значение.
$plainText = ($keys.Count -eq 1 -and $keys[0] -eq "text")
$plainParam = ($keys.Count -eq 1 -and $keys[0] -eq "param")
if ($plainText) { $out += $c["text"] }
if ($plainText) { $out += , $c["text"] }
elseif ($plainParam) { $out += "{$($c["param"])}" }
else {
$obj = [ordered]@{}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.15 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.16 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -791,7 +791,7 @@ def main():
def to_positional_cells(cells):
"""Позиционная запись списка ячеек: позиция берётся из порядка, `col` не пишется.
Ячейка, у которой кроме текста или параметра ничего нет, пишется строкой; span
Ячейка, у которой кроме текста или параметра ничего нет, пишется значением; span
раскрывается маркерами ">"; прочее — объектным элементом без col. Пропуск колонки —
null. Выбираем ту запись, которая КОРОЧЕ: раньше позиционная форма отбрасывалась,
как только первая ячейка стояла не в первой колонке, хотя один-два null впереди
@@ -809,7 +809,9 @@ def main():
expected += 1
span = int(c.get("span", 1) or 1)
keys = [k for k in c if k not in ("col", "span")]
plain_text = len(keys) == 1 and keys[0] == "text" and isinstance(c["text"], str)
# Текст — единственное содержимое: пишем значением. Оно бывает строкой либо
# объектом «язык → текст», и в позиционной записи элемент И ЕСТЬ это значение.
plain_text = len(keys) == 1 and keys[0] == "text"
plain_param = len(keys) == 1 and keys[0] == "param"
if plain_text:
out.append(c["text"])
@@ -1019,8 +1021,11 @@ def main():
dsl_row["cells"] = to_positional_cells(dsl_cells)
# Самая короткая из применимых форм: если у строки нет своих свойств, а список ячеек
# позиционный — строка пишется просто массивом, без ключа cells.
# Список позиционный, если позиция в нём НЕ записана: ни у одного элемента нет col.
# Прежняя проверка искала элемент-строку, поэтому строка из одних многоязычных
# текстов позиционной не признавалась и оставалась объектной.
if (len(dsl_row) == 1 and "cells" in dsl_row
and any(el is None or isinstance(el, str) for el in dsl_row["cells"])):
and not any(isinstance(el, dict) and "col" in el for el in dsl_row["cells"])):
area_rows.append(dsl_row["cells"])
else:
area_rows.append(dsl_row)
+4
View File
@@ -144,12 +144,16 @@
| Элемент | Значение |
|---------|----------|
| `"текст"` | Статический текст (`text`) |
| `{ "ru": "…", "en": "…" }` | Тот же текст на нескольких языках |
| `"{Имя}"` | Параметр (`param`) |
| `">"` | Продолжение ячейки слева — увеличивает её `span` |
| `"|"` | Продолжение ячейки сверху — увеличивает её `rowspan` |
| `null` | Пустая колонка: позиция занята, ячейка не создаётся |
| `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` |
Объект-элемент читается как ячейка, если несёт хоть один её ключ (`span`, `rowspan`, `style`,
`param`, `detail`, `text`, `template`), и как многоязычный текст в противном случае.
```json
"rows": [
["Вид", "Остаток", ">", "Итог"],