diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1
index 384a6a52..3b929986 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.47 — Compile 1C managed form from JSON or object metadata
+# form-compile v1.48 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -2264,6 +2264,38 @@ function Emit-Companion {
X "$indent$tag>"
}
+# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] }
+# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion).
+# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar).
+function Emit-CompanionPanel {
+ param([string]$tag, [string]$name, [string]$indent, $panel)
+ $id = New-Id
+ $autofill = $null
+ $children = $null
+ $halign = $null
+ if ($panel -is [array]) {
+ $children = $panel
+ } elseif ($null -ne $panel) {
+ if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill }
+ if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" }
+ $children = $panel.children
+ }
+ $hasChildren = $children -and @($children).Count -gt 0
+ if ($null -eq $autofill -and -not $hasChildren -and -not $halign) {
+ X "$indent<$tag name=`"$name`" id=`"$id`"/>"
+ return
+ }
+ X "$indent<$tag name=`"$name`" id=`"$id`">"
+ if ($halign) { X "$indent`t$halign" }
+ if ($null -ne $autofill) { X "$indent`t$(if ($autofill){'true'}else{'false'})" }
+ if ($hasChildren) {
+ X "$indent`t"
+ foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true }
+ X "$indent`t"
+ }
+ X "$indent$tag>"
+}
+
# Табличный addition (СтрокаПоиска/СостояниеПросмотра/УправлениеПоиском) с AdditionSource.
# Item = имя таблицы, Type фиксирован по виду; внутри — companion ContextMenu/ExtendedTooltip.
function Emit-TableAddition {
@@ -2283,8 +2315,14 @@ function Emit-TableAddition {
function Emit-Element {
param($el, [string]$indent, [bool]$inCmdBar = $false)
+ # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов.
+ Normalize-PanelSynonyms $el
+
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
# Maps any synonym to canonical short DSL key.
+ # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя);
+ # объект/массив уже отнесён к панель-свойству выше.
+ $strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель')
$synonyms = @{
"commandBar" = "cmdBar"
"autoCommandBar" = "autoCmdBar"
@@ -2324,6 +2362,7 @@ function Emit-Element {
}
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
+ if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue }
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
@@ -2357,6 +2396,8 @@ function Emit-Element {
"radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1
# naming & binding
"name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1
+ # companion-панели (свойства): командная панель + контекстное меню
+ "commandBar"=1;"contextMenu"=1
# visibility & state
"visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1
# events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости)
@@ -2393,7 +2434,7 @@ function Emit-Element {
# pages-specific
"pagesRepresentation"=1
# button-specific
- "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1
+ "type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1
# picture/decoration
"src"=1;"valuesPicture"=1;"loadTransparent"=1
# cmdBar-specific
@@ -2709,7 +2750,7 @@ function Emit-Input {
}
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "input"
@@ -2742,7 +2783,7 @@ function Emit-Check {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "check"
@@ -2967,7 +3008,7 @@ function Emit-Radio {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"
@@ -3007,7 +3048,7 @@ function Emit-Label {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "label"
@@ -3033,7 +3074,7 @@ function Emit-LabelField {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField"
@@ -3136,9 +3177,11 @@ function Emit-Table {
}
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
- # AutoCommandBar — with optional Autofill control
- if ($null -ne $el.tableAutofill) {
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
+ # AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто.
+ if ($null -ne $el.commandBar) {
+ Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar
+ } elseif ($null -ne $el.tableAutofill) {
$acbId = New-Id
X "$inner"
$afVal = if ($el.tableAutofill) { "true" } else { "false" }
@@ -3285,6 +3328,10 @@ function Emit-Button {
if ($el.command) {
X "$innerForm.Command.$($el.command)"
}
+ # commandName — глобальная команда «как есть» (CommonCommand.X, Catalog.X.Command.Y …), без обёртки Form.
+ if ($el.commandName -and -not $el.command) {
+ X "$inner$($el.commandName)"
+ }
if ($el.stdCommand) {
$sc = "$($el.stdCommand)"
if ($sc -match '^(.+)\.(.+)$') {
@@ -3294,7 +3341,7 @@ function Emit-Button {
}
}
- $btnAuto = -not ($el.command -or $el.stdCommand)
+ $btnAuto = -not ($el.command -or $el.commandName -or $el.stdCommand)
Emit-Title -el $el -name $name -indent $inner -auto:$btnAuto
Emit-CommonFlags -el $el -indent $inner
@@ -3347,7 +3394,7 @@ function Emit-PictureDecoration {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture"
@@ -3381,7 +3428,7 @@ function Emit-PictureField {
Emit-Layout -el $el -indent $inner
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField"
@@ -3423,7 +3470,7 @@ function Emit-Calendar {
if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner$v" }
# Companions
- Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
+ Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar"
@@ -3755,17 +3802,59 @@ function Emit-Properties {
# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
+# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый
+# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами
+# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство.
+function Normalize-PanelSynonyms {
+ param($el)
+ if ($null -eq $el) { return }
+ $panelSyns = @{
+ 'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель')
+ 'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню')
+ }
+ foreach ($canon in $panelSyns.Keys) {
+ foreach ($syn in $panelSyns[$canon]) {
+ $p = $el.PSObject.Properties[$syn]
+ if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) {
+ if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) {
+ $v = $p.Value
+ $el.PSObject.Properties.Remove($syn) | Out-Null
+ $el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force
+ }
+ break
+ }
+ }
+ }
+}
+
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
- $synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar"; "extTooltip" = "extendedTooltip" }
- foreach ($pair in $synonyms.GetEnumerator()) {
- if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
+ Normalize-PanelSynonyms $el
+ # Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому
+ # значению (имя элемента); объект/массив уже отнесён к панель-свойству выше.
+ $typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
+ foreach ($pair in $typeSyn.GetEnumerator()) {
+ $src = $el.PSObject.Properties[$pair.Key]
+ if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
+ if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) {
+ $val = $el.extTooltip
+ $el.PSObject.Properties.Remove("extTooltip") | Out-Null
+ $el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force
+ }
+ # Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри
+ foreach ($pk in @('commandBar','contextMenu')) {
+ $pp = $el.PSObject.Properties[$pk]
+ if ($null -ne $pp) {
+ $kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null }
+ if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } }
+ }
+ }
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
}
diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py
index 68274fc2..b2870381 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.47 — Compile 1C managed form from JSON or object metadata
+# form-compile v1.48 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1779,7 +1779,8 @@ KNOWN_KEYS = {
"commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation",
"excludedCommands",
"pagesRepresentation",
- "type", "command", "stdCommand", "defaultButton", "locationInCommandBar",
+ "type", "command", "commandName", "stdCommand", "defaultButton", "locationInCommandBar",
+ "commandBar", "contextMenu",
"src", "valuesPicture", "loadTransparent",
"autofill",
"choiceMode", "initialTreeView", "enableDrag", "enableStartDrag",
@@ -1834,6 +1835,28 @@ ELEMENT_TYPE_SYNONYMS = {
"ВсплывающееМеню": "popup",
}
+# Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив
+# у того же слова — companion-панель (свойство), см. normalize_panel_synonyms.
+STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"}
+
+# Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника.
+PANEL_SYNONYMS = {
+ 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'],
+ 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'],
+}
+
+
+def normalize_panel_synonyms(el):
+ if not isinstance(el, dict):
+ return
+ for canon, syns in PANEL_SYNONYMS.items():
+ for syn in syns:
+ if syn in el and isinstance(el[syn], (list, dict)):
+ if syn != canon and canon not in el:
+ el[canon] = el.pop(syn)
+ break
+
+
# Maps Russian/English root of typed reference path to canonical English root
REF_ROOT_SYNONYMS = {
"Перечисление": "Enum",
@@ -2045,6 +2068,38 @@ def emit_companion(lines, tag, name, indent, content=None):
lines.append(f'{indent}{tag}>')
+def emit_companion_panel(lines, tag, name, indent, panel):
+ # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] }
+ # или массив = shorthand для { children }. Пусто/нет → self-closing.
+ cid = new_id()
+ autofill = None
+ halign = None
+ children = None
+ if isinstance(panel, list):
+ children = panel
+ elif panel is not None:
+ if panel.get('autofill') is not None:
+ autofill = bool(panel.get('autofill'))
+ if panel.get('horizontalAlign'):
+ halign = str(panel.get('horizontalAlign'))
+ children = panel.get('children')
+ has_children = bool(children) and len(children) > 0
+ if autofill is None and not has_children and not halign:
+ lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>')
+ return
+ lines.append(f'{indent}<{tag} name="{name}" id="{cid}">')
+ if halign:
+ lines.append(f'{indent}\t{halign}')
+ if autofill is not None:
+ lines.append(f'{indent}\t{"true" if autofill else "false"}')
+ if has_children:
+ lines.append(f'{indent}\t')
+ for c in children:
+ emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True)
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}{tag}>')
+
+
def emit_table_addition(lines, tag, table_name, name_suffix, src_type, indent):
# Табличный addition с AdditionSource (Item = имя таблицы, Type фиксирован).
add_name = f'{table_name}{name_suffix}'
@@ -2372,9 +2427,15 @@ def emit_type(lines, type_str, indent):
# --- Element emitters ---
def emit_element(lines, el, indent, in_cmd_bar=False):
- # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio)
+ # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов.
+ normalize_panel_synonyms(el)
+
+ # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
+ # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя).
for src, dst in ELEMENT_TYPE_SYNONYMS.items():
if src in el and dst not in el:
+ if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str):
+ continue
el[dst] = el.pop(src)
type_key = None
@@ -2562,7 +2623,7 @@ def emit_input(lines, el, name, eid, indent):
emit_mltext(lines, inner, 'InputHint', el['inputHint'])
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'input')
@@ -2595,7 +2656,7 @@ def emit_check(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'check')
@@ -2654,7 +2715,7 @@ def emit_radio_button_field(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
- emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'radio')
@@ -2693,7 +2754,7 @@ def emit_label(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'label')
@@ -2721,7 +2782,7 @@ def emit_label_field(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'labelField')
@@ -2834,9 +2895,11 @@ def emit_table(lines, el, name, eid, indent):
lines.append(f'{inner}')
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
# AutoCommandBar — with optional Autofill control
- if el.get('tableAutofill') is not None:
+ if el.get('commandBar') is not None:
+ emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar'))
+ elif el.get('tableAutofill') is not None:
acb_id = new_id()
acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
af_val = 'true' if el['tableAutofill'] else 'false'
@@ -2963,6 +3026,9 @@ def emit_button(lines, el, name, eid, indent, in_cmd_bar=False):
# CommandName
if el.get('command'):
lines.append(f'{inner}Form.Command.{el["command"]}')
+ # commandName — глобальная команда «как есть» (CommonCommand.X, Catalog.X.Command.Y …), без обёртки Form.
+ if el.get('commandName') and not el.get('command'):
+ lines.append(f'{inner}{el["commandName"]}')
if el.get('stdCommand'):
sc = str(el['stdCommand'])
m = re.match(r'^(.+)\.(.+)$', sc)
@@ -2971,7 +3037,7 @@ def emit_button(lines, el, name, eid, indent, in_cmd_bar=False):
else:
lines.append(f'{inner}Form.StandardCommand.{sc}')
- emit_title(lines, el, name, inner, auto=not (el.get('command') or el.get('stdCommand')))
+ emit_title(lines, el, name, inner, auto=not (el.get('command') or el.get('commandName') or el.get('stdCommand')))
emit_common_flags(lines, el, inner)
if el.get('defaultButton') is True:
@@ -3019,7 +3085,7 @@ def emit_picture_decoration(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'picture')
@@ -3053,7 +3119,7 @@ def emit_picture_field(lines, el, name, eid, indent):
emit_layout(lines, el, inner)
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'picField')
@@ -3091,7 +3157,7 @@ def emit_calendar(lines, el, name, eid, indent):
lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}')
# Companions
- emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu'))
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip'))
emit_events(lines, el, name, inner, 'calendar')
@@ -3646,10 +3712,22 @@ def main():
def _normalize_synonyms(el):
if not isinstance(el, dict):
return
+ # Companion-панели (объект/массив-значение) → commandBar/contextMenu
+ normalize_panel_synonyms(el)
+ # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении
synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'}
for src, dst in synonyms.items():
if src in el and dst not in el:
+ if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str):
+ continue
el[dst] = el.pop(src)
+ # Рекурсия в детей панелей (commandBar/contextMenu)
+ for pk in ('commandBar', 'contextMenu'):
+ pv = el.get(pk)
+ kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None)
+ if isinstance(kids, list):
+ for child in kids:
+ _normalize_synonyms(child)
if isinstance(el.get('children'), list):
for child in el['children']:
_normalize_synonyms(child)
diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1
index c5dd842c..03b3d8b6 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.28 — Decompile 1C managed Form.xml to JSON DSL (draft)
+# form-decompile v0.29 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -883,6 +883,28 @@ function Decompile-Children {
return ,@($list)
}
+# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом
+# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing).
+# $isDynListTable: для дин-список-таблицы пустой AutoCommandBar с autofill=false восстановит
+# эвристика компилятора — молчим (как с tableAutofill), чтобы не плодить ключ.
+function Decompile-CompanionPanel {
+ param($node, [string]$tag, [bool]$isDynListTable = $false)
+ $p = $node.SelectSingleNode("lf:$tag", $ns)
+ if (-not $p) { return $null }
+ $autofillRaw = Get-Child $p 'Autofill'
+ $halign = Get-Child $p 'HorizontalAlign'
+ $kids = Decompile-Children $p
+ $hasKids = $kids -and @($kids).Count -gt 0
+ if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null }
+ if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign -and $autofillRaw -eq 'false') { return $null }
+ $o = [ordered]@{}
+ if ($halign) { $o['horizontalAlign'] = $halign }
+ if ($autofillRaw -eq 'false') { $o['autofill'] = $false }
+ elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true }
+ if ($hasKids) { $o['children'] = $kids }
+ return $o
+}
+
function Decompile-Element {
param($node)
$tag = $node.LocalName
@@ -1091,7 +1113,7 @@ function Decompile-Element {
if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] }
elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] }
elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" }
- else { $obj['command'] = $cmd }
+ else { $obj['commandName'] = $cmd }
}
Add-CommonProps $obj $node $name
$type = Get-Child $node 'Type'
@@ -1129,7 +1151,7 @@ function Decompile-Element {
if (-not $obj.Contains('title')) {
$autoTitle = $false
if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true }
- elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('stdCommand')) }
+ elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) }
elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') }
if ($autoTitle) { $obj['title'] = '' }
}
@@ -1137,6 +1159,12 @@ function Decompile-Element {
# extendedTooltip: контент companion (любой элемент)
$etTitle = $node.SelectSingleNode("lf:ExtendedTooltip/lf:Title", $ns)
if ($etTitle) { $et = Get-MLFormattedValue $etTitle; if ($null -ne $et) { $obj['extendedTooltip'] = $et } }
+ # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент)
+ $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange')
+ $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable
+ if ($null -ne $cb) { $obj['commandBar'] = $cb }
+ $cm = Decompile-CompanionPanel $node 'ContextMenu'
+ if ($null -ne $cm) { $obj['contextMenu'] = $cm }
return $obj
}
diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md
index ddce04e7..d39f5855 100644
--- a/docs/form-dsl-spec.md
+++ b/docs/form-dsl-spec.md
@@ -115,6 +115,8 @@
| `disabled` | bool | `true` → `false` |
| `readOnly` | bool | `true` → `true` |
| `userVisible` | bool/object | Пользовательская видимость по ролям (``). См. §4.1c. Отсутствие = виден всем |
+| `commandBar` | object/array | Командная панель элемента (companion ``) с контентом. См. §4.1d |
+| `contextMenu` | object/array | Контекстное меню элемента (companion ``) с контентом. См. §4.1d |
| `events` | object | Обработчики событий: `{ "ИмяСобытия": "ИмяОбработчика" }` — тот же формат, что у событий формы (§3). Значение `null` → имя по конвенции (§4.2). См. §4.2 |
| `titleLocation` | string | Расположение заголовка: `none`/`left`/`right`/`top`/`bottom`/`auto`. Эмитится при наличии (input, labelField, picField, table, calendar). У `check`/`radio` — особая семантика с умным дефолтом (см. их разделы) |
| `tooltip` | string/object | Всплывающая подсказка элемента (``). Строка → ru, объект `{ "ru": …, "en": … }` → мультиязычный (как `title`). Эмитится сразу после `title` |
@@ -161,6 +163,37 @@
{ "name": "Команда", "use": { "common": false, "roles": { "Роль.Бухгалтер": true } } }
```
+### 4.1d. Companion-панели элемента (`commandBar` / `contextMenu`)
+
+Командная панель (``) и контекстное меню (``) элемента — это
+companion-панели с собственным контентом. Оба несут одну грамматику.
+
+| Ключ | XML companion | Forgiving-синонимы (при объект/массив-значении) |
+|------|---------------|--------------------------------------------------|
+| `commandBar` | `` | `autoCommandBar`, `AutoCommandBar`, `autoCmdBar`, `cmdBar`, `КоманднаяПанель` |
+| `contextMenu` | `` | `ContextMenu`, `КонтекстноеМеню` |
+
+**Значение** (обе формы):
+- массив `[ … ]` → shorthand для `{ "children": [ … ] }`;
+- объект `{ "autofill"?: bool, "children": [ … ] }` (+ `horizontalAlign` у `commandBar`).
+
+`children` — обычная грамматика кнопок: `button` (с `command`/`commandName`/`stdCommand`), `buttonGroup`, `popup`.
+
+- `autofill`: отсутствует → дефолт платформы (тег не эмитим); `false` → подавить автозаполнение панели.
+- Отсутствие свойства целиком → пустой companion (как обычно).
+
+**Разведение тип-элемента и панель-свойства — по типу значения:** `cmdBar: "Имя"` (строка) — это
+отдельный элемент-панель в дереве (§4.3); `commandBar: { … }` (объект/массив) — companion-панель *данного*
+элемента. Поэтому модель может писать панель таблицы любым знакомым словом.
+
+```jsonc
+{ "table": "Список", "path": "Список",
+ "commandBar": { "autofill": false, "children": [
+ { "button": "Создать", "command": "СоздатьЭлемент" } ] },
+ "contextMenu": { "children": [
+ { "button": "Карта", "commandName": "CommonCommand.КартаМаршрута" } ] } }
+```
+
### 4.1a. Общие layout-свойства
Применимы к любому элементу (размеры, растягивание, выравнивание внутри родителя). Эмитятся только при указании.
@@ -447,7 +480,8 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
| Свойство | Тип | Описание |
|----------|-----|----------|
| `command` | string | Имя команды формы (→ `Form.Command.`) |
-| `stdCommand` | string | Стандартная команда (→ `Form.StandardCommand.`) |
+| `commandName` | string | Глобальная команда «как есть» (`CommonCommand.X`, `Catalog.X.Command.Y` …) — без обёртки `Form.` |
+| `stdCommand` | string | Стандартная команда (→ `Form.StandardCommand.`; `X.Y` → `Form.Item.X.StandardCommand.Y`) |
| `type` | string | `usual`, `hyperlink`, `commandBar` |
| `defaultButton` | bool | Кнопка по умолчанию |
| `picture` | string | Ссылка на картинку (`StdPicture.Name`) |
diff --git a/tests/skills/cases/form-compile/snapshots/table/DataProcessors/Таблица/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/table/DataProcessors/Таблица/Forms/Форма/Ext/Form.xml
index 547ff475..2ae76856 100644
--- a/tests/skills/cases/form-compile/snapshots/table/DataProcessors/Таблица/Forms/Форма/Ext/Form.xml
+++ b/tests/skills/cases/form-compile/snapshots/table/DataProcessors/Таблица/Forms/Форма/Ext/Form.xml
@@ -21,60 +21,82 @@
MoveUp
MoveDown
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
- Данные
SearchStringRepresentation
-
-
+
+
-
+
- Данные
ViewStatusRepresentation
-
-
+
+
-
+
- Данные
SearchControl
-
-
+
+
-
+
Данные.Дата
-
-
+
+
-
+
Данные.Сумма
-
-
+
+
-
+
Данные.Комментарий
-
-
+
+
-
+
cfg:DataProcessorObject.Таблица
true
-
+
ru
@@ -85,7 +107,7 @@
v8:ValueTable
-
+
xs:dateTime
@@ -93,7 +115,7 @@
-
+
xs:decimal
@@ -103,7 +125,7 @@
-
+
xs:string
@@ -115,4 +137,15 @@
+
+
+
+
+ ru
+ Обновить
+
+
+ ОбновитьОбработка
+
+
diff --git a/tests/skills/cases/form-compile/table.json b/tests/skills/cases/form-compile/table.json
index 311d81f7..b6eb0e0c 100644
--- a/tests/skills/cases/form-compile/table.json
+++ b/tests/skills/cases/form-compile/table.json
@@ -18,7 +18,16 @@
"elements": [
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "titleLocation": "top",
"viewStatusLocation": "None", "searchControlLocation": "None",
- "excludedCommands": ["Add", "Delete", "MoveUp", "MoveDown"], "columns": [
+ "excludedCommands": ["Add", "Delete", "MoveUp", "MoveDown"],
+ "commandBar": { "autofill": false, "children": [
+ { "button": "ПанельОбновить", "command": "Обновить" }
+ ]},
+ "contextMenu": { "children": [
+ { "buttonGroup": "МенюГруппа", "children": [
+ { "button": "МенюОбновить", "command": "Обновить" }
+ ]}
+ ]},
+ "columns": [
{ "input": "Дата", "path": "Данные.Дата" },
{ "input": "Сумма", "path": "Данные.Сумма" },
{ "input": "Комментарий", "path": "Данные.Комментарий" }
@@ -31,6 +40,9 @@
{ "name": "Сумма", "type": "decimal(15,2)" },
{ "name": "Комментарий", "type": "string(200)" }
]}
+ ],
+ "commands": [
+ { "name": "Обновить", "action": "ОбновитьОбработка" }
]
}
}