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 | Доступность команды по ролям (`