diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index c95ef155..88d4ca3f 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -59,7 +59,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J styles: { name: { font, horizontalAlignment, verticalAlignment, textPlacement, backColor, textColor, border, borderColor, format, hidden } }, areas: [{ name, columnSet, rows: [{ height, hidden, rowStyle, cells: [ - { col, span, rowspan, style, param, detail, text, template, valueType, controlType } + { col, span, rowspan, style, param, detail, text, template, valueType, controlType, value } ]}]}], namedAreas: [{ name, rows, cols }], columnSets: { name: { columns, columnWidths, columnStyles } } @@ -80,7 +80,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J - `col` — 1-based позиция колонки - `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) - Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]` -- `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, а `controlType: "checkbox"` меняет поле ввода на флажок +- `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, а `controlType: "checkbox"` меняет поле ввода на флажок; `value` — значение в поле Двухуровневая шапка массивами: ```json diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index 90a2f6f6..bbf10aa3 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -152,7 +152,8 @@ | `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` | Объект-элемент трактуется по его ключам: если среди них есть ключ ячейки (`span`, `rowspan`, -`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`) — объект описывает +`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`, `value`) — объект +описывает свойства ячейки. Иначе он целиком считается её текстом, а его ключи — идентификаторами языков. ```json @@ -184,6 +185,7 @@ | `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` | | `valueType` | нет | — | Тип значения: ячейка становится полем ввода (см. ниже) | | `controlType` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | +| `value` | нет | — | Значение в поле ввода. Только вместе с `valueType` | ### Содержимое ячейки @@ -248,6 +250,20 @@ Пустой тип `"valueType": ""` — «ячейка содержит значение», но тип не ограничен. +Значение поля ввода задаётся ключом `value`, и его тип выражается литералом JSON: строка — +строкой, число — числом, булево — `true`/`false`. Приведения к объявленному типу **не +происходит**: платформа хранит значение с его собственным типом и совпадать с объявленным он +не обязан (у ссылочного и составного типа значение всегда строковое). Пустая строка означает +пустое значение объявленного типа. Дата записывается строкой в формате `ГГГГ-ММ-ДДTчч:мм:сс` +и читается как дата только у ячейки, объявленной датой. + +```json +{ "col": 1, "valueType": "Number(15,3)", "value": 12.5 } +{ "col": 2, "valueType": "String(10)", "value": "5" } +{ "col": 3, "valueType": "Boolean", "value": true } +{ "col": 4, "valueType": "DateTime", "value": "" } +``` + `controlType` нужен редко: умолчание платформы — поле ввода, и оно применяется ко всем типам, включая `Boolean`. Флажок задаётся явно. Значение `"none"` (тега элемента управления нет вовсе) — форма раундтрипа, для ручного авторинга не нужна. @@ -266,8 +282,6 @@ DSL описывает не все конструкции табличного round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попадает, в сгенерированный XML не возвращается. -- начальное значение ячейки-поля ввода и сериализованные настройки её элемента управления - (тип значения при этом переживает цикл); - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 159b1b2e..cece0a02 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -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 } +# Значение ячейки-поля ввода — тег САМОЙ ячейки (), а не запись палитры: у двух ячеек +# с одинаковым оформлением значения разные, и в дедупликацию формата оно не входит. +# Тип значения берём из литерала 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$(Esc-XmlText $cellInfo.Value.Text)" + } + + # Настройки элемента управления платформа сериализует в base64 и держит у самой + # ячейки. Структуру не разбираем — возим дословно, иначе она теряется. + if ($null -ne $cellInfo.Control -and "$($cellInfo.Control)" -ne '') { + # Блоб платформа переносит по строкам, а X дописывает перевод строки сам — + # поэтому каждую строку блоба пишем отдельным вызовом, иначе перевод строки + # внутри значения разошёлся бы с остальным файлом. + $parts = @("$($cellInfo.Control)" -split "`r?`n") + $parts[0] = "`t`t`t`t`t$($parts[0])" + $parts[-1] = "$($parts[-1])" + foreach ($p in $parts) { X $p } + } + # Расшифровка от параметра заполнения не зависит: на корпусе 20 404 ячейки несут # её БЕЗ параметра (у текста, у пустой ячейки, у поля ввода) против 8 582 с ним. # Пока она стояла внутри ветки параметра, две трети расшифровок терялись молча. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 385ed8fb..fc3e993e 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -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): + """Значение ячейки-поля ввода — тег САМОЙ ячейки (), а не запись палитры: у двух ячеек + с одинаковым оформлением значения разные, и в дедупликацию формата оно не входит. + Тип значения берём из литерала 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{esc_xml_text(val["Text"])}') + + # Настройки элемента управления платформа сериализует в 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{parts[0]}' + parts[-1] = f'{parts[-1]}' + lines.extend(parts) + # Расшифровка от параметра заполнения не зависит: на корпусе 20 404 ячейки несут # её БЕЗ параметра (у текста, у пустой ячейки, у поля ввода) против 8 582 с ним. # Пока она стояла внутри ветки параметра, две трети расшифровок терялись молча. diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 137eb483..c82be858 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -1,4 +1,4 @@ -# mxl-decompile v1.21 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.22 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -92,6 +92,25 @@ $valueControlNames = @{ # значение» без указания типа — самостоятельное состояние. # Префикс ссылочного типа разрешаем по URI, а не по имени: локальное объявление платформа # называет сгенерированным dNpM. Чужое пространство имён оставляем как есть. +# Значение ячейки-поля ввода → литерал DSL. Тип значения выражается самим литералом: +# число → числом, булево → true/false, строка и дата → строкой. Пустое значение пишем +# как есть (0, false, пустая дата), а не схлопываем: так оно собирается обратно тем же +# типом независимо от того, какой тип объявлен у ячейки. +function ConvertTo-DslValue { + param([string]$xsiType, [string]$text) + if ($null -eq $text) { $text = '' } + if ($xsiType -ceq 'xs:boolean') { return ($text.Trim() -ceq 'true') } + if ($xsiType -ceq 'xs:decimal') { + $s = $text.Trim() + $i = 0; $d = 0.0 + if ($s -notlike '*.*' -and [int]::TryParse($s, [ref]$i)) { return $i } + if ([double]::TryParse($s, [System.Globalization.NumberStyles]::Float, + [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { return $d } + return $s + } + return $text +} + function Get-ValueQualifier { param($quals, [string]$kind, [string]$name, [string]$default = '') if (-not $quals.ContainsKey($kind)) { return $default } @@ -390,6 +409,22 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { $dNode = $cContent.SelectSingleNode("d:detailParameter", $ns) if ($dNode) { $detail = $dNode.InnerText } + # Значение ячейки-поля ввода. Тип значения свой, из объявленного не выводится, + # поэтому читаем и его. + $value = $null + $vNode = $cContent.SelectSingleNode("d:v", $ns) + if ($vNode) { + $value = @{ Type = $vNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance"); Text = $vNode.InnerText } + } + + # Настройки элемента управления — сериализованный base64 у самой ячейки. + # Структуру не разбираем, возим дословно. + $control = $null + $ctlNode = $cContent.SelectSingleNode("d:control", $ns) + # Переводы строк внутри блоба приводим к LF: XML-парсеры нормализуют их + # по-разному, и без этого порты давали разный JSON на одном файле. + if ($ctlNode -and $ctlNode.InnerText) { $control = $ctlNode.InnerText -replace "`r`n", "`n" } + # Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё # остальное терялось — в корпусе ERP 98% макетов держат текст и под ru, и под en. # Здесь собираем «язык → текст» как есть; свернётся в строку позже, когда станет @@ -423,6 +458,8 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { FormatIdx = $cellFmtIdx Param = $param Detail = $detail + Value = $value + Control = $control Text = $text HasText = $hasText } @@ -1135,6 +1172,10 @@ foreach ($area in $blocks) { # форматах корпуса. if ($name -cne 'input') { $dslCell["controlType"] = $name } } + if ($null -ne $cell.Value) { + $dslCell["value"] = ConvertTo-DslValue $cell.Value.Type $cell.Value.Text + } + if ($cell.Control) { $dslCell["control"] = $cell.Control } } # Content diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 9e330249..6ae66365 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-decompile v1.21 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.22 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -187,6 +187,23 @@ def value_type_to_dsl(node): return ' + '.join(parts) +def value_to_dsl(xsi_type, text): + """Значение ячейки-поля ввода → литерал DSL. Тип значения выражается самим литералом: + число → числом, булево → true/false, строка и дата → строкой. Пустое значение пишем + как есть (0, false, пустая дата), а не схлопываем: так оно собирается обратно тем же + типом независимо от того, какой тип объявлен у ячейки.""" + text = text or '' + if xsi_type == 'xs:boolean': + return text.strip() == 'true' + if xsi_type == 'xs:decimal': + s = text.strip() + try: + return int(s) if '.' not in s else float(s) + except ValueError: + return s + return text + + def int_of(node, default=0): if node is not None and node.text: return int(node.text) @@ -556,6 +573,22 @@ def main(): if d_node is not None and d_node.text: detail = d_node.text + # Значение ячейки-поля ввода. Тип значения свой, из объявленного не выводится, + # поэтому читаем и его: пустое значение сворачивается в "", остальные едут как есть. + value = None + v_node = find(c_content, "d:v") + if v_node is not None: + value = (v_node.get(f'{{{XSI_NS}}}type') or '', (v_node.text or '')) + + # Настройки элемента управления — сериализованный base64 у самой ячейки. + # Структуру не разбираем, возим дословно. + control = None + ctl_node = find(c_content, "d:control") + if ctl_node is not None and ctl_node.text: + # Переводы строк внутри блоба приводим к LF: XML-парсеры нормализуют их + # по-разному, и без этого порты давали разный JSON на одном файле. + control = ctl_node.text.replace('\r\n', '\n') + # Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё # остальное терялось — в корпусе ERP 98% макетов держат текст и под ru, и под en. # Здесь собираем «язык → текст» как есть; свернётся в строку позже, когда станет @@ -587,6 +620,8 @@ def main(): "FormatIdx": cell_fmt_idx, "Param": param, "Detail": detail, + "Value": value, + "Control": control, "Text": text, "HasText": has_text, }) @@ -1128,6 +1163,10 @@ def main(): # форматах корпуса. if name != "input": dsl_cell["controlType"] = name + if cell["Value"] is not None: + dsl_cell["value"] = value_to_dsl(*cell["Value"]) + if cell["Control"]: + dsl_cell["control"] = cell["Control"] # Content fill_type = cell_fmt["FillType"] if cell_fmt else "" diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 21b44a8e..76d9cf0c 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -152,7 +152,8 @@ | `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` | Объект-элемент трактуется по его ключам: если среди них есть ключ ячейки (`span`, `rowspan`, -`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`) — объект описывает +`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`, `value`) — объект +описывает свойства ячейки. Иначе он целиком считается её текстом, а его ключи — идентификаторами языков. ```json @@ -184,6 +185,7 @@ | `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` | | `valueType` | нет | — | Тип значения: ячейка становится полем ввода (см. ниже) | | `controlType` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | +| `value` | нет | — | Значение в поле ввода. Только вместе с `valueType` | ### Содержимое ячейки @@ -248,6 +250,20 @@ Пустой тип `"valueType": ""` — «ячейка содержит значение», но тип не ограничен. +Значение поля ввода задаётся ключом `value`, и его тип выражается литералом JSON: строка — +строкой, число — числом, булево — `true`/`false`. Приведения к объявленному типу **не +происходит**: платформа хранит значение с его собственным типом и совпадать с объявленным он +не обязан (у ссылочного и составного типа значение всегда строковое). Пустая строка означает +пустое значение объявленного типа. Дата записывается строкой в формате `ГГГГ-ММ-ДДTчч:мм:сс` +и читается как дата только у ячейки, объявленной датой. + +```json +{ "col": 1, "valueType": "Number(15,3)", "value": 12.5 } +{ "col": 2, "valueType": "String(10)", "value": "5" } +{ "col": 3, "valueType": "Boolean", "value": true } +{ "col": 4, "valueType": "DateTime", "value": "" } +``` + `controlType` нужен редко: умолчание платформы — поле ввода, и оно применяется ко всем типам, включая `Boolean`. Флажок задаётся явно. Значение `"none"` (тега элемента управления нет вовсе) — форма раундтрипа, для ручного авторинга не нужна. @@ -266,8 +282,6 @@ DSL описывает не все конструкции табличного round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попадает, в сгенерированный XML не возвращается. -- начальное значение ячейки-поля ввода и сериализованные настройки её элемента управления - (тип значения при этом переживает цикл); - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/tests/skills/cases/mxl-compile/error-value-without-type.json b/tests/skills/cases/mxl-compile/error-value-without-type.json new file mode 100644 index 00000000..70281c0c --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-without-type.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: value без valueType", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "value": "5" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Cell 'value' requires 'valueType' (area \"Ввод\")" +} diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml index 089de07d..b89825ed 100644 --- a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml @@ -112,10 +112,129 @@ + + 7 + + + + 3 + 5 + + + + + 2 + 8 + + + + + + 8 + + + + 3 + 4 5 + + + + + 2 + 0 + + + + + 12 + 0001-01-01T00:00:00 + + + + + + 9 + + + + 13 + 4 5 + + + + + 12 + 0001-01-01T00:00:00 + + + + + 4 + true + + + + + + 10 + + + + 14 + Составной + + + + + 2 + 12.5 + + + + + 2 + -3 + + + + + + 11 + + + + 15 + + + + + 12 + 0001-01-01T00:00:00 + + + + + 3 + 77u/MiwxLDM4MWVkNjI0LTkyMTctNGU2My04NWRiLWM0YzNjYjg3ZGFhZSwNCnsN +Cns5LA0KeyJQYXR0ZXJuIiwNCnsiUyJ9DQp9LA0Kew0Kew0KezE2LDEsDQp7Myw0 +LA0KezB9DQp9LA0KezMsNCwNCnswfQ0KfSwNCns3LDMsMCwxLDEwMH0sMCwNCnsz +LDQsDQp7MH0NCn0sDQp7Myw0LA0KezB9DQp9LA0KezMsNCwNCnswfQ0KfSwNCnsz +LDMsDQp7LTd9DQp9LA0KezMsMywNCnstMjF9DQp9LA0KezMsMSwNCnstMTh9LDAs +MCwwfSwNCnsxLDB9LDAsMCwxMDAsMCwwfSwzMCwwLDAsMSwwLDAsMSwxLDEsMCwx +LDAsMCwwLDAsMCw0LDAsDQp7IlUifSwNCnsiVSJ9LCIiLDAsMSwwLDAsMCwwLA0K +ezQsMCwNCnswfSwiIiwtMSwtMSwxLDAsIiJ9LA0KezQsMCwNCnswfSwiIiwtMSwt +MSwxLDAsIiJ9LDAsMCwwLA0KezAsMCwwfSwNCnsxLDB9LDAsMCwwLDAsMCwwLDAs +MTY3NzcyMTUsMn0NCn0sDQp7MSwNCns5YTc2NDNkMi0xOWU5LTQ1ZTItODg5My0y +ODBiYzkxOTVhOTcsDQp7NCwNCnsiVSJ9LA0KeyJVIn0sMCwiIiwwLDB9DQp9DQp9 +LA0KezB9LDAsMSwwLA0KezEsMH0sMH0NCn0sMA== + + + + true - 12 - 7 - 7 + 16 + 12 + 12 true @@ -232,6 +351,57 @@ 381ed624-9217-4e63-85db-c4c3cb87daae + + true + + xs:dateTime + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:CatalogRef.Бригады + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + xs:string + xs:dateTime + xs:decimal + + 0 + 0 + Any + + + 0 + Variable + + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 0 + 0 + Any + + + 35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26 + 72 diff --git a/tests/skills/cases/mxl-compile/snapshots/value-content/Template.xml b/tests/skills/cases/mxl-compile/snapshots/value-content/Template.xml new file mode 100644 index 00000000..61341b05 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/value-content/Template.xml @@ -0,0 +1,225 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 1 + 5 + + + + + 2 + 12.5 + + + + + 3 + -3 + + + + + + 1 + + + + 4 + true + + + + + 4 + false + + + + + 5 + 2026-08-09T13:45:00 + + + + + + 2 + + + + 6 + + + + + + 7 + 0 + + + + + 5 + 0001-01-01T00:00:00 + + + + + + 3 + + + + 8 + 4 5 + + + + + 9 + Составной + + + + + 3 + 8 + Расшифровка + + + + + true + 10 + 4 + 4 + + Значения + + Rows + 0 + 3 + -1 + -1 + + + + true + + xs:string + + 10 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 15 + 3 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 10 + 2 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:dateTime + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 0 + 0 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:CatalogRef.Валюты + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + xs:string + xs:decimal + + 0 + 0 + Any + + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/value-content.json b/tests/skills/cases/mxl-compile/value-content.json new file mode 100644 index 00000000..40c33416 --- /dev/null +++ b/tests/skills/cases/mxl-compile/value-content.json @@ -0,0 +1,37 @@ +{ + "name": "Ячейки-поля ввода: сохранённое значение", + "input": { + "columns": 3, + "areas": [ + { + "name": "Значения", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "String(10)", "value": "5" }, + { "col": 2, "valueType": "Number(15,3)", "value": 12.5 }, + { "col": 3, "valueType": "Number(10,2)", "value": -3 } + ]}, + { "cells": [ + { "col": 1, "valueType": "Boolean", "value": true }, + { "col": 2, "valueType": "Boolean", "value": false }, + { "col": 3, "valueType": "DateTime", "value": "2026-08-09T13:45:00" } + ]}, + { "cells": [ + { "col": 1, "valueType": "String", "value": "" }, + { "col": 2, "valueType": "Number", "value": "" }, + { "col": 3, "valueType": "DateTime", "value": "" } + ]}, + { "cells": [ + { "col": 1, "valueType": "CatalogRef.Валюты", "value": "4 5" }, + { "col": 2, "valueType": "Boolean + String + Number", "value": "Составной" }, + { "col": 3, "valueType": "Number(10,2)", "value": 8, "detail": "Расшифровка" } + ]} + ] + } + ] + }, + "params": { + "outputPath": "Template.xml" + }, + "skipPlatformVerify": "ссылочный тип разрешается только в базе с матч-конфигурацией: временная база-заглушка таких объектов не содержит" +} diff --git a/tests/skills/cases/mxl-decompile/roundtrip-value-content.json b/tests/skills/cases/mxl-decompile/roundtrip-value-content.json new file mode 100644 index 00000000..69c29035 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-value-content.json @@ -0,0 +1,41 @@ +{ + "name": "Roundtrip — сохранённые значения ячеек-полей ввода", + "preRun": [ + { + "script": "mxl-compile/scripts/mxl-compile", + "input": { + "columns": 3, + "areas": [ + { + "name": "Значения", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "String(10)", "value": "5" }, + { "col": 2, "valueType": "Number(15,3)", "value": 12.5 }, + { "col": 3, "valueType": "Number(10,2)", "value": -3 } + ]}, + { "cells": [ + { "col": 1, "valueType": "Boolean", "value": true }, + { "col": 2, "valueType": "DateTime", "value": "2026-08-09T13:45:00" }, + { "col": 3, "valueType": "DateTime", "value": "" } + ]}, + { "cells": [ + { "col": 1, "valueType": "String", "value": "" }, + { "col": 2, "valueType": "Number", "value": "" }, + { "col": 3, "valueType": "Boolean + String + Number", "value": "Составной" } + ]} + ] + } + ] + }, + "args": { "-JsonPath": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" + } + ], + "params": { "templatePath": "Template.xml" }, + "args_extra": ["-OutputPath", "{workDir}/back.json"], + "expect": { + "files": ["back.json"], + "stdoutContains": "[OK] Decompiled:" + } +} diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/Template.xml new file mode 100644 index 00000000..6fb437df --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/Template.xml @@ -0,0 +1,194 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 1 + 5 + + + + + 2 + 12.5 + + + + + 3 + -3 + + + + + + 1 + + + + 4 + true + + + + + 5 + 2026-08-09T13:45:00 + + + + + 5 + 0001-01-01T00:00:00 + + + + + + 2 + + + + 6 + + + + + + 7 + 0 + + + + + 8 + Составной + + + + + true + 9 + 3 + 3 + + Значения + + Rows + 0 + 2 + -1 + -1 + + + + true + + xs:string + + 10 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 15 + 3 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 10 + 2 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:dateTime + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 0 + 0 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + xs:string + xs:decimal + + 0 + 0 + Any + + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/back.json new file mode 100644 index 00000000..1dd253ca --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-content/back.json @@ -0,0 +1,16 @@ +{ + "columns": 3, + "defaultWidth": 10, + "fonts": {}, + "styles": {}, + "areas": [ + { + "name": "Значения", + "rows": [ + [{ "valueType": "String(10)", "value": "5" }, { "valueType": "Number(15,3)", "value": 12.5 }, { "valueType": "Number(10,2)", "value": -3 }], + [{ "valueType": "Boolean", "value": true }, { "valueType": "DateTime", "value": "2026-08-09T13:45:00" }, { "valueType": "DateTime", "value": "0001-01-01T00:00:00" }], + [{ "valueType": "String", "value": "" }, { "valueType": "Number", "value": 0 }, { "valueType": "Boolean + String + Number", "value": "Составной" }] + ] + } + ] +} \ No newline at end of file