diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index b29353d0..757c3a43 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.52 — Compile 1C managed form from JSON or object metadata +# form-compile v1.53 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -2134,7 +2134,10 @@ function Emit-SingleType { if ($script:knownInvalidTypes.ContainsKey($typeStr)) { throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" } - if ($typeStr.Contains('.')) { + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod). + if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') { + X "$indent$typeStr" + } elseif ($typeStr.Contains('.')) { X "$indentcfg:$typeStr" } else { Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" @@ -3597,6 +3600,22 @@ function Emit-Popup { # --- 8. Attribute emitter --- +# FunctionalOption.X — у Attribute/Command/Column. +# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. +function Emit-FunctionalOptions { + param($fo, [string]$indent) + if (-not $fo -or @($fo).Count -eq 0) { return } + X "$indent" + foreach ($opt in @($fo)) { + $v = "$opt" + if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть + elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом + else { $v = "FunctionalOption.$v" } + X "$indent`t$v" + } + X "$indent" +} + function Emit-Attributes { param($attrs, [string]$indent) @@ -3638,6 +3657,7 @@ function Emit-Attributes { if ($attr.fillChecking) { X "$inner$($attr.fillChecking)" } + Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner # Columns (for ValueTable/ValueTree) if ($attr.columns -and $attr.columns.Count -gt 0) { @@ -3649,6 +3669,7 @@ function Emit-Attributes { Emit-MLText -tag "Title" -text $col.title -indent "$inner`t`t" } Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" + Emit-FunctionalOptions -fo $col.functionalOptions -indent "$inner`t`t" X "$inner`t" } X "$inner" @@ -3756,6 +3777,8 @@ function Emit-Commands { X "$inner$($cmd.action)" } + Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner + if ($cmd.currentRowUse) { X "$inner$($cmd.currentRowUse)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 6ddc0363..7abda8a1 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.52 — Compile 1C managed form from JSON or object metadata +# form-compile v1.53 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -2452,7 +2452,10 @@ def emit_single_type(lines, type_str, indent): # Fallback with validation if type_str in KNOWN_INVALID_TYPES: raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") - if '.' in type_str: + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). + if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): + lines.append(f'{indent}{type_str}') + elif '.' in type_str: lines.append(f'{indent}cfg:{type_str}') else: print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) @@ -3271,6 +3274,24 @@ def emit_button_group(lines, el, name, eid, indent): # --- Attribute emitter --- +def emit_functional_options(lines, fo, indent): + # FunctionalOption.X…> — у Attribute/Command/Column. + # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. + if not fo: + return + lines.append(f'{indent}') + for opt in fo: + v = str(opt) + if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): + pass + elif v.startswith('FunctionalOption.'): + pass + else: + v = f'FunctionalOption.{v}' + lines.append(f'{indent}\t{v}') + lines.append(f'{indent}') + + def emit_attributes(lines, attrs, indent): if not attrs or len(attrs) == 0: return @@ -3310,6 +3331,7 @@ def emit_attributes(lines, attrs, indent): lines.append(f'{inner}true') if attr.get('fillChecking'): lines.append(f'{inner}{attr["fillChecking"]}') + emit_functional_options(lines, attr.get('functionalOptions'), inner) # Columns (for ValueTable/ValueTree) if attr.get('columns') and len(attr['columns']) > 0: @@ -3320,6 +3342,7 @@ def emit_attributes(lines, attrs, indent): if col.get('title'): emit_mltext(lines, f'{inner}\t\t', 'Title', col['title']) emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') + emit_functional_options(lines, col.get('functionalOptions'), f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') @@ -3414,6 +3437,8 @@ def emit_commands(lines, cmds, indent): if cmd.get('action'): lines.append(f'{inner}{cmd["action"]}') + emit_functional_options(lines, cmd.get('functionalOptions'), inner) + if cmd.get('currentRowUse'): lines.append(f'{inner}{cmd["currentRowUse"]}') diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 74f5b087..e567a259 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.34 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.35 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -802,6 +802,20 @@ function Decompile-XrFlag { return $o } +# FunctionalOption.X…> → массив строк (префикс FunctionalOption. снят; GUID — как есть). +function Decompile-FunctionalOptions { + param($node) + $foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns) + if (-not $foNode) { return $null } + $opts = New-Object System.Collections.ArrayList + foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) { + $t = $it.InnerText.Trim() -replace '^FunctionalOption\.', '' + [void]$opts.Add($t) + } + if ($opts.Count -gt 0) { return ,@($opts) } + return $null +} + # Общие свойства элемента (visible/enabled/readonly/title/events) → в hash function Add-CommonProps { param($obj, $node, [string]$elName) @@ -830,10 +844,12 @@ function Decompile-Type { foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { $raw = $vt.InnerText.Trim() $short = $raw + # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). switch -regex ($raw) { '^xs:string$' { $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) if ($len -and [int]$len.InnerText -gt 0) { $short = "string($($len.InnerText))" } else { $short = "string" } + break } '^xs:decimal$' { $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) @@ -842,15 +858,28 @@ function Decompile-Type { $dd = if ($d) { $d.InnerText } else { '0' } $ff = if ($f) { $f.InnerText } else { '0' } if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } + break } - '^xs:boolean$' { $short = "boolean" } + '^xs:boolean$' { $short = "boolean"; break } '^xs:dateTime$' { $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) $dfv = if ($df) { $df.InnerText } else { 'DateTime' } switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } + break + } + '^cfg:(.+)$' { $short = $matches[1]; break } + '^(v8|v8ui):' { + # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом + # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. + $rev = @{ + 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' + 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' + 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' + 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' + } + if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } + break } - '^v8:ValueListType$' { $short = 'ValueList' } - '^(v8|v8ui|cfg):(.+)$' { $short = $matches[2] } default { $short = $raw } } [void]$parts.Add($short) @@ -1287,6 +1316,7 @@ if ($attrsNode) { $tNode = $a.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $ao['title'] = $t } } if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } $fc = Get-Child $a 'FillChecking'; if ($fc) { $ao['fillChecking'] = $fc } + $afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo } $colsNode = $a.SelectSingleNode("lf:Columns", $ns) if ($colsNode) { $cols = New-Object System.Collections.ArrayList @@ -1294,6 +1324,7 @@ if ($attrsNode) { $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") $cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty } $ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangText $ctNode; if ($null -ne $t) { $co['title'] = $t } } + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } [void]$cols.Add($co) } if ($cols.Count -gt 0) { $ao['columns'] = @($cols) } @@ -1378,6 +1409,7 @@ if ($cmdsNode) { $tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } $ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 172a4f29..3bcda4cf 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -611,6 +611,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | `title` | string | Заголовок | | `view` | bool/object | Просмотр по ролям (``). См. §4.1c | | `edit` | bool/object | Редактирование по ролям (``). См. §4.1c | +| `functionalOptions` | array | Функциональные опции (`FunctionalOption.X…`). Массив имён; forgiving: `"X"`/`"FunctionalOption.X"`. Также у колонок (`columns[*]`) и команд (§7) | | `savedData` | bool | Сохраняемые данные | | `fillChecking` | string | `Show`, `DontShow` | | `columns` | array | Колонки для ValueTable/ValueTree | @@ -700,6 +701,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | `title` | string | Заголовок | | `tooltip` | string/object | Всплывающая подсказка команды (``) | | `use` | bool/object | Доступность команды по ролям (``). См. §4.1c | +| `functionalOptions` | array | Функциональные опции команды (см. §5) | | `currentRowUse` | string | Использование текущей строки: `Auto`, `DontUse`, `Use` | | `shortcut` | string | Клавиатурное сочетание | | `picture` | string | Ссылка на картинку | diff --git a/tests/skills/cases/form-compile/attributes-types.json b/tests/skills/cases/form-compile/attributes-types.json index 6a4e5ce5..dda6aa3c 100644 --- a/tests/skills/cases/form-compile/attributes-types.json +++ b/tests/skills/cases/form-compile/attributes-types.json @@ -26,7 +26,9 @@ { "name": "Строка", "type": "string(200)", "view": false }, { "name": "Число", "type": "decimal(10,0,nonneg)", "edit": false }, { "name": "Дата", "type": "dateTime" }, - { "name": "Булево", "type": "boolean" } + { "name": "Булево", "type": "boolean" }, + { "name": "СписокЗначений", "type": "ValueList" }, + { "name": "Идентификатор", "type": "v8:UUID" } ] } } diff --git a/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml index 94420237..ee0219a5 100644 --- a/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml @@ -102,5 +102,27 @@ xs:boolean + + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Список значений</v8:content> + </v8:item> + + + v8:ValueListType + + + + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Идентификатор</v8:content> + </v8:item> + + + v8:UUID + +