diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index adc1486f..3c2a6d8a 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 } + { col, span, rowspan, style, param, detail, text, template, valueType, control } ]}]}], namedAreas: [{ name, rows, cols }], columnSets: { name: { columns, columnWidths, columnStyles } } @@ -80,6 +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.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, а `control: "checkbox"` меняет поле ввода на флажок Двухуровневая шапка массивами: ```json diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index ef379cf3..97dc4554 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -182,6 +182,8 @@ | `detail` | нет | — | Параметр расшифровки (только с `param`) | | `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже | | `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` | +| `valueType` | нет | — | Тип значения: ячейка становится полем ввода (см. ниже) | +| `control` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | ### Содержимое ячейки @@ -212,6 +214,44 @@ Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста. +## Ячейка-поле ввода + +Ячейка может не показывать текст, а принимать значение от пользователя — так делают макеты для +ввода данных. Достаточно задать тип значения: + +```json +{ "col": 1, "valueType": "Number(15,3,nonneg)" } +{ "col": 2, "valueType": "String(10)" } +{ "col": 3, "valueType": "Boolean", "control": "checkbox" } +{ "col": 4, "valueType": "CatalogRef.Валюты" } +{ "col": 5, "valueType": "Boolean + String + Date + CatalogRef.Валюты" } +{ "col": 6, "valueType": "AnyRef" } +``` + +Грамматика типа: + +| Запись | Значение | +|---|---| +| `Boolean` | булево | +| `String`, `String(10)`, `String(10,fixed)` | строка; без длины — неограниченная, `fixed` — фиксированной длины | +| `Number`, `Number(15,3)`, `Number(15,3,nonneg)` | число; без параметров — без ограничения разрядности, `nonneg` — неотрицательное | +| `Date`, `DateTime`, `Time` | дата, дата со временем, время | +| `CatalogRef.Валюты`, `DocumentRef.Реализация`, `EnumRef.Статусы`, `DefinedType.Сумма` | ссылочный тип | +| `CatalogRef`, `DocumentRef`, `AnyRef` | категория целиком: любая ссылка этого вида | +| `A + B` | составной тип; повтор одного примитива недопустим | + +Русские имена принимаются наравне с английскими: `Число(15,3)`, `Строка(10)`, +`СправочникСсылка.Валюты`. + +`text` и `template` в такой ячейке недопустимы — платформа не показывает текст там, где вводится +значение. `param` и `detail` допустимы: они описывают заполнение и расшифровку, а не содержимое. + +Пустой тип `"valueType": ""` — «ячейка содержит значение», но тип не ограничен. + +`control` нужен редко: умолчание платформы — поле ввода, и оно применяется ко всем типам, включая +`Boolean`. Флажок задаётся явно. Значение `"none"` (тега элемента управления нет вовсе) — форма +раундтрипа, для ручного авторинга не нужна. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -226,7 +266,8 @@ DSL описывает не все конструкции табличного round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попадает, в сгенерированный XML не возвращается. -- ячейки-поля ввода (`containsValue` / `valueType` / `controlType`); +- начальное значение ячейки-поля ввода и сериализованные настройки её элемента управления + (тип значения при этом переживает цикл); - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/.claude/skills/mxl-compile/reference/format-properties.md b/.claude/skills/mxl-compile/reference/format-properties.md index cfab577e..89a81394 100644 --- a/.claude/skills/mxl-compile/reference/format-properties.md +++ b/.claude/skills/mxl-compile/reference/format-properties.md @@ -74,4 +74,4 @@ - `fillType` — выводится из того, каким ключом задано содержимое ячейки (`text` / `param` / `template`); - `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего - оформления; сейчас не поддерживаются. + оформления: задаются ключами ячейки `valueType` и `control`. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 6ed48953..233a00b8 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.40 — Compile 1C spreadsheet from JSON +# mxl-compile v1.41 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -621,6 +621,270 @@ function Resolve-Style { return $props } +# --- 5.9. Ячейка-поле ввода: тип значения и элемент управления --- +# containsValue/valueType/controlType платформа держит в записи палитры формата, но принадлежат +# они конкретной ЯЧЕЙКЕ: на корпусе ERP 370 197 ссылок на такие записи — все из , ни одной +# из строки, колонки или defaultFormatIndex. Поэтому в DSL это ключи ячейки, а не стиля. + +# Прощающий ввод типа — общая семья Resolve-TypeStr (эталон meta-compile). Словарь синонимов +# у каждого навыка свой: здесь только то, что бывает значением ячейки макета. +$script:typeSynonyms = @{} +$script:typeSynonyms["число"] = "Number" +$script:typeSynonyms["строка"] = "String" +$script:typeSynonyms["булево"] = "Boolean" +$script:typeSynonyms["дата"] = "Date" +$script:typeSynonyms["датавремя"] = "DateTime" +$script:typeSynonyms["время"] = "Time" +$script:typeSynonyms["number"] = "Number" +$script:typeSynonyms["string"] = "String" +$script:typeSynonyms["boolean"] = "Boolean" +$script:typeSynonyms["bool"] = "Boolean" +$script:typeSynonyms["date"] = "Date" +$script:typeSynonyms["datetime"] = "DateTime" +$script:typeSynonyms["time"] = "Time" +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:typeSynonyms["задачассылка"] = "TaskRef" +$script:typeSynonyms["любаяссылка"] = "AnyRef" +$script:typeSynonyms["catalogref"] = "CatalogRef" +$script:typeSynonyms["documentref"] = "DocumentRef" +$script:typeSynonyms["enumref"] = "EnumRef" +$script:typeSynonyms["anyref"] = "AnyRef" +$script:typeSynonyms["определяемыйтип"] = "DefinedType" +$script:typeSynonyms["definedtype"] = "DefinedType" + +# Голые метатипы-категории («любой объект категории») — множество, а не тип: . +$script:valueTypeSets = @('CatalogRef', 'DocumentRef', 'EnumRef', 'ChartOfAccountsRef', + 'ChartOfCharacteristicTypesRef', 'ChartOfCalculationTypesRef', 'ExchangePlanRef', + 'BusinessProcessRef', 'TaskRef', 'AnyRef', 'AnyIBRef') +# Категории ссылочных типов с именем объекта — . +$script:valueRefKinds = @('CatalogRef', 'DocumentRef', 'EnumRef', 'ChartOfAccountsRef', + 'ChartOfCharacteristicTypesRef', 'ChartOfCalculationTypesRef', 'ExchangePlanRef', + 'BusinessProcessRef', 'BusinessProcessRoutePointRef', 'TaskRef') + +# Элемент управления. В выгрузке — GUID, имён у платформы в XML нет вовсе, поэтому канон DSL +# придуман: input и checkbox. Умолчание платформы — поле ввода для ВСЕХ типов, включая Булево +# (корпус: 63 629 форматов против 7 у флажка), поэтому input не пишем в XML как «по умолчанию», +# а эмитим всегда — так делает платформа. +$script:valueControlGuids = @{ + 'input' = '381ed624-9217-4e63-85db-c4c3cb87daae' + 'checkbox' = '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26' +} +$script:valueControlSynonyms = @{ + # Ключи нормализованы: без пробелов, в нижнем регистре («Поле ввода» → «полеввода»). + 'полеввода' = 'input'; 'inputfield' = 'input' + 'полефлажка' = 'checkbox'; 'флажок' = 'checkbox'; 'checkboxfield' = 'checkbox' +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает + # поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает + # «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию. + # Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса + # платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой: + # сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument, + # d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения. + if ($typeStr.StartsWith('cfg:')) { + $typeStr = $typeStr.Substring(4) + } elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') { + $typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1) + } + + # Параметризованные типы: Number(15,2), Строка(100) + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + # Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) # includes the dot + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + # Простое имя + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + return $typeStr +} + +# Канон типа: развёрнутая запись со всеми умолчаниями. Она же ложится в ключ дедупликации +# палитры, поэтому «Число(15,3)» и «Number(15,3)» обязаны дать ОДНУ запись формата — иначе +# палитра распухнет на дубли и все ссылки уедут. +# Умолчания здесь ПЛАТФОРМЕННЫЕ, а не как у реквизита метаданных: голая строка — безлимитная +# (Length 0), голое число — 0,0 (проверено на макетах, собранных Конфигуратором). +function Get-CanonValueType { + param([string]$typeStr, [string]$where) + $parts = @() + $seen = @{} + foreach ($raw in ($typeStr -split '[+|]')) { + $p = Resolve-TypeStr $raw.Trim() + if (-not $p) { continue } + $canon = $null + $kind = $null + if ($p -ceq 'Boolean') { + $canon = 'Boolean'; $kind = 'Boolean' + } elseif ($p -match '^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { '0' } + $al = if ($Matches[4] -and $Matches[4].ToLowerInvariant() -eq 'fixed') { 'fixed' } else { 'variable' } + $canon = "String($len,$al)"; $kind = 'String' + } elseif ($p -match '^Number(\((\d+)(\s*,\s*(\d+))?(\s*,\s*(nonneg|any))?\))?$') { + $digits = if ($Matches[2]) { $Matches[2] } else { '0' } + $fraction = if ($Matches[4]) { $Matches[4] } else { '0' } + $sign = if ($Matches[6] -and $Matches[6].ToLowerInvariant() -eq 'nonneg') { 'nonneg' } else { 'any' } + $canon = "Number($digits,$fraction,$sign)"; $kind = 'Number' + } elseif ($p -ceq 'Date' -or $p -ceq 'DateTime' -or $p -ceq 'Time') { + $canon = $p; $kind = 'Date' + } elseif ($p -match '^(DefinedType|Characteristic)\.(.+)$') { + $canon = $p; $kind = "set:$p" + } elseif ($p.Contains('.')) { + $kindName = $p.Substring(0, $p.IndexOf('.')) + if ($script:valueRefKinds -ccontains $kindName) { + $canon = $p; $kind = "ref:$p" + } else { + [Console]::Error.WriteLine("Unknown value type `"$($raw.Trim())`" ($where). Reference kinds: $($script:valueRefKinds -join ', ')") + exit 1 + } + } elseif ($p.Contains(':')) { + # Чужое пространство имён — пишем как есть: калечить его мы не вправе. + $canon = $p; $kind = "raw:$p" + } elseif ($script:valueTypeSets -ccontains $p) { + $canon = $p; $kind = "set:$p" + } else { + [Console]::Error.WriteLine("Unknown value type `"$($raw.Trim())`" ($where). Expected: Boolean, String(N), Number(D,F), Date, Ref. or a bare metatype") + exit 1 + } + # Повтор одного вида в составном типе платформа выразить не может: блок квалификаторов + # внутри один на вид. + if ($seen.ContainsKey($kind)) { + [Console]::Error.WriteLine("Duplicate type `"$canon`" in composite value type ($where)") + exit 1 + } + $seen[$kind] = $true + $parts += $canon + } + if ($parts.Count -eq 0) { + [Console]::Error.WriteLine("Empty value type ($where)") + exit 1 + } + return ($parts -join ' + ') +} + +# Содержимое по КАНОНУ — умолчаний здесь нет, они уже развёрнуты. Порядок снят +# с платформы: сначала ВСЕ / в порядке источника, затем блоки +# квалификаторов Number → String → Date. Ссылочные типы несут ЛОКАЛЬНОЕ объявление xmlns +# на каждом узле — в макете префикс current-config всегда d4p1. +function Emit-ValueTypeContent { + param([string]$indent, [string]$canonType) + $cfgNs = 'http://v8.1c.ru/8.1/data/enterprise/current-config' + $typeLines = @() + $quals = @{} + foreach ($p in ($canonType -split ' \+ ')) { + if ($p -ceq 'Boolean') { + $typeLines += "$indentxs:boolean" + } elseif ($p -match '^String\((\d+),(fixed|variable)\)$') { + $len = $Matches[1] + $al = if ($Matches[2] -ceq 'fixed') { 'Fixed' } else { 'Variable' } + $typeLines += "$indentxs:string" + $quals['String'] = @( + "$indent", + "$indent`t$len", + "$indent`t$al", + "$indent") + } elseif ($p -match '^Number\((\d+),(\d+),(nonneg|any)\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3] -ceq 'nonneg') { 'Nonnegative' } else { 'Any' } + $typeLines += "$indentxs:decimal" + $quals['Number'] = @( + "$indent", + "$indent`t$digits", + "$indent`t$fraction", + "$indent`t$sign", + "$indent") + } elseif ($p -ceq 'Date' -or $p -ceq 'DateTime' -or $p -ceq 'Time') { + $typeLines += "$indentxs:dateTime" + $quals['Date'] = @( + "$indent", + "$indent`t$p", + "$indent") + } elseif ($p -match '^(DefinedType|Characteristic)\.') { + $typeLines += "$indentd4p1:$p" + } elseif ($p.Contains('.')) { + $typeLines += "$indentd4p1:$p" + } elseif ($p.Contains(':')) { + $typeLines += "$indent$p" + } else { + $typeLines += "$indentd4p1:$p" + } + } + foreach ($l in $typeLines) { X $l } + foreach ($q in @('Number', 'String', 'Date')) { + if ($quals.ContainsKey($q)) { foreach ($l in $quals[$q]) { X $l } } + } +} + +# Свойства значения ячейки — пустой набор, если ячейка обычная. Считаются ОДНОЙ функцией: +# пре-проход регистрации палитры и генерация обязаны получить один и тот же набор, иначе +# состав палитры разойдётся и все ссылки сдвинутся. +function Get-CellValueProps { + param($cell, [string]$where) + $props = @{} + $vt = $cell.valueType + $ctl = $cell.control + if ($null -eq $vt) { + if ($null -ne $ctl -and "$ctl" -ne '') { + [Console]::Error.WriteLine("Cell 'control' requires 'valueType' ($where)") + exit 1 + } + return $props + } + # Ячейка, содержащая значение, текста не несёт: платформа этого не допускает, и в корпусе + # нет ни одной такой ячейки из 370 197. + if ($null -ne $cell.text -or $null -ne $cell.template) { + [Console]::Error.WriteLine("Cell with 'valueType' cannot have 'text' or 'template' ($where)") + exit 1 + } + $props['containsValue'] = 'true' + # Пустая строка — третье состояние: есть, тип не задан (). + $props['valueType'] = if ("$vt" -eq '') { '' } else { Get-CanonValueType "$vt" $where } + + $name = 'input' + if ($null -ne $ctl -and "$ctl" -ne '') { + $norm = ("$ctl" -replace '\s', '').ToLowerInvariant() + if ($script:valueControlSynonyms.ContainsKey($norm)) { $norm = $script:valueControlSynonyms[$norm] } + $name = $norm + } + if ($name -ceq 'none') { return $props } # формат вовсе без + if ($script:valueControlGuids.ContainsKey($name)) { + $props['controlType'] = $script:valueControlGuids[$name] + } elseif ($name -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { + # Неизвестный элемент управления сохраняем как есть — терять его нельзя. + $props['controlType'] = $name + } else { + [Console]::Error.WriteLine("Unknown 'control' value `"$ctl`" ($where). Allowed: input, checkbox, none or a GUID") + exit 1 + } + return $props +} + # --- 6. Format palette builder --- $formatRegistry = [ordered]@{} # key -> hashtable with properties @@ -843,8 +1107,13 @@ function Get-FillType { # Helper: register a cell format and return its index function Register-CellFormat { - param($styleName, [string]$fillType) + param($styleName, [string]$fillType, [hashtable]$valueProps) $resolved = Resolve-Style -styleName $styleName -fillType $fillType + # Свойства значения кладём ПОВЕРХ стиля — до проверки на пустоту: у ячейки-поля ввода + # оформления может не быть вовсе, и без слияния она получила бы 0, потеряв значение. + if ($valueProps) { + foreach ($k in $valueProps.Keys) { $resolved[$k] = $valueProps[$k] } + } # У ячейки без собственного оформления формата НЕТ вовсе: 0, где ноль — не индекс # записи, а «формата нет». В корпусе так у 170 710 ячеек против 50 635, ссылающихся на # формат по умолчанию; 0 встречается в 71% макетов. @@ -870,7 +1139,8 @@ function Set-CellProp { # это ru, en, ru1, Русский. function Test-CellObject { param($el) - $cellKeys = @('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template') + $cellKeys = @('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template', + 'valueType', 'control') foreach ($p in $el.PSObject.Properties) { if ($cellKeys -contains $p.Name) { return $true } } @@ -1051,7 +1321,8 @@ foreach ($area in $def.areas) { foreach ($cell in $row.cells) { $cellStyle = if ($cell.style) { $cell.style } elseif ($cellsStyle) { $cellsStyle } else { "default" } $ft = Get-FillType $cell - Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null + $vp = Get-CellValueProps $cell "area `"$($area.name)`"" + Register-CellFormat -styleName $cellStyle -fillType $ft -valueProps $vp | Out-Null } } } @@ -1332,7 +1603,8 @@ foreach ($area in $def.areas) { $rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 } $cellStyle = if ($cell.style) { $cell.style } elseif ($cellsStyle) { $cellsStyle } else { "default" } $ft = Get-FillType $cell - $fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft + $vp = Get-CellValueProps $cell "area `"$areaName`", row $($localRow + 1)" + $fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft -valueProps $vp $cellInfo = @{ Col = $colStart - 1 # 0-based @@ -1659,6 +1931,15 @@ foreach ($key in $formatRegistry.Keys) { X "`t`t`t`t$(Esc-XmlText $val)" X "`t`t`t" X "`t`t" + } elseif ($tag -ceq 'valueType') { + # Пустой тип — самостоятельное состояние: «содержит значение», тип не задан. + if ("$val" -eq '') { + X "`t`t" + } else { + X "`t`t" + Emit-ValueTypeContent "`t`t`t" "$val" + X "`t`t" + } } elseif ($script:formatTagKind[$tag] -ceq 'color' -and (Get-ColorNamespace "$val")) { # web/win-палитры в корне документа не объявлены — платформа дописывает объявление # прямо на узел и пишет значение с этим префиксом. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 4c651d88..e1a6b697 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.39 — Compile 1C spreadsheet from JSON +# mxl-compile v1.41 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -439,6 +439,251 @@ def get_format_key(props): return '|'.join(parts) +# --- Ячейка-поле ввода: тип значения и элемент управления --- +# containsValue/valueType/controlType платформа держит в записи палитры формата, но принадлежат +# они конкретной ЯЧЕЙКЕ: на корпусе ERP 370 197 ссылок на такие записи — все из , ни одной +# из строки, колонки или defaultFormatIndex. Поэтому в DSL это ключи ячейки, а не стиля. + +# Прощающий ввод типа — общая семья resolve_type_str (эталон meta-compile). Словарь синонимов +# у каждого навыка свой: здесь только то, что бывает значением ячейки макета. +TYPE_SYNONYMS = { + 'число': 'Number', 'строка': 'String', 'булево': 'Boolean', + 'дата': 'Date', 'датавремя': 'DateTime', 'время': 'Time', + 'number': 'Number', 'string': 'String', 'boolean': 'Boolean', 'bool': 'Boolean', + 'date': 'Date', 'datetime': 'DateTime', 'time': 'Time', + 'справочникссылка': 'CatalogRef', + 'документссылка': 'DocumentRef', + 'перечислениессылка': 'EnumRef', + 'плансчетовссылка': 'ChartOfAccountsRef', + 'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypesRef', + 'планвидоврасчётассылка': 'ChartOfCalculationTypesRef', + 'планвидоврасчетассылка': 'ChartOfCalculationTypesRef', + 'планобменассылка': 'ExchangePlanRef', + 'бизнеспроцессссылка': 'BusinessProcessRef', + 'задачассылка': 'TaskRef', + 'любаяссылка': 'AnyRef', + 'catalogref': 'CatalogRef', 'documentref': 'DocumentRef', 'enumref': 'EnumRef', + 'anyref': 'AnyRef', + 'определяемыйтип': 'DefinedType', 'definedtype': 'DefinedType', +} + +# Голые метатипы-категории («любой объект категории») — множество, а не тип: . +VALUE_TYPE_SETS = ('CatalogRef', 'DocumentRef', 'EnumRef', 'ChartOfAccountsRef', + 'ChartOfCharacteristicTypesRef', 'ChartOfCalculationTypesRef', 'ExchangePlanRef', + 'BusinessProcessRef', 'TaskRef', 'AnyRef', 'AnyIBRef') +# Категории ссылочных типов с именем объекта — . +VALUE_REF_KINDS = ('CatalogRef', 'DocumentRef', 'EnumRef', 'ChartOfAccountsRef', + 'ChartOfCharacteristicTypesRef', 'ChartOfCalculationTypesRef', 'ExchangePlanRef', + 'BusinessProcessRef', 'BusinessProcessRoutePointRef', 'TaskRef') + +# Элемент управления. В выгрузке — GUID, имён у платформы в XML нет вовсе, поэтому канон DSL +# придуман: input и checkbox. Умолчание платформы — поле ввода для ВСЕХ типов, включая Булево +# (корпус: 63 629 форматов против 7 у флажка), поэтому input не пишем в XML как «по умолчанию», +# а эмитим всегда — так делает платформа. +VALUE_CONTROL_GUIDS = { + 'input': '381ed624-9217-4e63-85db-c4c3cb87daae', + 'checkbox': '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26', +} +VALUE_CONTROL_SYNONYMS = { + # Ключи нормализованы: без пробелов, в нижнем регистре («Поле ввода» → «полеввода»). + 'полеввода': 'input', 'inputfield': 'input', + 'полефлажка': 'checkbox', 'флажок': 'checkbox', 'checkboxfield': 'checkbox', +} + +CFG_NS = 'http://v8.1c.ru/8.1/data/enterprise/current-config' + + +def resolve_type_str(type_str): + if not type_str: + return type_str + + # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает + # поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает + # «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию. + # Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса + # платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой: + # сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument, + # d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения. + if type_str.startswith('cfg:'): + type_str = type_str[4:] + elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str): + type_str = type_str[type_str.index(':') + 1:] + + # Параметризованные типы: Number(15,2), Строка(100) + m = re.match(r'^([^(]+)\((.+)\)$', type_str) + if m: + base_name = m.group(1).strip() + params = m.group(2) + resolved = TYPE_SYNONYMS.get(base_name.lower()) + if resolved: + return f'{resolved}({params})' + return type_str + + # Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации + if '.' in type_str: + dot_idx = type_str.index('.') + prefix = type_str[:dot_idx] + suffix = type_str[dot_idx:] # includes the dot + resolved = TYPE_SYNONYMS.get(prefix.lower()) + if resolved: + return f'{resolved}{suffix}' + return type_str + + # Простое имя + resolved = TYPE_SYNONYMS.get(type_str.lower()) + if resolved: + return resolved + return type_str + + +def canon_value_type(type_str, where): + """Канон типа: развёрнутая запись со всеми умолчаниями. Она же ложится в ключ дедупликации + палитры, поэтому «Число(15,3)» и «Number(15,3)» обязаны дать ОДНУ запись формата — иначе + палитра распухнет на дубли и все ссылки уедут. + Умолчания здесь ПЛАТФОРМЕННЫЕ, а не как у реквизита метаданных: голая строка — безлимитная + (Length 0), голое число — 0,0 (проверено на макетах, собранных Конфигуратором).""" + parts = [] + seen = set() + for raw in re.split(r'[+|]', type_str): + p = resolve_type_str(raw.strip()) + if not p: + continue + m_str = re.match(r'^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$', p) + m_num = re.match(r'^Number(\((\d+)(\s*,\s*(\d+))?(\s*,\s*(nonneg|any))?\))?$', p) + if p == 'Boolean': + canon, kind = 'Boolean', 'Boolean' + elif m_str: + length = m_str.group(2) if m_str.group(2) else '0' + al = 'fixed' if (m_str.group(4) and m_str.group(4).lower() == 'fixed') else 'variable' + canon, kind = f'String({length},{al})', 'String' + elif m_num: + digits = m_num.group(2) if m_num.group(2) else '0' + fraction = m_num.group(4) if m_num.group(4) else '0' + sign = 'nonneg' if (m_num.group(6) and m_num.group(6).lower() == 'nonneg') else 'any' + canon, kind = f'Number({digits},{fraction},{sign})', 'Number' + elif p in ('Date', 'DateTime', 'Time'): + canon, kind = p, 'Date' + elif re.match(r'^(DefinedType|Characteristic)\.(.+)$', p): + canon, kind = p, f'set:{p}' + elif '.' in p: + kind_name = p[:p.index('.')] + if kind_name in VALUE_REF_KINDS: + canon, kind = p, f'ref:{p}' + else: + print(f'Unknown value type "{raw.strip()}" ({where}).' + f' Reference kinds: {", ".join(VALUE_REF_KINDS)}', file=sys.stderr) + sys.exit(1) + elif ':' in p: + # Чужое пространство имён — пишем как есть: калечить его мы не вправе. + canon, kind = p, f'raw:{p}' + elif p in VALUE_TYPE_SETS: + canon, kind = p, f'set:{p}' + else: + print(f'Unknown value type "{raw.strip()}" ({where}). Expected: Boolean, String(N),' + f' Number(D,F), Date, Ref. or a bare metatype', file=sys.stderr) + sys.exit(1) + # Повтор одного вида в составном типе платформа выразить не может: блок квалификаторов + # внутри один на вид. + if kind in seen: + print(f'Duplicate type "{canon}" in composite value type ({where})', file=sys.stderr) + sys.exit(1) + seen.add(kind) + parts.append(canon) + if not parts: + print(f'Empty value type ({where})', file=sys.stderr) + sys.exit(1) + return ' + '.join(parts) + + +def emit_value_type_content(lines, indent, canon_type): + """Содержимое по КАНОНУ — умолчаний здесь нет, они уже развёрнуты. Порядок снят + с платформы: сначала ВСЕ / в порядке источника, затем блоки + квалификаторов Number → String → Date. Ссылочные типы несут ЛОКАЛЬНОЕ объявление xmlns + на каждом узле — в макете префикс current-config всегда d4p1.""" + type_lines = [] + quals = {} + for p in canon_type.split(' + '): + m_str = re.match(r'^String\((\d+),(fixed|variable)\)$', p) + m_num = re.match(r'^Number\((\d+),(\d+),(nonneg|any)\)$', p) + if p == 'Boolean': + type_lines.append(f'{indent}xs:boolean') + elif m_str: + al = 'Fixed' if m_str.group(2) == 'fixed' else 'Variable' + type_lines.append(f'{indent}xs:string') + quals['String'] = [ + f'{indent}', + f'{indent}\t{m_str.group(1)}', + f'{indent}\t{al}', + f'{indent}'] + elif m_num: + sign = 'Nonnegative' if m_num.group(3) == 'nonneg' else 'Any' + type_lines.append(f'{indent}xs:decimal') + quals['Number'] = [ + f'{indent}', + f'{indent}\t{m_num.group(1)}', + f'{indent}\t{m_num.group(2)}', + f'{indent}\t{sign}', + f'{indent}'] + elif p in ('Date', 'DateTime', 'Time'): + type_lines.append(f'{indent}xs:dateTime') + quals['Date'] = [ + f'{indent}', + f'{indent}\t{p}', + f'{indent}'] + elif re.match(r'^(DefinedType|Characteristic)\.', p): + type_lines.append(f'{indent}d4p1:{p}') + elif '.' in p: + type_lines.append(f'{indent}d4p1:{p}') + elif ':' in p: + type_lines.append(f'{indent}{p}') + else: + type_lines.append(f'{indent}d4p1:{p}') + lines.extend(type_lines) + for q in ('Number', 'String', 'Date'): + if q in quals: + lines.extend(quals[q]) + + +def cell_value_props(cell, where): + """Свойства значения ячейки — пустой набор, если ячейка обычная. Считаются ОДНОЙ функцией: + пре-проход регистрации палитры и генерация обязаны получить один и тот же набор, иначе + состав палитры разойдётся и все ссылки сдвинутся.""" + props = {} + vt = cell.get('valueType') + ctl = cell.get('control') + if vt is None: + if ctl is not None and str(ctl) != '': + print(f"Cell 'control' requires 'valueType' ({where})", file=sys.stderr) + sys.exit(1) + return props + # Ячейка, содержащая значение, текста не несёт: платформа этого не допускает, и в корпусе + # нет ни одной такой ячейки из 370 197. + if cell.get('text') is not None or cell.get('template') is not None: + print(f"Cell with 'valueType' cannot have 'text' or 'template' ({where})", file=sys.stderr) + sys.exit(1) + props['containsValue'] = 'true' + # Пустая строка — третье состояние: есть, тип не задан (). + props['valueType'] = '' if str(vt) == '' else canon_value_type(str(vt), where) + + name = 'input' + if ctl is not None and str(ctl) != '': + norm = re.sub(r'\s', '', str(ctl)).lower() + norm = VALUE_CONTROL_SYNONYMS.get(norm, norm) + name = norm + if name == 'none': + return props # формат вовсе без + if name in VALUE_CONTROL_GUIDS: + props['controlType'] = VALUE_CONTROL_GUIDS[name] + elif re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', name): + # Неизвестный элемент управления сохраняем как есть — терять его нельзя. + props['controlType'] = name + else: + print(f'Unknown \'control\' value "{ctl}" ({where}).' + f' Allowed: input, checkbox, none or a GUID', file=sys.stderr) + sys.exit(1) + return props + + def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -885,8 +1130,12 @@ def main(): return '' # Helper: register a cell format and return its index - def register_cell_format(style_name, fill_type): + def register_cell_format(style_name, fill_type, value_props=None): resolved = resolve_style(style_name, fill_type) + # Свойства значения кладём ПОВЕРХ стиля — до проверки на пустоту: у ячейки-поля ввода + # оформления может не быть вовсе, и без слияния она получила бы 0, потеряв значение. + if value_props: + resolved.update(value_props) # У ячейки без собственного оформления формата НЕТ вовсе: 0, где ноль — не # индекс записи, а «формата нет». В корпусе так у 170 710 ячеек против 50 635, # ссылающихся на формат по умолчанию; 0 встречается в 71% макетов. @@ -904,7 +1153,8 @@ def main(): """Объект описывает СВОЙСТВА ячейки или является её ЗНАЧЕНИЕМ: в первом случае среди ключей есть ключ схемы ячейки, во втором ключи — идентификаторы языков. Пересечений нет: в корпусе это ru, en, ru1, Русский.""" - cell_keys = ('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template') + cell_keys = ('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template', + 'valueType', '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): @@ -1069,7 +1319,8 @@ def main(): for cell in row['cells']: cell_style = cell.get('style') or cells_style or 'default' ft = get_fill_type(cell) - register_cell_format(cell_style, ft) + vp = cell_value_props(cell, f'area "{area.get("name") or ""}"') + register_cell_format(cell_style, ft, vp) # Формат по умолчанию — последняя запись палитры (см. выше). default_format_index = register_format({'width': default_width}) @@ -1309,7 +1560,8 @@ def main(): rowspan = int(cell.get('rowspan', 1)) cell_style = cell.get('style') or cells_style or 'default' ft = get_fill_type(cell) - fmt_idx = register_cell_format(cell_style, ft) + vp = cell_value_props(cell, f'area "{area_name}", row {local_row + 1}') + fmt_idx = register_cell_format(cell_style, ft, vp) cell_info = { 'Col': col_start - 1, # 0-based @@ -1603,6 +1855,14 @@ def main(): lines.append(f'\t\t\t\t{esc_xml_text(val)}') lines.append('\t\t\t') lines.append(f'\t\t') + elif tag == 'valueType': + # Пустой тип — самостоятельное состояние: «содержит значение», тип не задан. + if str(val) == '': + lines.append('\t\t') + else: + lines.append('\t\t') + emit_value_type_content(lines, '\t\t\t', str(val)) + lines.append('\t\t') elif kinds.get(tag) == 'color' and color_namespace(str(val)): # web/win-палитры в корне документа не объявлены — платформа дописывает # объявление прямо на узел и пишет значение с этим префиксом. diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index c736c20e..900a6358 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.20 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.21 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -78,6 +78,78 @@ function ConvertTo-DslColor { return $val } +$CFG_NS = 'http://v8.1c.ru/8.1/data/enterprise/current-config' + +# Элемент управления ячейки: в выгрузке это GUID, имён у платформы в XML нет. +$valueControlNames = @{ + '381ed624-9217-4e63-85db-c4c3cb87daae' = 'input' + '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26' = 'checkbox' +} + +# Обратная функция к эмиттеру типа компилятора: → строка DSL в КРАТЧАЙШЕЙ форме +# (Boolean, String, String(10,fixed), Number(15,3,nonneg), DateTime, CatalogRef.X, AnyRef), +# из которой компилятор восстановит тот же XML. Пустой тег → пустая строка: «содержит +# значение» без указания типа — самостоятельное состояние. +# Префикс ссылочного типа разрешаем по URI, а не по имени: локальное объявление платформа +# называет сгенерированным dNpM. Чужое пространство имён оставляем как есть. +function Get-ValueQualifier { + param($quals, [string]$kind, [string]$name, [string]$default = '') + if (-not $quals.ContainsKey($kind)) { return $default } + $sub = $quals[$kind].SelectSingleNode("v8:$name", $ns) + if ($sub -and $sub.InnerText) { return $sub.InnerText.Trim() } + return $default +} + +function ConvertFrom-ValueTypeNode { + param($node) + $types = @() + $quals = @{} + foreach ($child in $node.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + $tag = $child.get_LocalName() + if ($tag -ceq 'Type' -or $tag -ceq 'TypeSet') { + $types += ,@($child, $child.InnerText.Trim()) + } elseif ($tag -like '*Qualifiers') { + $quals[$tag.Substring(0, $tag.Length - 'Qualifiers'.Length)] = $child + } + } + + $parts = @() + foreach ($t in $types) { + $child = $t[0] + $val = $t[1] + if ($val -like '*:*') { + $prefix = $val.Substring(0, $val.IndexOf(':')) + $name = $val.Substring($val.IndexOf(':') + 1) + # get_*(): XML-адаптер PowerShell перекрывает .NET-члены одноимёнными атрибутами узла. + $uri = $child.GetNamespaceOfPrefix($prefix) + if ($uri -ceq $CFG_NS) { $val = $name } + elseif ($uri -ceq 'http://www.w3.org/2001/XMLSchema') { $val = "xs:$name" } + } + if ($val -ceq 'xs:boolean') { + $parts += 'Boolean' + } elseif ($val -ceq 'xs:string') { + $length = Get-ValueQualifier $quals 'String' 'Length' '0' + $fixed = (Get-ValueQualifier $quals 'String' 'AllowedLength' 'Variable') -ceq 'Fixed' + if ($fixed) { $parts += "String($length,fixed)" } + elseif ($length -ne '0') { $parts += "String($length)" } + else { $parts += 'String' } + } elseif ($val -ceq 'xs:decimal') { + $digits = Get-ValueQualifier $quals 'Number' 'Digits' '0' + $fraction = Get-ValueQualifier $quals 'Number' 'FractionDigits' '0' + $nonneg = (Get-ValueQualifier $quals 'Number' 'AllowedSign' 'Any') -ceq 'Nonnegative' + if ($nonneg) { $parts += "Number($digits,$fraction,nonneg)" } + elseif ($digits -ne '0' -or $fraction -ne '0') { $parts += "Number($digits,$fraction)" } + else { $parts += 'Number' } + } elseif ($val -ceq 'xs:dateTime') { + $parts += (Get-ValueQualifier $quals 'Date' 'DateFractions' 'DateTime') + } else { + $parts += $val + } + } + return ($parts -join ' + ') +} + # --- 2. Extract font palette --- # Размер шрифта бывает дробным (8.3, 11.3 — в корпусе ERP это треть макетов). [int] его @@ -144,6 +216,13 @@ foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) { if ($item -and $item.InnerText) { $fmt.Props[$tag] = $item.InnerText } continue } + # Тип значения ячейки — единственный тег со СВОЕЙ структурой: разбираем его до обеих + # отсечек ниже. Пустой тоже значим — это «содержит значение» без + # указания типа. + if ($tag -ceq 'valueType') { + $fmt.Props[$tag] = ConvertFrom-ValueTypeNode $child + continue + } # Вложенный элемент скаляром не является: InnerText склеил бы всё поддерево вместе # с переводами строк. py-порт брал .text и получал пустоту — отсюда расхождение портов. if ($child.SelectNodes('*').Count -gt 0) { continue } @@ -952,7 +1031,11 @@ foreach ($area in $blocks) { $gapCells = @() foreach ($cell in $rd.Cells) { - $hasContent = $cell.Param -or $cell.HasText + # Ячейка-поле ввода — содержательная, даже когда ни текста, ни параметра в ней нет: + # значение задаёт её формат. Без этого такая строка уходила бы в пустые. + $cf = Get-Format $cell.FormatIdx + $hasValue = ($cf -and $cf.Props['containsValue'] -ceq 'true') + $hasContent = $cell.Param -or $cell.HasText -or $hasValue $hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)") if ($hasContent -or $hasMerge) { @@ -1034,6 +1117,24 @@ foreach ($area in $blocks) { } } + # Значение (ячейка-поле ввода). Свойство живёт в формате, но принадлежит ячейке: + # на корпусе на такие записи ссылаются только . + if ($cellFmt -and $cellFmt.Props['containsValue'] -ceq 'true') { + $dslCell["valueType"] = if ($cellFmt.Props.Contains('valueType')) { $cellFmt.Props['valueType'] } else { "" } + if (-not $cellFmt.Props.Contains('controlType')) { + # Формат без вовсе — так пишут машинно-сгенерированные макеты + # регламентированной отчётности; компилятор такое повторит только по + # явному указанию. + $dslCell["control"] = "none" + } else { + $ctl = "$($cellFmt.Props['controlType'])" + $name = if ($valueControlNames.ContainsKey($ctl.ToLowerInvariant())) { $valueControlNames[$ctl.ToLowerInvariant()] } else { $ctl } + # input — умолчание компилятора, писать его значит шуметь на 63 629 + # форматах корпуса. + if ($name -cne 'input') { $dslCell["control"] = $name } + } + } + # Content $fillType = if ($cellFmt) { $cellFmt.FillType } else { "" } diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index bd9d7e10..3dd40309 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.20 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.21 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -117,6 +117,76 @@ def color_to_dsl(node, val): return val +CFG_NS = 'http://v8.1c.ru/8.1/data/enterprise/current-config' + +# Элемент управления ячейки: в выгрузке это GUID, имён у платформы в XML нет. +VALUE_CONTROL_NAMES = { + '381ed624-9217-4e63-85db-c4c3cb87daae': 'input', + '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26': 'checkbox', +} + + +def value_type_to_dsl(node): + """Обратная функция к эмиттеру типа компилятора: → строка DSL в КРАТЧАЙШЕЙ + форме (Boolean, String, String(10,fixed), Number(15,3,nonneg), DateTime, CatalogRef.X, + AnyRef), из которой компилятор восстановит тот же XML. Пустой тег → пустая строка: + «содержит значение» без указания типа — самостоятельное состояние. + Префикс ссылочного типа разрешаем по URI, а не по имени: локальное объявление платформа + называет сгенерированным dNpM. Чужое пространство имён оставляем как есть.""" + types = [] + quals = {} + for child in node: + tag = etree.QName(child).localname + if tag in ('Type', 'TypeSet'): + types.append((tag, child, (child.text or '').strip())) + elif tag.endswith('Qualifiers'): + quals[tag[:-len('Qualifiers')]] = child + + def qual(kind, name, default=''): + el = quals.get(kind) + if el is None: + return default + sub = find(el, 'v8:' + name) + val = text_of(sub) + return val.strip() if val else default + + parts = [] + for tag, child, val in types: + if ':' in val: + prefix, name = val.split(':', 1) + uri = child.nsmap.get(prefix) + if uri == CFG_NS: + val = name + elif uri == 'http://www.w3.org/2001/XMLSchema': + val = 'xs:' + name + if val == 'xs:boolean': + parts.append('Boolean') + elif val == 'xs:string': + length = qual('String', 'Length', '0') + fixed = qual('String', 'AllowedLength', 'Variable') == 'Fixed' + if fixed: + parts.append(f'String({length},fixed)') + elif length != '0': + parts.append(f'String({length})') + else: + parts.append('String') + elif val == 'xs:decimal': + digits = qual('Number', 'Digits', '0') + fraction = qual('Number', 'FractionDigits', '0') + nonneg = qual('Number', 'AllowedSign', 'Any') == 'Nonnegative' + if nonneg: + parts.append(f'Number({digits},{fraction},nonneg)') + elif digits != '0' or fraction != '0': + parts.append(f'Number({digits},{fraction})') + else: + parts.append('Number') + elif val == 'xs:dateTime': + parts.append(qual('Date', 'DateFractions', 'DateTime')) + else: + parts.append(val) + return ' + '.join(parts) + + def int_of(node, default=0): if node is not None and node.text: return int(node.text) @@ -311,6 +381,12 @@ def main(): if val: fmt["Props"][tag] = val continue + # Тип значения ячейки — единственный тег со СВОЕЙ структурой: разбираем его до + # обеих отсечек ниже. Пустой тоже значим — это «содержит значение» + # без указания типа. + if tag == "valueType": + fmt["Props"][tag] = value_type_to_dsl(child) + continue # Вложенный элемент скаляром не является: .text дал бы пустоту, а ps1-порт брал # InnerText и получал склейку поддерева — отсюда расхождение портов. if len(child): @@ -952,7 +1028,11 @@ def main(): gap_cells = [] for cell in rd["Cells"]: - has_content = cell["Param"] or cell["HasText"] + # Ячейка-поле ввода — содержательная, даже когда ни текста, ни параметра в ней + # нет: значение задаёт её формат. Без этого такая строка уходила бы в пустые. + cf = get_format(cell["FormatIdx"]) + has_value = bool(cf and cf["Props"].get("containsValue") == "true") + has_content = cell["Param"] or cell["HasText"] or has_value has_merge = f"{global_row},{cell['Col']}" in merge_map if has_content or has_merge: @@ -1030,6 +1110,23 @@ def main(): if sn != "default" or cells_name: dsl_cell["style"] = sn + # Значение (ячейка-поле ввода). Свойство живёт в формате, но принадлежит + # ячейке: на корпусе на такие записи ссылаются только . + if cell_fmt and cell_fmt["Props"].get("containsValue") == "true": + dsl_cell["valueType"] = cell_fmt["Props"].get("valueType", "") + ctl = cell_fmt["Props"].get("controlType") + if ctl is None: + # Формат без вовсе — так пишут машинно-сгенерированные + # макеты регламентированной отчётности; компилятор такое повторит + # только по явному указанию. + dsl_cell["control"] = "none" + else: + name = VALUE_CONTROL_NAMES.get(ctl.lower(), ctl) + # input — умолчание компилятора, писать его значит шуметь на 63 629 + # форматах корпуса. + if name != "input": + dsl_cell["control"] = name + # Content fill_type = cell_fmt["FillType"] if cell_fmt else "" diff --git a/.claude/skills/mxl-validate/SKILL.md b/.claude/skills/mxl-validate/SKILL.md index e817607d..faf037fe 100644 --- a/.claude/skills/mxl-validate/SKILL.md +++ b/.claude/skills/mxl-validate/SKILL.md @@ -10,7 +10,7 @@ allowed-tools: # /mxl-validate — валидация макета табличного документа (MXL) -Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений. +Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений, согласованность ячеек-полей ввода. ## Параметры diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index 7cf6338f..635eb05a 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 @@ -1,4 +1,4 @@ -# mxl-validate v1.2 — Validate 1C spreadsheet +# mxl-validate v1.3 — Validate 1C spreadsheet # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Alias('Path')] @@ -91,7 +91,9 @@ function Report-Warn { $templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath))) if ($Detailed) { - Write-Host "=== Validation: $templateName ===" + # Форма имени та же, что в итоговой строке и у py-порта: раньше шапка -Detailed + # печаталась без префикса «Template.», и вывод портов расходился одной строкой. + Write-Host "=== Validation: Template.$templateName ===" Write-Host "" } @@ -400,6 +402,80 @@ foreach ($drawing in $root.SelectNodes("d:drawing", $nsMgr)) { } } +# --- Check 13: value cells (input fields) --- +# Свойства containsValue/valueType/controlType принадлежат ЯЧЕЙКЕ: на корпусе ERP +# (370 197 ссылок) на такие записи палитры ссылаются только , ни строка, ни колонка, +# ни defaultFormatIndex. Ячейка со значением текста не несёт — ни одна из 370 197. + +$valueFormatIdx = @{} +$controlGuids = @('381ed624-9217-4e63-85db-c4c3cb87daae', '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26') +for ($i = 0; $i -lt $formatNodes.Count; $i++) { + $fmt = $formatNodes[$i] + $contains = $fmt.SelectSingleNode("d:containsValue", $nsMgr) + $vt = $fmt.SelectSingleNode("d:valueType", $nsMgr) + if (-not $contains -and -not $vt) { continue } + $num = $i + 1 + $valueFormatIdx[$num] = $true + if (-not $contains -or $contains.InnerText.Trim() -cne 'true') { + Report-Error "Format ${num}: without true" + } elseif (-not $vt) { + Report-Error "Format ${num}: without " + } + if ($vt) { + foreach ($child in $vt.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + $tag = $child.get_LocalName() + if ($tag -cne 'Type' -and $tag -cne 'TypeSet' -and $tag -notlike '*Qualifiers') { + Report-Error "Format ${num}: unexpected <$tag> inside " + } + } + } + $ctl = $fmt.SelectSingleNode("d:controlType", $nsMgr) + if ($ctl -and ($controlGuids -notcontains $ctl.InnerText.Trim().ToLowerInvariant())) { + Report-Warn "Format ${num}: unknown controlType $($ctl.InnerText.Trim())" + } +} + +if ($valueFormatIdx.Count -gt 0) { + foreach ($ri in $root.SelectNodes("d:rowsItem", $nsMgr)) { + if ($stopped) { break } + $row = $ri.SelectSingleNode("d:row", $nsMgr) + if (-not $row) { continue } + $idxNode = $ri.SelectSingleNode("d:index", $nsMgr) + $rn = if ($idxNode) { $idxNode.InnerText } else { '?' } + $rowFmt = $row.SelectSingleNode("d:formatIndex", $nsMgr) + if ($rowFmt -and $valueFormatIdx.ContainsKey([int]$rowFmt.InnerText)) { + Report-Warn "Row ${rn}: formatIndex points to a value format (cell-only property)" + } + foreach ($cGroup in $row.SelectNodes("d:c", $nsMgr)) { + $cell = $cGroup.SelectSingleNode("d:c", $nsMgr) + if (-not $cell) { continue } + $fNode = $cell.SelectSingleNode("d:f", $nsMgr) + if (-not $fNode -or -not $valueFormatIdx.ContainsKey([int]$fNode.InnerText)) { continue } + if ($cell.SelectSingleNode("d:tl", $nsMgr)) { + Report-Error "Row ${rn}: cell contains a value and text at the same time" + } + } + } + foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) { + foreach ($ci in $cols.SelectNodes("d:columnsItem", $nsMgr)) { + $col = $ci.SelectSingleNode("d:column", $nsMgr) + if (-not $col) { continue } + $fmtNode = $col.SelectSingleNode("d:formatIndex", $nsMgr) + if ($fmtNode -and $valueFormatIdx.ContainsKey([int]$fmtNode.InnerText)) { + $colIdxNode = $ci.SelectSingleNode("d:index", $nsMgr) + $colIdxText = if ($colIdxNode) { $colIdxNode.InnerText } else { '?' } + Report-Warn "Column ${colIdxText}: formatIndex points to a value format (cell-only property)" + } + } + } + $dfi = $root.SelectSingleNode("d:defaultFormatIndex", $nsMgr) + if ($dfi -and $valueFormatIdx.ContainsKey([int]$dfi.InnerText)) { + Report-Warn "defaultFormatIndex points to a value format (cell-only property)" + } + Report-OK "Value cells: $($valueFormatIdx.Count) value formats" +} + # --- Summary --- $checks = $script:okCount + $errors + $warnings diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.py b/.claude/skills/mxl-validate/scripts/mxl-validate.py index dd4bef79..390731a1 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.py +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-validate v1.2 — Validate 1C spreadsheet document Template.xml +# mxl-validate v1.3 — Validate 1C spreadsheet document Template.xml # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges.""" import sys, os, argparse @@ -223,7 +223,11 @@ def main(): r.error(f'Font index {max_font_ref} exceeds palette size ({font_count})') elif max_font_ref > 0: r.error(f'Font index {max_font_ref} referenced but no fonts defined') - # No font references — no check needed + else: + # Макет без единого шрифта — норма (ячейки-поля ввода, макеты без оформления). + # Строку печатаем как ps1-порт: раньше py молчал, и на таком макете число проверок + # у портов расходилось. + r.ok('No font references') # --- Check 11: line/border indices in formats --- if line_count > 0: @@ -233,7 +237,8 @@ def main(): r.error(f'Line index {max_line_ref} exceeds palette size ({line_count})') elif max_line_ref > 0: r.error(f'Line index {max_line_ref} referenced but no lines defined') - # No line/border references — no check needed + else: + r.ok('No line/border references') # --- Check 3, 4, 5, 6: row/cell checks --- max_cell_format_ref = 0 @@ -393,6 +398,67 @@ def main(): draw_id = draw_id_node.text if draw_id_node is not None else '?' r.error(f'Drawing id={draw_id}: pictureIndex={pic_idx} > picture count ({picture_count})') + # --- Check 13: value cells (input fields) --- + # Свойства containsValue/valueType/controlType принадлежат ЯЧЕЙКЕ: на корпусе ERP + # (370 197 ссылок) на такие записи палитры ссылаются только , ни строка, ни колонка, + # ни defaultFormatIndex. Ячейка со значением текста не несёт — ни одна из 370 197. + value_format_idx = set() + control_guids = ('381ed624-9217-4e63-85db-c4c3cb87daae', '35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26') + for i, fmt in enumerate(format_nodes, start=1): + contains = fmt.find(f'{{{NS_D}}}containsValue') + vt = fmt.find(f'{{{NS_D}}}valueType') + if contains is None and vt is None: + continue + value_format_idx.add(i) + if contains is None or (contains.text or '').strip() != 'true': + r.error(f'Format {i}: without true') + elif vt is None: + r.error(f'Format {i}: without ') + if vt is not None: + for child in vt: + tag = child.tag.split('}')[-1] + if tag not in ('Type', 'TypeSet') and not tag.endswith('Qualifiers'): + r.error(f'Format {i}: unexpected <{tag}> inside ') + ctl = fmt.find(f'{{{NS_D}}}controlType') + if ctl is not None and (ctl.text or '').strip().lower() not in control_guids: + r.warn(f'Format {i}: unknown controlType {(ctl.text or "").strip()}') + + if value_format_idx: + for ri in root.findall(f'{{{NS_D}}}rowsItem'): + if r.stopped: + break + row = ri.find(f'{{{NS_D}}}row') + if row is None: + continue + idx_node = ri.find(f'{{{NS_D}}}index') + rn = idx_node.text if idx_node is not None else '?' + row_fmt = row.find(f'{{{NS_D}}}formatIndex') + if row_fmt is not None and row_fmt.text and int(row_fmt.text) in value_format_idx: + r.warn(f'Row {rn}: formatIndex points to a value format (cell-only property)') + for c_group in row.findall(f'{{{NS_D}}}c'): + cell = c_group.find(f'{{{NS_D}}}c') + if cell is None: + continue + f_node = cell.find(f'{{{NS_D}}}f') + if f_node is None or not f_node.text or int(f_node.text) not in value_format_idx: + continue + if cell.find(f'{{{NS_D}}}tl') is not None: + r.error(f'Row {rn}: cell contains a value and text at the same time') + for cols in root.findall(f'{{{NS_D}}}columns'): + for ci in cols.findall(f'{{{NS_D}}}columnsItem'): + col = ci.find(f'{{{NS_D}}}column') + if col is None: + continue + fmt_node = col.find(f'{{{NS_D}}}formatIndex') + if fmt_node is not None and fmt_node.text and int(fmt_node.text) in value_format_idx: + col_idx_node = ci.find(f'{{{NS_D}}}index') + col_idx_text = col_idx_node.text if col_idx_node is not None else '?' + r.warn(f'Column {col_idx_text}: formatIndex points to a value format (cell-only property)') + dfi = root.find(f'{{{NS_D}}}defaultFormatIndex') + if dfi is not None and dfi.text and int(dfi.text) in value_format_idx: + r.warn('defaultFormatIndex points to a value format (cell-only property)') + r.ok(f'Value cells: {len(value_format_idx)} value formats') + # --- Finalize --- checks = r.ok_count + r.errors + r.warnings if r.errors == 0 and r.warnings == 0 and not detailed: diff --git a/docs/1c-spreadsheet-spec.md b/docs/1c-spreadsheet-spec.md index 1f154e1f..58300992 100644 --- a/docs/1c-spreadsheet-spec.md +++ b/docs/1c-spreadsheet-spec.md @@ -513,6 +513,84 @@ pictureSizeMode · picHorizontalAlignment · picVerticalAlignment · textPositio каждой ячейки (`backColor`: 13 899 ячеек повторяют против 96). Исключение — `hidden`: оно остаётся только у строки (24 026 против 62 856). +### Ячейка-поле ввода: `containsValue` / `valueType` / `controlType` + +Ячейка может содержать не текст, а редактируемое значение. Три тега формата описывают это +целиком: + +```xml + + true + + xs:decimal + + 15 + 3 + Nonnegative + + + 381ed624-9217-4e63-85db-c4c3cb87daae + +``` + +Свойство принадлежит ЯЧЕЙКЕ, хотя и живёт в общей палитре: из 370 197 ссылок на такие записи +(корпус ERP, 5 809 макетов из 10 924) все до одной идут из `` — ни строка, ни колонка, ни +`` на них не ссылаются ни разу. + +| Наблюдение | Данные корпуса | +|---|---| +| `containsValue` всегда `true` | 63 726 форматов, других значений нет | +| `valueType` присутствует всегда | 0 форматов без него; 12 — с ПУСТЫМ `` (тип не ограничен) | +| текста у такой ячейки не бывает | 0 из 370 197 несут `` | +| `` и `` — бывают | 3 219 и 2 278 | +| `fillType` у формата со значением недостоверен | 94 062 записи несут `Text` при полном отсутствии текста — след прежнего состояния, как `width` | + +**Элемент управления** задаётся GUID; имён в XML нет вовсе. В Конфигураторе это «ЭлементУправления» +со значениями «Поле ввода» и «Поле флажка»: + +| GUID | Элемент | Вхождений | +|---|---|---:| +| `381ed624-9217-4e63-85db-c4c3cb87daae` | Поле ввода | 63 629 | +| `35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26` | Поле флажка | 7 | + +Умолчание — поле ввода, и оно же стоит у Булево: из 664 булевых форматов флажок выбран в семи. +У 90 форматов (22 макета регламентированной отчётности, явно машинно-сгенерированных) тега +`` нет вовсе. + +**Структура ``** — то же описание типов, что в метаданных: сначала все `` и +`` в порядке источника, затем блоки квалификаторов в порядке Number → String → Date. +Голая категория без имени объекта (`CatalogRef`, `AnyRef`) — это ``, а не ``. + +Ссылочные типы несут **локальное объявление пространства имён на каждом узле**: + +```xml +d4p1:CatalogRef.Валюты +``` + +Префикс всегда `d4p1` (15 вхождений на корпусе, отклонений нет) — независимо от того, есть ли +в макете цвета web-палитры, которым платформа даёт `d3p1`. Корень `` пространство +current-config не объявляет, поэтому вынести объявление наверх нельзя. + +**Продолжение конструкции — на уровне САМОЙ ячейки**, а не палитры: + +```xml + + 34 + 0 + 77u/MiwxLDM4MWVkNjI0… + +``` + +| Тег ячейки | Смысл | Вхождений | Макетов | +|---|---|---:|---:| +| `` | сохранённое значение ячейки; `xsi:type` соответствует объявленному типу (decimal 153 566, string 61 203, dateTime 3 333, boolean 1 012) | 219 114 | 4 578 | +| `` | сериализованные настройки элемента управления (base64; внутри — тот же GUID и структура вида маски/списка выбора) | 26 370 | 1 246 | + +Оба тега встречаются ИСКЛЮЧИТЕЛЬНО в ячейках, чей формат несёт `containsValue` — ни одного +вхождения в обычной ячейке. То есть это вторая половина той же конструкции, а не отдельное +свойство. Обрати внимание на совпадение имён: `` у ячейки и `` в формате — +разные вещи. + **Ловушка: `width` в формате ЯЧЕЙКИ не описывает ячейку.** Ссылка ячейки на запись палитры не обновляется при изменении ширины колонки, поэтому там остаётся ширина от прежнего состояния документа. Проверено на контролируемом стенде: в макете, где ширины колонок поменяли на diff --git a/docs/mxl-dsl-format-properties.md b/docs/mxl-dsl-format-properties.md index 82868b3f..ea83d882 100644 --- a/docs/mxl-dsl-format-properties.md +++ b/docs/mxl-dsl-format-properties.md @@ -74,4 +74,4 @@ - `fillType` — выводится из того, каким ключом задано содержимое ячейки (`text` / `param` / `template`); - `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего - оформления; сейчас не поддерживаются. + оформления: задаются ключами ячейки `valueType` и `control`. diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 5be9fe46..a5784818 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -182,6 +182,8 @@ | `detail` | нет | — | Параметр расшифровки (только с `param`) | | `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже | | `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` | +| `valueType` | нет | — | Тип значения: ячейка становится полем ввода (см. ниже) | +| `control` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | ### Содержимое ячейки @@ -212,6 +214,44 @@ Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста. +## Ячейка-поле ввода + +Ячейка может не показывать текст, а принимать значение от пользователя — так делают макеты для +ввода данных. Достаточно задать тип значения: + +```json +{ "col": 1, "valueType": "Number(15,3,nonneg)" } +{ "col": 2, "valueType": "String(10)" } +{ "col": 3, "valueType": "Boolean", "control": "checkbox" } +{ "col": 4, "valueType": "CatalogRef.Валюты" } +{ "col": 5, "valueType": "Boolean + String + Date + CatalogRef.Валюты" } +{ "col": 6, "valueType": "AnyRef" } +``` + +Грамматика типа: + +| Запись | Значение | +|---|---| +| `Boolean` | булево | +| `String`, `String(10)`, `String(10,fixed)` | строка; без длины — неограниченная, `fixed` — фиксированной длины | +| `Number`, `Number(15,3)`, `Number(15,3,nonneg)` | число; без параметров — без ограничения разрядности, `nonneg` — неотрицательное | +| `Date`, `DateTime`, `Time` | дата, дата со временем, время | +| `CatalogRef.Валюты`, `DocumentRef.Реализация`, `EnumRef.Статусы`, `DefinedType.Сумма` | ссылочный тип | +| `CatalogRef`, `DocumentRef`, `AnyRef` | категория целиком: любая ссылка этого вида | +| `A + B` | составной тип; повтор одного примитива недопустим | + +Русские имена принимаются наравне с английскими: `Число(15,3)`, `Строка(10)`, +`СправочникСсылка.Валюты`. + +`text` и `template` в такой ячейке недопустимы — платформа не показывает текст там, где вводится +значение. `param` и `detail` допустимы: они описывают заполнение и расшифровку, а не содержимое. + +Пустой тип `"valueType": ""` — «ячейка содержит значение», но тип не ограничен. + +`control` нужен редко: умолчание платформы — поле ввода, и оно применяется ко всем типам, включая +`Boolean`. Флажок задаётся явно. Значение `"none"` (тега элемента управления нет вовсе) — форма +раундтрипа, для ручного авторинга не нужна. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -226,7 +266,8 @@ DSL описывает не все конструкции табличного round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попадает, в сгенерированный XML не возвращается. -- ячейки-поля ввода (`containsValue` / `valueType` / `controlType`); +- начальное значение ячейки-поля ввода и сериализованные настройки её элемента управления + (тип значения при этом переживает цикл); - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; - группировки строк и колонок; diff --git a/tests/skills/cases/mxl-compile/error-value-bad-control.json b/tests/skills/cases/mxl-compile/error-value-bad-control.json new file mode 100644 index 00000000..7527c8b0 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-bad-control.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: неизвестный элемент управления", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "valueType": "Boolean", "control": "кнопка" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Unknown 'control' value \"кнопка\" (area \"Ввод\"). Allowed: input, checkbox, none or a GUID" +} diff --git a/tests/skills/cases/mxl-compile/error-value-bad-type.json b/tests/skills/cases/mxl-compile/error-value-bad-type.json new file mode 100644 index 00000000..3ddb32c3 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-bad-type.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: неизвестный тип значения (голое имя не становится множеством типов)", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "valueType": "Boolen" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Unknown value type \"Boolen\" (area \"Ввод\")" +} diff --git a/tests/skills/cases/mxl-compile/error-value-control-without-type.json b/tests/skills/cases/mxl-compile/error-value-control-without-type.json new file mode 100644 index 00000000..59f584c7 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-control-without-type.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: control без valueType", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "control": "checkbox" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Cell 'control' requires 'valueType' (area \"Ввод\")" +} diff --git a/tests/skills/cases/mxl-compile/error-value-duplicate-type.json b/tests/skills/cases/mxl-compile/error-value-duplicate-type.json new file mode 100644 index 00000000..c13d4a04 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-duplicate-type.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: составной тип с двумя строками (блок квалификаторов один)", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "valueType": "String(10) + String(20)" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Duplicate type \"String(20,variable)\" in composite value type (area \"Ввод\")" +} diff --git a/tests/skills/cases/mxl-compile/error-value-text-with-type.json b/tests/skills/cases/mxl-compile/error-value-text-with-type.json new file mode 100644 index 00000000..b53f05e0 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-value-text-with-type.json @@ -0,0 +1,14 @@ +{ + "name": "Ошибка: ячейка со значением несёт текст", + "input": { + "columns": 1, + "areas": [ + { + "name": "Ввод", + "rows": [{ "cells": [{ "col": 1, "valueType": "Boolean", "text": "Да" }] }] + } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "Cell with 'valueType' cannot have 'text' or 'template' (area \"Ввод\")" +} diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml new file mode 100644 index 00000000..089de07d --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СЗначениями.xml @@ -0,0 +1,238 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 3 + + + 0 + + + + 1 + + + + + 2 + + + + + 3 + + + + + + 1 + + + + 4 + + + + + 5 + + + + + + 2 + + + + 3 + + + + + 6 + + + + + 7 + + + + + + 3 + + + + 8 + + + + + + 4 + + + + 9 + + + + + + 5 + + + + 10 + + + + + + 6 + + + + 11 + + + + + true + 12 + 7 + 7 + + true + + xs:decimal + + 15 + 3 + Nonnegative + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 0 + 0 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26 + + + true + + xs:string + + 10 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 10 + Fixed + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:CatalogRef.Валюты + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + d4p1:CatalogRef.ГорячиеКлавиши + xs:string + xs:dateTime + xs:decimal + + 0 + 0 + Any + + + 0 + Variable + + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:DocumentRef + d4p1:CatalogRef + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:AnyRef + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 72 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/platform-value-cells.json b/tests/skills/cases/mxl-compile/platform-value-cells.json new file mode 100644 index 00000000..0d9da99d --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-value-cells.json @@ -0,0 +1,25 @@ +{ + "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: СЗначениями (ячейки-поля ввода)", + "setup": "fixture:platform-roundtrip", + "preRun": [ + { + "script": "mxl-decompile/scripts/mxl-decompile", + "args": { + "-TemplatePath": "СЗначениями.xml", + "-OutputPath": "back.json" + }, + "cwd": "{workDir}" + } + ], + "params": { + "outputPath": "out.xml" + }, + "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual", + "expect": { + "filesEqual": { + "actual": "out.xml", + "expected": "СЗначениями.xml" + } + }, + "inputFrom": "back.json" +} diff --git a/tests/skills/cases/mxl-compile/snapshots/value-cells/Template.xml b/tests/skills/cases/mxl-compile/snapshots/value-cells/Template.xml new file mode 100644 index 00000000..65e6a868 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/value-cells/Template.xml @@ -0,0 +1,230 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 1 + + + + + 2 + + + + + 3 + + + + + + 1 + + + + 4 + + + + + 5 + + + + + + 2 + + + + 6 + + + + + 7 + + + + + 8 + + + + + + 3 + + + + 9 + + + + + 10 + + + + + 11 + + + + + true + 12 + 4 + 4 + + Ввод + + Rows + 0 + 3 + -1 + -1 + + + + true + + xs:decimal + + 15 + 3 + Nonnegative + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 0 + 0 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 0 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26 + + + true + + xs:string + + 10 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 10 + Fixed + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:dateTime + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:dateTime + + Date + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:dateTime + + Time + + + 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 + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/value-dedup/Template.xml b/tests/skills/cases/mxl-compile/snapshots/value-dedup/Template.xml new file mode 100644 index 00000000..2e543a22 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/value-dedup/Template.xml @@ -0,0 +1,97 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 6 + + + 0 + + + + 1 + + + + + 1 + + + + + 1 + + + + + 2 + + + + + 2 + + + + + 3 + + + + + true + 4 + 1 + 1 + + Дедупликация + + Rows + 0 + 0 + -1 + -1 + + + + true + + xs:decimal + + 15 + 3 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 10 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26 + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/value-ref-types/Template.xml b/tests/skills/cases/mxl-compile/snapshots/value-ref-types/Template.xml new file mode 100644 index 00000000..77e6438b --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/value-ref-types/Template.xml @@ -0,0 +1,131 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 1 + + + + + 2 + + + + + 3 + + + + + + 1 + + + + 4 + + + + + 5 + + + + + 6 + + + + + true + 7 + 2 + 2 + + Ввод + + Rows + 0 + 1 + -1 + -1 + + + + true + + d4p1:CatalogRef.Валюты + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:DocumentRef.Реализация + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:DefinedType.СуммаДокумента + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:DocumentRef + d4p1:CatalogRef + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:AnyRef + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + d4p1:CatalogRef.ГорячиеКлавиши + xs:string + xs:dateTime + xs:decimal + + 0 + 0 + Any + + + 0 + Variable + + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/value-states/Template.xml b/tests/skills/cases/mxl-compile/snapshots/value-states/Template.xml new file mode 100644 index 00000000..16efa4d8 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/value-states/Template.xml @@ -0,0 +1,140 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + + 1 + + 5 + + + 5 + + + + + 6 + Сумма + Расшифровка + + + + + 5 + + + + + 5 + + + + + true + 7 + 2 + 2 + + Состояния + + Rows + 0 + 1 + -1 + -1 + + + + Solid + + + true + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 15 + 3 + Any + + + + + true + + xs:boolean + + aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee + + + 0 + true + + xs:string + + 50 + Variable + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 0 + + + 0 + Parameter + true + + xs:decimal + + 10 + 2 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/value-cells.json b/tests/skills/cases/mxl-compile/value-cells.json new file mode 100644 index 00000000..3ef600d0 --- /dev/null +++ b/tests/skills/cases/mxl-compile/value-cells.json @@ -0,0 +1,35 @@ +{ + "name": "Ячейки-поля ввода: примитивные типы значения", + "input": { + "columns": 3, + "areas": [ + { + "name": "Ввод", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "Number(15,3,nonneg)" }, + { "col": 2, "valueType": "Number" }, + { "col": 3, "valueType": "String" } + ]}, + { "cells": [ + { "col": 1, "valueType": "Boolean" }, + { "col": 2, "valueType": "Boolean", "control": "checkbox" } + ]}, + { "cells": [ + { "col": 1, "valueType": "String(10)" }, + { "col": 2, "valueType": "String(10,fixed)" }, + { "col": 3, "valueType": "DateTime" } + ]}, + { "cells": [ + { "col": 1, "valueType": "Date" }, + { "col": 2, "valueType": "Time" }, + { "col": 3, "valueType": "Boolean + String + DateTime + Number" } + ]} + ] + } + ] + }, + "params": { + "outputPath": "Template.xml" + } +} diff --git a/tests/skills/cases/mxl-compile/value-dedup.json b/tests/skills/cases/mxl-compile/value-dedup.json new file mode 100644 index 00000000..587caf02 --- /dev/null +++ b/tests/skills/cases/mxl-compile/value-dedup.json @@ -0,0 +1,24 @@ +{ + "name": "Ячейки-поля ввода: разные написания одного типа дают ОДИН формат", + "input": { + "columns": 6, + "areas": [ + { + "name": "Дедупликация", + "rows": [ + [ + { "valueType": "Число(15,3)" }, + { "valueType": "Number(15,3)" }, + { "valueType": "Number(15,3,any)" }, + { "valueType": "Строка(10)" }, + { "valueType": "String(10,variable)" }, + { "valueType": "Булево", "control": "Поле Флажка" } + ] + ] + } + ] + }, + "params": { + "outputPath": "Template.xml" + } +} diff --git a/tests/skills/cases/mxl-compile/value-ref-types.json b/tests/skills/cases/mxl-compile/value-ref-types.json new file mode 100644 index 00000000..8f8ac061 --- /dev/null +++ b/tests/skills/cases/mxl-compile/value-ref-types.json @@ -0,0 +1,27 @@ +{ + "name": "Ячейки-поля ввода: ссылочные типы и категории целиком", + "input": { + "columns": 3, + "areas": [ + { + "name": "Ввод", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "CatalogRef.Валюты" }, + { "col": 2, "valueType": "DocumentRef.Реализация" }, + { "col": 3, "valueType": "DefinedType.СуммаДокумента" } + ]}, + { "cells": [ + { "col": 1, "valueType": "DocumentRef + CatalogRef" }, + { "col": 2, "valueType": "AnyRef" }, + { "col": 3, "valueType": "Boolean + CatalogRef.ГорячиеКлавиши + String + DateTime + Number" } + ]} + ] + } + ] + }, + "params": { + "outputPath": "Template.xml" + }, + "skipPlatformVerify": "ссылочные типы разрешаются только в базе с матч-конфигурацией: временная база-заглушка таких объектов не содержит" +} diff --git a/tests/skills/cases/mxl-compile/value-states.json b/tests/skills/cases/mxl-compile/value-states.json new file mode 100644 index 00000000..93a05cb0 --- /dev/null +++ b/tests/skills/cases/mxl-compile/value-states.json @@ -0,0 +1,26 @@ +{ + "name": "Ячейки-поля ввода: три состояния типа и элемент управления", + "input": { + "columns": 4, + "styles": { "bordered": { "border": "Solid" } }, + "areas": [ + { + "name": "Состояния", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "" }, + { "col": 2, "valueType": "Number(15,3)", "control": "none" }, + { "col": 3, "valueType": "Boolean", "control": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }, + { "col": 4, "valueType": "String(50)", "style": "bordered" } + ]}, + { "rowStyle": "bordered", "cells": [ + { "col": 2, "valueType": "Number(10,2)", "param": "Сумма", "detail": "Расшифровка" } + ]} + ] + } + ] + }, + "params": { + "outputPath": "Template.xml" + } +} diff --git a/tests/skills/cases/mxl-decompile/roundtrip-value-cells.json b/tests/skills/cases/mxl-decompile/roundtrip-value-cells.json new file mode 100644 index 00000000..b2b4e924 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-value-cells.json @@ -0,0 +1,40 @@ +{ + "name": "Roundtrip — ячейки-поля ввода (тип значения и элемент управления)", + "preRun": [ + { + "script": "mxl-compile/scripts/mxl-compile", + "input": { + "columns": 3, + "areas": [ + { + "name": "Ввод", + "rows": [ + { "cells": [ + { "col": 1, "valueType": "Number(15,3,nonneg)" }, + { "col": 2, "valueType": "String(10,fixed)" }, + { "col": 3, "valueType": "Boolean", "control": "checkbox" } + ]}, + { "cells": [ + { "col": 1, "valueType": "" }, + { "col": 2, "valueType": "Number(10,2)", "control": "none" }, + { "col": 3, "valueType": "Boolean + CatalogRef.Валюты + String + DateTime" } + ]}, + { "cells": [ + { "col": 1, "valueType": "AnyRef" }, + { "col": 2, "valueType": "Number(10,2)", "param": "Сумма", "detail": "Расшифровка" } + ]} + ] + } + ] + }, + "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-cells/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-cells/Template.xml new file mode 100644 index 00000000..913543bc --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-cells/Template.xml @@ -0,0 +1,172 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 1 + + + + + 2 + + + + + 3 + + + + + + 1 + + + + 4 + + + + + 5 + + + + + 6 + + + + + + 2 + + + + 7 + + + + + 8 + Сумма + Расшифровка + + + + + true + 9 + 3 + 3 + + Ввод + + Rows + 0 + 2 + -1 + -1 + + + + true + + xs:decimal + + 15 + 3 + Nonnegative + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:string + + 10 + Fixed + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:boolean + + 35af3d93-d7c7-4a2e-a8eb-bac87a1a3f26 + + + true + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + xs:decimal + + 10 + 2 + Any + + + + + true + + xs:boolean + d4p1:CatalogRef.Валюты + xs:string + xs:dateTime + + 0 + Variable + + + DateTime + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + true + + d4p1:AnyRef + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + Parameter + true + + xs:decimal + + 10 + 2 + Any + + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-cells/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-cells/back.json new file mode 100644 index 00000000..6c3abee0 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-value-cells/back.json @@ -0,0 +1,12 @@ +{ + "columns": 3, + "defaultWidth": 10, + "fonts": {}, + "styles": {}, + "areas": [ + { + "name": "Ввод", + "rows": [[{ "valueType": "Number(15,3,nonneg)" }, { "valueType": "String(10,fixed)" }, { "valueType": "Boolean", "control": "checkbox" }], [{ "valueType": "" }, { "valueType": "Number(10,2)", "control": "none" }, { "valueType": "Boolean + CatalogRef.Валюты + String + DateTime" }], [{ "valueType": "AnyRef" }, { "valueType": "Number(10,2)", "param": "Сумма", "detail": "Расшифровка" }]] + } + ] +} \ No newline at end of file diff --git a/tests/skills/cases/mxl-validate/error-value-cell-with-text.json b/tests/skills/cases/mxl-validate/error-value-cell-with-text.json new file mode 100644 index 00000000..46505e7e --- /dev/null +++ b/tests/skills/cases/mxl-validate/error-value-cell-with-text.json @@ -0,0 +1,7 @@ +{ + "name": "Ошибка: ячейка одновременно содержит значение и текст", + "setup": "fixture:value-cell-with-text", + "params": { "templatePath": "Template.xml" }, + "expectError": true, + "expect": { "stdoutContains": "cell contains a value and text at the same time" } +} diff --git a/tests/skills/cases/mxl-validate/fixtures/value-cell-with-text/Template.xml b/tests/skills/cases/mxl-validate/fixtures/value-cell-with-text/Template.xml new file mode 100644 index 00000000..bae76689 --- /dev/null +++ b/tests/skills/cases/mxl-validate/fixtures/value-cell-with-text/Template.xml @@ -0,0 +1,44 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 1 + + + 0 + + + + 1 + + + ru + Текст в ячейке со значением + + + + + + + true + 2 + 1 + + true + + xs:boolean + + 381ed624-9217-4e63-85db-c4c3cb87daae + + + 72 + + diff --git a/tests/skills/cases/mxl-validate/fixtures/value-format-misplaced/Template.xml b/tests/skills/cases/mxl-validate/fixtures/value-format-misplaced/Template.xml new file mode 100644 index 00000000..b5b6c820 --- /dev/null +++ b/tests/skills/cases/mxl-validate/fixtures/value-format-misplaced/Template.xml @@ -0,0 +1,39 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 1 + + + 0 + + 1 + + + 1 + + + + + true + 2 + 1 + + true + + xs:boolean + + 11111111-2222-3333-4444-555555555555 + + + 72 + + diff --git a/tests/skills/cases/mxl-validate/snapshots/warn-value-format-misplaced/Template.xml b/tests/skills/cases/mxl-validate/snapshots/warn-value-format-misplaced/Template.xml new file mode 100644 index 00000000..b5b6c820 --- /dev/null +++ b/tests/skills/cases/mxl-validate/snapshots/warn-value-format-misplaced/Template.xml @@ -0,0 +1,39 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 1 + + + 0 + + 1 + + + 1 + + + + + true + 2 + 1 + + true + + xs:boolean + + 11111111-2222-3333-4444-555555555555 + + + 72 + + diff --git a/tests/skills/cases/mxl-validate/warn-value-format-misplaced.json b/tests/skills/cases/mxl-validate/warn-value-format-misplaced.json new file mode 100644 index 00000000..27cc6c10 --- /dev/null +++ b/tests/skills/cases/mxl-validate/warn-value-format-misplaced.json @@ -0,0 +1,7 @@ +{ + "name": "Предупреждения: формат со значением у строки и неизвестный элемент управления", + "setup": "fixture:value-format-misplaced", + "params": { "templatePath": "Template.xml" }, + "args_extra": ["-Detailed"], + "expect": { "stdoutContains": "unknown controlType" } +} diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index 743ecc27..b569de77 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -170,7 +170,7 @@ const FAMILIES = [ // TYPE_SYNONYMS / $script:typeSynonyms на свой локальный словарь. variants: [ { id: 'base', authority: 'meta-compile', - consumers: ['form-compile', 'form-edit', 'meta-edit', 'skd-compile', 'skd-edit'] }, + consumers: ['form-compile', 'form-edit', 'meta-edit', 'mxl-compile', 'skd-compile', 'skd-edit'] }, ], },