feat(meta-compile,meta-decompile): ChoiceParameterLinks + ChoiceParameters реквизита (v1.22/v0.15)

Ограничение выбора реквизита (связи/параметры) — порт из form-compile
(структура реквизита ⟷ элемента формы совпадает для Links). DSL-ключи:
  • choiceParameterLinks — [{name, dataPath, valueChange?}] ИЛИ строки
    "name=dataPath[:DontChange]"; valueChange дефолт Clear.
  • choiceParameters — [{name, value?}] ИЛИ строки "name=value"; значение
    bool/число/строка/DTR ИЛИ массив (→ FixedArray); без value → nil.

ВАЖНО: в метаданных значение ChoiceParameters ПРЯМОЕ на app:value
(xsi:type=тип), БЕЗ обёртки FormChoiceListDesTimeValue/Presentation (в отличие
от формы) — подтверждено survey (0 wrapper на 591 значений). Массив → app:value
xsi:type=v8:FixedArray с детьми v8:Value. Декомпилятор захватывает оба блока
(namespace app; valueChange=Clear → компактная строка).

remaining ChoiceParameterLinks/ChoiceParameters/value=0 на выборке (rt-cp),
регресс 43/43 ps1+py, ps1↔py идентичны. spec §4.2, кейс catalog-attr-choice-params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-03 21:36:08 +03:00
co-authored by Claude Opus 4.8
parent 9fec03bfb6
commit 65de4e5c97
10 changed files with 915 additions and 7 deletions
@@ -1,4 +1,4 @@
# meta-compile v1.21 — Compile 1C metadata object from JSON
# meta-compile v1.22 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -826,6 +826,8 @@ function Parse-AttributeShorthand {
hasFillValue = ($val.PSObject -and $val.PSObject.Properties -and ($val.PSObject.Properties.Name -contains 'fillValue'))
fillValue = $val.fillValue
linkByType = $val.linkByType
choiceParameterLinks = $val.choiceParameterLinks
choiceParameters = $val.choiceParameters
}
}
@@ -1154,6 +1156,133 @@ function Emit-LinkByType {
X "$indent</LinkByType>"
}
# --- Параметры/связи выбора (порт из form-compile; структура реквизита ⟷ элемента формы совпадает) ---
# Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null).
function Get-ChElProp {
param($obj, [string[]]$names)
if ($null -eq $obj) { return $null }
foreach ($n in $names) {
if ($obj -is [System.Collections.IDictionary]) { if ($obj.Contains($n)) { return $obj[$n] } }
elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { return $obj.PSObject.Properties[$n].Value }
}
return $null
}
# Строковый литерал shorthand → скаляр: true/false→bool, целое/дробное→число, иначе строка.
function ConvertTo-ChScalar {
param([string]$s)
$t = "$s".Trim()
if ($t -match '^(?i:true|истина)$') { return $true }
if ($t -match '^(?i:false|ложь)$') { return $false }
if ($t -match '^-?\d+$') { return [int]$t }
if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
return $t
}
# Значение параметра выбора → @{XsiType; Text}. Авто-детект по значению (без типа реквизита).
function Normalize-ChoiceValue {
param($value)
if ($value -is [bool]) { return @{ XsiType='xs:boolean'; Text=$(if ($value) { 'true' } else { 'false' }) } }
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType='xs:decimal'; Text=(Format-FillNum $value) }
}
$s = "$value"
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { return @{ XsiType='xs:dateTime'; Text=$s } }
$ref = Normalize-FillRef $s
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
return @{ XsiType='xs:string'; Text=$s }
}
# Shorthand "name=value" | "name=v1, v2" → {name, value}. "name=path" для links.
function ConvertFrom-ChParamShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$name = $s.Substring(0, $eq).Trim(); $rest = $s.Substring($eq + 1)
if ($rest -match ',') {
$vals = @(); foreach ($p in ($rest -split ',')) { $vals += ,(ConvertTo-ChScalar $p) }
return @{ name = $name; value = $vals }
}
return @{ name = $name; value = (ConvertTo-ChScalar $rest) }
}
function ConvertFrom-ChLinkShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$o = @{ name = $s.Substring(0, $eq).Trim() }; $rest = $s.Substring($eq + 1).Trim()
if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] }
else { $o['dataPath'] = $rest }
return $o
}
# <ChoiceParameters> — [{name, value?}]. Значение ПРЯМО на app:value (xsi:type=тип); массив → v8:FixedArray
# с детьми v8:Value; без value → app:value nil.
function Emit-ChoiceParameters {
param([string]$indent, $cp)
if (-not $cp -or @($cp).Count -eq 0) { X "$indent<ChoiceParameters/>"; return }
X "$indent<ChoiceParameters>"
foreach ($item in @($cp)) {
if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
$name = Get-ChElProp $item @('name','имя')
$hasVal = $false; $val = $null
if ($item -is [System.Collections.IDictionary]) {
if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
} elseif ($item.PSObject) {
if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
}
$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
X "$indent`t<app:item name=`"$(Esc-Xml "$name")`">"
if (-not $hasVal) {
X "$indent`t`t<app:value xsi:nil=`"true`"/>"
} elseif ($valIsArray) {
X "$indent`t`t<app:value xsi:type=`"v8:FixedArray`">"
foreach ($v in $val) {
$norm = Normalize-ChoiceValue -value $v
if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`"/>" }
else { X "$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</v8:Value>" }
}
X "$indent`t`t</app:value>"
} else {
$norm = Normalize-ChoiceValue -value $val
if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`"/>" }
else { X "$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</app:value>" }
}
X "$indent`t</app:item>"
}
X "$indent</ChoiceParameters>"
}
# <ChoiceParameterLinks> — [{name, dataPath, valueChange?}]. valueChange дефолт Clear.
function Emit-ChoiceParameterLinks {
param([string]$indent, $cpl)
if (-not $cpl -or @($cpl).Count -eq 0) { X "$indent<ChoiceParameterLinks/>"; return }
X "$indent<ChoiceParameterLinks>"
foreach ($lk in @($cpl)) {
if ($lk -is [string]) { $lk = ConvertFrom-ChLinkShorthand $lk }
$name = Get-ChElProp $lk @('name','имя')
$dp = Get-ChElProp $lk @('dataPath','path','путь')
$vcRaw = Get-ChElProp $lk @('valueChange','режимИзменения')
$vc = 'Clear'
if ($vcRaw) {
$vc = switch -Regex ("$vcRaw".ToLower()) {
'^(clear|очистить|очистка)$' { 'Clear'; break }
'^(dontchange|неизменять|неменять|нет)$' { 'DontChange'; break }
default { "$vcRaw" }
}
}
X "$indent`t<xr:Link>"
X "$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>"
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>"
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
X "$indent`t</xr:Link>"
}
X "$indent</ChoiceParameterLinks>"
}
function Emit-Attribute {
param([string]$indent, $parsed, [string]$context)
# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register"
@@ -1221,8 +1350,8 @@ function Emit-Attribute {
X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"
X "$indent`t`t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>"
X "$indent`t`t<ChoiceParameterLinks/>"
X "$indent`t`t<ChoiceParameters/>"
Emit-ChoiceParameterLinks "$indent`t`t" $parsed.choiceParameterLinks
Emit-ChoiceParameters "$indent`t`t" $parsed.choiceParameters
$qc = if ($parsed.quickChoice) { $parsed.quickChoice } else { "Auto" }
X "$indent`t`t<QuickChoice>$qc</QuickChoice>"
$coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.21 — Compile 1C metadata object from JSON
# meta-compile v1.22 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -850,6 +850,8 @@ def parse_attribute_shorthand(val):
'hasFillValue': ('fillValue' in val),
'fillValue': val.get('fillValue'),
'linkByType': val.get('linkByType'),
'choiceParameterLinks': val.get('choiceParameterLinks'),
'choiceParameters': val.get('choiceParameters'),
}
def parse_enum_value_shorthand(val):
@@ -1200,6 +1202,133 @@ def emit_link_by_type(indent, spec):
X(f'{indent}\t<xr:LinkItem>{li}</xr:LinkItem>')
X(f'{indent}</LinkByType>')
# --- Параметры/связи выбора (порт из form-compile) ---
def ch_el_prop(obj, names):
if obj is None:
return None
if isinstance(obj, dict):
for n in names:
if n in obj:
return obj[n]
return None
def convert_to_ch_scalar(s):
t = str(s).strip()
if re.match(r'^(?i:true|истина)$', t):
return True
if re.match(r'^(?i:false|ложь)$', t):
return False
if re.match(r'^-?\d+$', t):
return int(t)
if re.match(r'^-?\d+\.\d+$', t):
return float(t)
return t
def normalize_choice_value(value):
"""Значение параметра выбора → (xsi_type, text). Авто-детект по значению."""
if isinstance(value, bool):
return ('xs:boolean', 'true' if value else 'false')
if isinstance(value, (int, float)):
return ('xs:decimal', format_fill_num(value))
s = str(value)
if s == '':
return ('xs:string', '')
if re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$', s):
return ('xs:dateTime', s)
ref = normalize_fill_ref(s)
if ref:
return ('xr:DesignTimeRef', ref)
return ('xs:string', s)
def convert_from_ch_param_shorthand(s):
eq = s.find('=')
if eq < 0:
return {'name': s.strip()}
name = s[:eq].strip()
rest = s[eq + 1:]
if ',' in rest:
return {'name': name, 'value': [convert_to_ch_scalar(p) for p in rest.split(',')]}
return {'name': name, 'value': convert_to_ch_scalar(rest)}
def convert_from_ch_link_shorthand(s):
eq = s.find('=')
if eq < 0:
return {'name': s.strip()}
o = {'name': s[:eq].strip()}
rest = s[eq + 1:].strip()
m = re.match(r'^(.*):(?i:(Clear|DontChange|очистить|неизменять))$', rest)
if m:
o['dataPath'] = m.group(1).strip()
o['valueChange'] = m.group(2)
else:
o['dataPath'] = rest
return o
def emit_choice_parameters(indent, cp):
if not cp:
X(f'{indent}<ChoiceParameters/>')
return
if isinstance(cp, (str, dict)):
cp = [cp]
X(f'{indent}<ChoiceParameters>')
for item in cp:
if isinstance(item, str):
item = convert_from_ch_param_shorthand(item)
name = ch_el_prop(item, ['name', 'имя'])
has_val = isinstance(item, dict) and ('value' in item or 'значение' in item)
val = item.get('value', item.get('значение')) if has_val else None
val_is_array = isinstance(val, (list, tuple))
X(f'{indent}\t<app:item name="{esc_xml(str(name))}">')
if not has_val:
X(f'{indent}\t\t<app:value xsi:nil="true"/>')
elif val_is_array:
X(f'{indent}\t\t<app:value xsi:type="v8:FixedArray">')
for v in val:
xt, tx = normalize_choice_value(v)
if tx == '' or tx is None:
X(f'{indent}\t\t\t<v8:Value xsi:type="{xt}"/>')
else:
X(f'{indent}\t\t\t<v8:Value xsi:type="{xt}">{esc_xml(tx)}</v8:Value>')
X(f'{indent}\t\t</app:value>')
else:
xt, tx = normalize_choice_value(val)
if tx == '' or tx is None:
X(f'{indent}\t\t<app:value xsi:type="{xt}"/>')
else:
X(f'{indent}\t\t<app:value xsi:type="{xt}">{esc_xml(tx)}</app:value>')
X(f'{indent}\t</app:item>')
X(f'{indent}</ChoiceParameters>')
def emit_choice_parameter_links(indent, cpl):
if not cpl:
X(f'{indent}<ChoiceParameterLinks/>')
return
if isinstance(cpl, (str, dict)):
cpl = [cpl]
X(f'{indent}<ChoiceParameterLinks>')
for lk in cpl:
if isinstance(lk, str):
lk = convert_from_ch_link_shorthand(lk)
name = ch_el_prop(lk, ['name', 'имя'])
dp = ch_el_prop(lk, ['dataPath', 'path', 'путь'])
vc_raw = ch_el_prop(lk, ['valueChange', 'режимИзменения'])
vc = 'Clear'
if vc_raw:
low = str(vc_raw).lower()
if re.match(r'^(clear|очистить|очистка)$', low):
vc = 'Clear'
elif re.match(r'^(dontchange|неизменять|неменять|нет)$', low):
vc = 'DontChange'
else:
vc = str(vc_raw)
X(f'{indent}\t<xr:Link>')
X(f'{indent}\t\t<xr:Name>{esc_xml(str(name))}</xr:Name>')
X(f'{indent}\t\t<xr:DataPath xsi:type="xs:string">{esc_xml(str(dp))}</xr:DataPath>')
X(f'{indent}\t\t<xr:ValueChange>{vc}</xr:ValueChange>')
X(f'{indent}\t</xr:Link>')
X(f'{indent}</ChoiceParameterLinks>')
def emit_attribute(indent, parsed, context):
attr_name = parsed['name']
ctx_reserved = RESERVED_BY_CONTEXT.get(context)
@@ -1254,8 +1383,8 @@ def emit_attribute(indent, parsed, context):
fill_checking = parsed['fillChecking']
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
X(f'{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>')
X(f'{indent}\t\t<ChoiceParameterLinks/>')
X(f'{indent}\t\t<ChoiceParameters/>')
emit_choice_parameter_links(f'{indent}\t\t', parsed.get('choiceParameterLinks'))
emit_choice_parameters(f'{indent}\t\t', parsed.get('choiceParameters'))
X(f'{indent}\t\t<QuickChoice>{parsed.get("quickChoice") or "Auto"}</QuickChoice>')
X(f'{indent}\t\t<CreateOnInput>{parsed.get("createOnInput") or "Auto"}</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
@@ -1,4 +1,4 @@
# meta-decompile v0.14 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.15 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только
@@ -79,6 +79,7 @@ $nsm.AddNamespace('md', 'http://v8.1c.ru/8.3/MDClasses')
$nsm.AddNamespace('v8', 'http://v8.1c.ru/8.1/data/core')
$nsm.AddNamespace('xr', 'http://v8.1c.ru/8.3/xcf/readable')
$nsm.AddNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
$nsm.AddNamespace('app', 'http://v8.1c.ru/8.2/managed-application/core')
$rootEl = $doc.DocumentElement
if ($rootEl.LocalName -ne 'MetaDataObject') {
@@ -183,6 +184,31 @@ function Get-TypeShorthand {
return ($parts -join ' + ')
}
# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
function Convert-ChScalarNode {
param($vN)
$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
$txt = $vN.InnerText
if ($xt -match 'boolean$') { return ($txt -eq 'true') }
if ($xt -match 'decimal$') {
if ($txt -match '^-?\d+$') { return [int]$txt }
return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
}
return $txt
}
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
function Get-ChoiceParamValue {
param($valNode)
$xt = $valNode.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
if ($xt -match 'FixedArray$') {
$arr = [System.Collections.ArrayList]@()
foreach ($sub in @($valNode.SelectNodes('v8:Value', $nsm))) { [void]$arr.Add((Convert-ChScalarNode $sub)) }
return $arr
}
return Convert-ChScalarNode $valNode
}
# --- Реквизит → DSL: shorthand-строка "Имя: Тип | флаги" ЛИБО object-форма при кастомном синониме.
# (Синоним ≠ авто → object {name, type, synonym, [flags]}; иначе компактный shorthand.) ---
function Attr-ToDsl {
@@ -263,6 +289,46 @@ function Attr-ToDsl {
}
}
# ChoiceParameterLinks — [{name, dataPath, valueChange?}]. valueChange=Clear → компактно строкой "name=dataPath".
$cplNode = $ap.SelectSingleNode('md:ChoiceParameterLinks', $nsm)
if ($cplNode) {
$links = @($cplNode.SelectNodes('xr:Link', $nsm))
if ($links.Count -gt 0) {
$arr = [System.Collections.ArrayList]@()
foreach ($lk in $links) {
$lName = $lk.SelectSingleNode('xr:Name', $nsm).InnerText
$lDp = $lk.SelectSingleNode('xr:DataPath', $nsm).InnerText
$vcN = $lk.SelectSingleNode('xr:ValueChange', $nsm)
$vcv = if ($vcN) { $vcN.InnerText } else { 'Clear' }
if ($vcv -eq 'Clear') { [void]$arr.Add("$lName=$lDp") }
else { [void]$arr.Add([ordered]@{ name = $lName; dataPath = $lDp; valueChange = $vcv }) }
}
$extra['choiceParameterLinks'] = $arr
}
}
# ChoiceParameters — [{name, value?}]. app:value nil → без value; иначе типизированное значение.
$cpNode = $ap.SelectSingleNode('md:ChoiceParameters', $nsm)
if ($cpNode) {
$items = @($cpNode.SelectNodes('app:item', $nsm))
if ($items.Count -gt 0) {
$arr = [System.Collections.ArrayList]@()
foreach ($it in $items) {
$pName = $it.GetAttribute('name')
$valN = $it.SelectSingleNode('app:value', $nsm)
$nilAttr = if ($valN) { $valN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') } else { '' }
if (-not $valN -or $nilAttr -eq 'true') {
[void]$arr.Add([ordered]@{ name = $pName })
} else {
$o = [ordered]@{ name = $pName }
$o['value'] = Get-ChoiceParamValue $valN
[void]$arr.Add($o)
}
}
$extra['choiceParameters'] = $arr
}
}
if ($synCustom -or ($null -ne $ttVal) -or $extra.Count -gt 0) {
$o = [ordered]@{ name = $nm }
if ($ts) { $o['type'] = $ts }