feat(meta-compile,meta-decompile): команды объекта (Command + CommandModule.bsl) (v1.26/v0.11)

Новая возможность (не только раундтрип): meta-compile теперь умеет добавлять команды объекту
(раньше не умел вовсе). Корпус: 243 команды в 133 справочниках.

DSL `commands` (map имя→объект ИЛИ array): synonym/tooltip (ML, авто-синоним), comment, group,
commandParameterType (тип), parameterUseMode (Single), modifiesData (false), representation (Auto),
picture/shortcut, onMainServerUnavalableBehavior (Auto). Все omit-on-default.

- meta-compile (дуал-порт): Emit-Command → <Command>-блок в ChildObjects (после ТЧ) + генерация
  Commands/<Имя>/Ext/CommandModule.bsl с заготовкой обработчика ОбработкаКоманды.
- meta-decompile: захват команд из ChildObjects (тела модулей — вне скоупа, как ObjectModule).
- spec §7.1.3; тест-кейс catalog-command.

Валидация: PS==PY (XML+модуль); Command-категория 0; −6244 (53436→47192); регресс 38/38; 1С-cert
зелёный (справочник с командой+параметр-типом+модулем грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-03 16:58:03 +03:00
parent 1701bdac96
commit d8146e1f1b
11 changed files with 609 additions and 3 deletions
@@ -1068,6 +1068,42 @@ function Emit-Attribute {
X "$indent</Attribute>"
}
# --- 8b. Command emitter ---
# $cmd — объект свойств команды. Поля (omit-on-default): synonym/tooltip (ML), comment, group,
# commandParameterType (тип), parameterUseMode (Single), modifiesData (false), representation (Auto),
# picture, shortcut, onMainServerUnavalableBehavior (Auto).
function Emit-Command {
param([string]$indent, [string]$cmdName, $cmd)
X "$indent<Command uuid=`"$(New-Guid-String)`">"
X "$indent`t<Properties>"
X "$indent`t`t<Name>$(Esc-Xml $cmdName)</Name>"
$syn = if ($null -ne $cmd.synonym) { $cmd.synonym } else { Split-CamelCase $cmdName }
Emit-MLText "$indent`t`t" "Synonym" $syn
if ($cmd.comment) { X "$indent`t`t<Comment>$(Esc-XmlText "$($cmd.comment)")</Comment>" } else { X "$indent`t`t<Comment/>" }
$group = if ($cmd.group) { "$($cmd.group)" } else { "" }
X "$indent`t`t<Group>$group</Group>"
if ($cmd.commandParameterType) {
X "$indent`t`t<CommandParameterType>"
Emit-TypeContent "$indent`t`t`t" "$($cmd.commandParameterType)"
X "$indent`t`t</CommandParameterType>"
} else {
X "$indent`t`t<CommandParameterType/>"
}
$pum = if ($cmd.parameterUseMode) { "$($cmd.parameterUseMode)" } else { "Single" }
X "$indent`t`t<ParameterUseMode>$pum</ParameterUseMode>"
$md = if ($cmd.modifiesData -eq $true) { "true" } else { "false" }
X "$indent`t`t<ModifiesData>$md</ModifiesData>"
$rep = if ($cmd.representation) { "$($cmd.representation)" } else { "Auto" }
X "$indent`t`t<Representation>$rep</Representation>"
Emit-MLText "$indent`t`t" "ToolTip" $cmd.tooltip
if ($cmd.picture) { X "$indent`t`t<Picture>$(Esc-Xml "$($cmd.picture)")</Picture>" } else { X "$indent`t`t<Picture/>" }
if ($cmd.shortcut) { X "$indent`t`t<Shortcut>$(Esc-Xml "$($cmd.shortcut)")</Shortcut>" } else { X "$indent`t`t<Shortcut/>" }
$osu = if ($cmd.onMainServerUnavalableBehavior) { "$($cmd.onMainServerUnavalableBehavior)" } else { "Auto" }
X "$indent`t`t<OnMainServerUnavalableBehavior>$osu</OnMainServerUnavalableBehavior>"
X "$indent`t</Properties>"
X "$indent</Command>"
}
# --- 9. TabularSection emitter ---
function Emit-TabularSection {
@@ -2903,7 +2939,16 @@ if ($objType -in $typesWithAttrTS) {
$addrAttrs = @($def.addressingAttributes)
}
$childCount = $attrs.Count + $tsSections.Count + $acctFlags.Count + $extDimFlags.Count + $addrAttrs.Count
# Commands (map имя→объект ИЛИ array [{name,...}]) — генерируем блок + CommandModule.bsl-заготовку.
$commands = @()
if ($def.commands) {
if ($def.commands -is [array] -or $def.commands.GetType().Name -eq 'Object[]') {
foreach ($c in $def.commands) { $commands += @{ name = "$($c.name)"; def = $c } }
} else {
$def.commands.PSObject.Properties | ForEach-Object { $commands += @{ name = $_.Name; def = $_.Value } }
}
}
$childCount = $attrs.Count + $tsSections.Count + $acctFlags.Count + $extDimFlags.Count + $addrAttrs.Count + $commands.Count
if ($childCount -gt 0) {
$hasChildren = $true
X "`t`t<ChildObjects>"
@@ -2932,6 +2977,9 @@ if ($objType -in $typesWithAttrTS) {
foreach ($aa in $addrAttrs) {
Emit-AddressingAttribute "`t`t`t" $aa
}
foreach ($cmd in $commands) {
Emit-Command "`t`t`t" $cmd.name $cmd.def
}
X "`t`t</ChildObjects>"
} else {
X "`t`t<ChildObjects/>"
@@ -3266,6 +3314,18 @@ if ($objType -eq "Catalog" -and $def.predefined -and @($def.predefined).Count -g
$modulesCreated += $predefPath
}
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
if ($commands -and $commands.Count -gt 0) {
$cmdModuleStub = "&НаКлиенте`r`nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)`r`n`r`n`t// Вставьте обработчик команды.`r`n`r`nКонецПроцедуры`r`n"
foreach ($cmd in $commands) {
$cmdDir = Join-Path (Join-Path (Join-Path $objSubDir "Commands") $cmd.name) "Ext"
if (-not (Test-Path $cmdDir)) { New-Item -ItemType Directory -Path $cmdDir -Force | Out-Null }
$cmdModPath = Join-Path $cmdDir "CommandModule.bsl"
[System.IO.File]::WriteAllText($cmdModPath, $cmdModuleStub, $enc)
$modulesCreated += $cmdModPath
}
}
# --- 17. Register in Configuration.xml ---
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
@@ -1061,6 +1061,39 @@ def emit_attribute(indent, parsed, context):
# 9. TabularSection emitter
# ---------------------------------------------------------------------------
def emit_command(indent, cmd_name, cmd):
X(f'{indent}<Command uuid="{new_uuid()}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(cmd_name)}</Name>')
syn = cmd['synonym'] if cmd.get('synonym') is not None else split_camel_case(cmd_name)
emit_mltext(f'{indent}\t\t', 'Synonym', syn)
if cmd.get('comment'):
X(f'{indent}\t\t<Comment>{esc_xml_text(str(cmd["comment"]))}</Comment>')
else:
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t\t<Group>{cmd.get("group") or ""}</Group>')
if cmd.get('commandParameterType'):
X(f'{indent}\t\t<CommandParameterType>')
emit_type_content(f'{indent}\t\t\t', str(cmd['commandParameterType']))
X(f'{indent}\t\t</CommandParameterType>')
else:
X(f'{indent}\t\t<CommandParameterType/>')
X(f'{indent}\t\t<ParameterUseMode>{cmd.get("parameterUseMode") or "Single"}</ParameterUseMode>')
X(f'{indent}\t\t<ModifiesData>{"true" if cmd.get("modifiesData") is True else "false"}</ModifiesData>')
X(f'{indent}\t\t<Representation>{cmd.get("representation") or "Auto"}</Representation>')
emit_mltext(f'{indent}\t\t', 'ToolTip', cmd.get('tooltip'))
if cmd.get('picture'):
X(f'{indent}\t\t<Picture>{esc_xml(str(cmd["picture"]))}</Picture>')
else:
X(f'{indent}\t\t<Picture/>')
if cmd.get('shortcut'):
X(f'{indent}\t\t<Shortcut>{esc_xml(str(cmd["shortcut"]))}</Shortcut>')
else:
X(f'{indent}\t\t<Shortcut/>')
X(f'{indent}\t\t<OnMainServerUnavalableBehavior>{cmd.get("onMainServerUnavalableBehavior") or "Auto"}</OnMainServerUnavalableBehavior>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Command>')
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None):
uid = new_uuid()
X(f'{indent}<TabularSection uuid="{uid}">')
@@ -2579,6 +2612,7 @@ types_with_attr_ts = [
'BusinessProcess', 'Task',
]
commands = [] # заполняется внутри блока types_with_attr_ts; на уровне модуля для записи модулей команд
if obj_type in types_with_attr_ts:
def _as_list(val):
"""Normalize attributes: dict {"K":"V"} → ["K:V"], list/other → list."""
@@ -2623,7 +2657,17 @@ if obj_type in types_with_attr_ts:
addr_attrs = []
if obj_type == 'Task' and defn.get('addressingAttributes'):
addr_attrs = _as_list(defn['addressingAttributes'])
child_count = len(attrs) + len(ts_sections) + len(acct_flags) + len(ext_dim_flags) + len(addr_attrs)
# Commands (map имя→объект ИЛИ array [{name,...}])
commands = []
if defn.get('commands'):
cd = defn['commands']
if isinstance(cd, list):
for c in cd:
commands.append({'name': str(c.get('name', '')), 'def': c})
else:
for k, v in cd.items():
commands.append({'name': k, 'def': v})
child_count = len(attrs) + len(ts_sections) + len(acct_flags) + len(ext_dim_flags) + len(addr_attrs) + len(commands)
if child_count > 0:
has_children = True
X('\t\t<ChildObjects>')
@@ -2650,6 +2694,8 @@ if obj_type in types_with_attr_ts:
emit_ext_dimension_accounting_flag('\t\t\t', edf_name)
for aa in addr_attrs:
emit_addressing_attribute('\t\t\t', aa)
for cmd in commands:
emit_command('\t\t\t', cmd['name'], cmd['def'])
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
@@ -2935,6 +2981,16 @@ if obj_type == 'Catalog' and defn.get('predefined'):
write_utf8_bom(predef_path, predef_xml)
modules_created.append(predef_path)
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
if commands:
cmd_module_stub = '&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n'
for cmd in commands:
cmd_dir = os.path.join(obj_sub_dir, 'Commands', cmd['name'], 'Ext')
os.makedirs(cmd_dir, exist_ok=True)
cmd_mod_path = os.path.join(cmd_dir, 'CommandModule.bsl')
write_utf8_bom(cmd_mod_path, cmd_module_stub)
modules_created.append(cmd_mod_path)
# ---------------------------------------------------------------------------
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
# meta-decompile v0.10 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.11 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только
@@ -357,6 +357,32 @@ if ($childObjs) {
}
$dsl['tabularSections'] = $tsMap
}
# --- Commands (полноблочные <Command> в ChildObjects) → DSL commands (map имя→объект, omit-on-default).
# Тела модулей команд (CommandModule.bsl) — вне скоупа (как ObjectModule). ---
$cmdNodes = @($childObjs.SelectNodes('md:Command', $nsm))
if ($cmdNodes.Count -gt 0) {
$cmdMap = [ordered]@{}
foreach ($cm in $cmdNodes) {
$cp = $cm.SelectSingleNode('md:Properties', $nsm)
$cn = ($cp.SelectSingleNode('md:Name', $nsm)).InnerText
$o = [ordered]@{}
$syn = Get-MLValue ($cp.SelectSingleNode('md:Synonym', $nsm))
if ($syn -is [string]) { if ($syn -ne (Split-CamelWords $cn)) { $o['synonym'] = $syn } }
elseif ($null -ne $syn) { $o['synonym'] = $syn }
$cmtN = $cp.SelectSingleNode('md:Comment', $nsm); if ($cmtN -and $cmtN.InnerText) { $o['comment'] = $cmtN.InnerText }
$grpN = $cp.SelectSingleNode('md:Group', $nsm); if ($grpN -and $grpN.InnerText) { $o['group'] = $grpN.InnerText }
$cpt = Get-TypeShorthand ($cp.SelectSingleNode('md:CommandParameterType', $nsm)); if ($cpt) { $o['commandParameterType'] = $cpt }
$pumN = $cp.SelectSingleNode('md:ParameterUseMode', $nsm); if ($pumN -and $pumN.InnerText -ne 'Single') { $o['parameterUseMode'] = $pumN.InnerText }
$mdN = $cp.SelectSingleNode('md:ModifiesData', $nsm); if ($mdN -and $mdN.InnerText -eq 'true') { $o['modifiesData'] = $true }
$repN = $cp.SelectSingleNode('md:Representation', $nsm); if ($repN -and $repN.InnerText -ne 'Auto') { $o['representation'] = $repN.InnerText }
$ctt = Get-MLValue ($cp.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $ctt) { $o['tooltip'] = $ctt }
$picN = $cp.SelectSingleNode('md:Picture', $nsm); if ($picN -and $picN.InnerText) { $o['picture'] = $picN.InnerText }
$scN = $cp.SelectSingleNode('md:Shortcut', $nsm); if ($scN -and $scN.InnerText) { $o['shortcut'] = $scN.InnerText }
$osuN = $cp.SelectSingleNode('md:OnMainServerUnavalableBehavior', $nsm); if ($osuN -and $osuN.InnerText -ne 'Auto') { $o['onMainServerUnavalableBehavior'] = $osuN.InnerText }
$cmdMap[$cn] = $o
}
$dsl['commands'] = $cmdMap
}
}
# --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined.