feat(meta-edit): структурное свойство ChoiceParameters + фикс namespace app (Фаза 2 Шаг 3b-2a, v1.16)

modify-attribute/-dimension/-resource: ChoiceParameters ([{name, type?, value?}]) —
порт Emit-ChoiceParameters + полный кластер fill-ref машинерии в meta-edit
(fillRefRoots/fillRefKindRoot/fillEmptyRefWords/accountTypeValues, Normalize-FillRef,
Normalize-ChoiceValue/T, Expand-ChoiceRefValue, ConvertFrom-ChParamShorthand,
ConvertTo-ChScalar, Format-FillNum). Значение → app:value с xsi:type (bool/decimal/
DesignTimeRef); массив → v8:FixedArray/v8:Value; type разворачивает голые ref-имена
(EnumRef.X + "Оптовая" → Enum.X.EnumValue.Оптовая).

КРИТИЧНО: Import-Fragment (ps1+py) теперь объявляет xmlns:app + xmlns:ent — иначе
app:item/app:value падали бы "undeclared prefix". app/ent объявлены в корне 1С-файлов
→ вставка без per-element xmlns.

Cert декомпиляцией: выход meta-edit БАЙТ-В-БАЙТ = meta-compile (ps1 и py; включая
v8:FixedArray и DesignTimeRef-разворот). verify-snapshots --case modify-attribute-structural
(расширен ChoiceParameters bool+EmptyRef) грузится в 1С. meta-edit 14/14 ps1+py.

Остаток Шага 3: fillValue явный (нужна экстракция типа реквизита из XML).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-12 10:41:52 +03:00
parent 41df320153
commit 39dc3fc1e7
4 changed files with 350 additions and 4 deletions
+155 -1
View File
@@ -1,4 +1,4 @@
# meta-edit v1.15 — Edit existing 1C metadata object XML (+structural attr props Format/EditFormat/ToolTip/ChoiceForm/MinValue/MaxValue/LinkByType/ChoiceParameterLinks)
# meta-edit v1.16 — Edit existing 1C metadata object XML (+structural attr props Format/EditFormat/ToolTip/ChoiceForm/MinValue/MaxValue/LinkByType/ChoiceParameterLinks/ChoiceParameters)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -607,6 +607,8 @@ function Import-Fragment([string]$xmlString) {
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"
xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"
xmlns:app="http://v8.1c.ru/8.2/managed-application/core"
xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"
xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString</_W>
"@
$frag = New-Object System.Xml.XmlDocument
@@ -2280,6 +2282,11 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
Info "Set $xmlTag '$elemName'.ChoiceParameterLinks"; $script:modifyCount++
}
}
"ChoiceParameters" {
if (Set-AttrPropertyElement $propsEl "ChoiceParameters" (Build-ChoiceParametersXml (Get-ChildIndent $propsEl) $changeValue)) {
Info "Set $xmlTag '$elemName'.ChoiceParameters"; $script:modifyCount++
}
}
default {
# Scalar property change (Indexing, FillChecking, Use, etc.)
$scalarEl = $null
@@ -2544,6 +2551,153 @@ function Build-ChoiceParameterLinksXml([string]$indent, $cpl) {
return $sb.ToString()
}
# --- Порт из meta-compile: значения параметров выбора (ChoiceParameters) ---
$script:fillRefRoots = @{
'перечисление'='Enum'; 'справочник'='Catalog'; 'документ'='Document';
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
'enum'='Enum'; 'catalog'='Catalog'; 'document'='Document'; 'chartofaccounts'='ChartOfAccounts';
'chartofcharacteristictypes'='ChartOfCharacteristicTypes'; 'chartofcalculationtypes'='ChartOfCalculationTypes';
'exchangeplan'='ExchangePlan'; 'businessprocess'='BusinessProcess'; 'task'='Task'
}
$script:fillEmptyRefWords = @('emptyref','пустаяссылка')
$script:fillEnumValWords = @('enumvalue','значениеперечисления')
$script:accountTypeValues = @('Active','Passive','ActivePassive')
$script:fillRefKindRoot = @{
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
'businessprocessref'='BusinessProcess'; 'taskref'='Task'
}
function ConvertTo-ChScalar([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
}
function Format-FillNum($n) {
if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) }
return "$n"
}
function Normalize-FillRef([string]$s) {
if ([string]::IsNullOrEmpty($s)) { return $null }
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$') { return $s }
$parts = $s -split '\.'
if ($parts.Count -lt 2) { return $null }
$root = $script:fillRefRoots[$parts[0].ToLower()]
if (-not $root) { return $null }
$typeName = $parts[1]
if ($root -eq 'Enum') {
if ($parts.Count -eq 2) { return $null }
if ($parts.Count -eq 3) {
if ($script:fillEmptyRefWords -contains $parts[2].ToLower()) { return "Enum.$typeName.EmptyRef" }
return "Enum.$typeName.EnumValue.$($parts[2])"
}
$member = $parts[2]
if ($script:fillEnumValWords -contains $member.ToLower()) { $rest = $parts[3..($parts.Count-1)] -join '.' }
else { $rest = $parts[2..($parts.Count-1)] -join '.' }
return "Enum.$typeName.EnumValue.$rest"
}
$tail = @($parts[1..($parts.Count-1)])
for ($i = 0; $i -lt $tail.Count; $i++) {
if ($script:fillEmptyRefWords -contains $tail[$i].ToLower()) { $tail[$i] = 'EmptyRef' }
}
return "$root." + ($tail -join '.')
}
function Expand-ChoiceRefValue([string]$value, [string]$typeStr) {
if (-not $typeStr) { return $null }
$t = Resolve-TypeStr $typeStr
$root = $null; $tn = $null
if ($t -match '^(\w+Ref)\.(.+)$') { $root = $script:fillRefKindRoot[$Matches[1].ToLower()]; $tn = $Matches[2] }
elseif ($t -match '^([^.]+)\.(.+)$') { $root = $script:fillRefRoots[$Matches[1].ToLower()]; $tn = $Matches[2] }
if (-not $root) { return $null }
if ($script:fillEmptyRefWords -contains "$value".ToLower()) { return "$root.$tn.EmptyRef" }
if ($root -eq 'Enum') { return "Enum.$tn.EnumValue.$value" }
return "$root.$tn.$value"
}
function Normalize-ChoiceValue($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 } }
if ($script:accountTypeValues -contains $s) { return @{ XsiType='ent:AccountType'; Text=$s } }
return @{ XsiType='xs:string'; Text=$s }
}
function Normalize-ChoiceValueT($value, [string]$typeStr) {
if ($typeStr -and ($value -is [string]) -and (-not "$value".Contains('.'))) {
$ex = Expand-ChoiceRefValue "$value" $typeStr
if ($ex) { return @{ XsiType='xr:DesignTimeRef'; Text=$ex } }
}
return Normalize-ChoiceValue $value
}
function ConvertFrom-ChParamShorthand([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) }
}
# ChoiceParameters — [{name, type?, value?}] (порт Emit-ChoiceParameters). Значение на app:value (xsi:type=тип);
# массив → v8:FixedArray с v8:Value; без value → nil. Требует xmlns:app в Import-Fragment.
function Build-ChoiceParametersXml([string]$indent, $cp) {
if (-not $cp -or @($cp).Count -eq 0) { return "$indent<ChoiceParameters/>" }
$sb = New-Object System.Text.StringBuilder
$sb.Append("$indent<ChoiceParameters>") | Out-Null
foreach ($item in @($cp)) {
if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
$name = Get-ChElProp $item @('name','имя')
$ptype = Get-ChElProp $item @('type','тип')
$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])
$sb.Append("`r`n$indent`t<app:item name=`"$(Esc-Xml "$name")`">") | Out-Null
if (-not $hasVal) {
$sb.Append("`r`n$indent`t`t<app:value xsi:nil=`"true`"/>") | Out-Null
} elseif ($valIsArray) {
$sb.Append("`r`n$indent`t`t<app:value xsi:type=`"v8:FixedArray`">") | Out-Null
foreach ($v in $val) {
$norm = Normalize-ChoiceValueT $v $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
else { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</v8:Value>") | Out-Null }
}
$sb.Append("`r`n$indent`t`t</app:value>") | Out-Null
} else {
$norm = Normalize-ChoiceValueT $val $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
else { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</app:value>") | Out-Null }
}
$sb.Append("`r`n$indent`t</app:item>") | Out-Null
}
$sb.Append("`r`n$indent</ChoiceParameters>") | Out-Null
return $sb.ToString()
}
function Find-PropertyElement([string]$propName) {
foreach ($child in $script:propertiesEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) {
+182 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.15 — Edit existing 1C metadata object XML (+structural attr props Format/EditFormat/ToolTip/ChoiceForm/MinValue/MaxValue/LinkByType/ChoiceParameterLinks)
# meta-edit v1.16 — Edit existing 1C metadata object XML (+structural attr props Format/EditFormat/ToolTip/ChoiceForm/MinValue/MaxValue/LinkByType/ChoiceParameterLinks/ChoiceParameters)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -567,6 +567,8 @@ def import_fragment(xml_string):
f' xmlns:v8="{V8_NS}"'
f' xmlns:xr="{XR_NS}"'
f' xmlns:cfg="{CFG_NS}"'
f' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
f' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
f' xmlns:xs="{XS_NS}">'
f"{xml_string}</_W>"
)
@@ -2121,6 +2123,10 @@ def modify_child_elements(modify_def, child_type):
if set_attr_property_element(props_el, "ChoiceParameterLinks", build_choice_parameter_links_xml(get_child_indent(props_el), change_value)):
info(f"Set {xml_tag} '{elem_name}'.ChoiceParameterLinks")
modify_count += 1
elif change_prop == "ChoiceParameters":
if set_attr_property_element(props_el, "ChoiceParameters", build_choice_parameters_xml(get_child_indent(props_el), change_value)):
info(f"Set {xml_tag} '{elem_name}'.ChoiceParameters")
modify_count += 1
else:
# Scalar property change (Indexing, FillChecking, Use, etc.)
@@ -2396,6 +2402,181 @@ def build_choice_parameter_links_xml(indent, cpl):
return "\r\n".join(parts)
# --- Порт из meta-compile: значения параметров выбора (ChoiceParameters) ---
fill_ref_roots = {
'перечисление': 'Enum', 'справочник': 'Catalog', 'документ': 'Document',
'плансчетов': 'ChartOfAccounts', 'планвидовхарактеристик': 'ChartOfCharacteristicTypes',
'планвидоврасчета': 'ChartOfCalculationTypes', 'планвидоврасчёта': 'ChartOfCalculationTypes',
'планобмена': 'ExchangePlan', 'бизнеспроцесс': 'BusinessProcess', 'задача': 'Task',
'enum': 'Enum', 'catalog': 'Catalog', 'document': 'Document', 'chartofaccounts': 'ChartOfAccounts',
'chartofcharacteristictypes': 'ChartOfCharacteristicTypes', 'chartofcalculationtypes': 'ChartOfCalculationTypes',
'exchangeplan': 'ExchangePlan', 'businessprocess': 'BusinessProcess', 'task': 'Task',
}
fill_empty_ref_words = {'emptyref', 'пустаяссылка'}
fill_enum_val_words = {'enumvalue', 'значениеперечисления'}
account_type_values = ('Active', 'Passive', 'ActivePassive')
fill_ref_kind_root = {
'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum',
'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes',
'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan',
'businessprocessref': 'BusinessProcess', 'taskref': 'Task',
}
def convert_to_ch_scalar(s):
t = str(s).strip()
if re.match(r'^(true|истина)$', t, re.IGNORECASE):
return True
if re.match(r'^(false|ложь)$', t, re.IGNORECASE):
return False
if re.match(r'^-?\d+$', t):
return int(t)
if re.match(r'^-?\d+\.\d+$', t):
return float(t)
return t
def format_fill_num(n):
if isinstance(n, float):
return str(int(n)) if n == int(n) else repr(n)
return str(n)
def normalize_fill_ref(s):
if not s:
return None
if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$', s):
return s
parts = s.split('.')
if len(parts) < 2:
return None
root = fill_ref_roots.get(parts[0].lower())
if not root:
return None
type_name = parts[1]
if root == 'Enum':
if len(parts) == 2:
return None
if len(parts) == 3:
if parts[2].lower() in fill_empty_ref_words:
return f"Enum.{type_name}.EmptyRef"
return f"Enum.{type_name}.EnumValue.{parts[2]}"
member = parts[2]
rest = '.'.join(parts[3:]) if member.lower() in fill_enum_val_words else '.'.join(parts[2:])
return f"Enum.{type_name}.EnumValue.{rest}"
tail = list(parts[1:])
for i in range(len(tail)):
if tail[i].lower() in fill_empty_ref_words:
tail[i] = 'EmptyRef'
return f"{root}." + '.'.join(tail)
def expand_choice_ref_value(value, type_str):
if not type_str:
return None
t = resolve_type_str(type_str)
root = None
tn = None
m = re.match(r'^(\w+Ref)\.(.+)$', t)
if m:
root = fill_ref_kind_root.get(m.group(1).lower())
tn = m.group(2)
else:
m2 = re.match(r'^([^.]+)\.(.+)$', t)
if m2:
root = fill_ref_roots.get(m2.group(1).lower())
tn = m2.group(2)
if not root:
return None
if str(value).lower() in fill_empty_ref_words:
return f"{root}.{tn}.EmptyRef"
if root == 'Enum':
return f"Enum.{tn}.EnumValue.{value}"
return f"{root}.{tn}.{value}"
def normalize_choice_value(value):
if isinstance(value, bool):
return {'XsiType': 'xs:boolean', 'Text': 'true' if value else 'false'}
if isinstance(value, (int, float)):
return {'XsiType': 'xs:decimal', 'Text': format_fill_num(value)}
s = str(value)
if s == '':
return {'XsiType': 'xs:string', 'Text': ''}
if re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$', s):
return {'XsiType': 'xs:dateTime', 'Text': s}
ref = normalize_fill_ref(s)
if ref:
return {'XsiType': 'xr:DesignTimeRef', 'Text': ref}
if s in account_type_values:
return {'XsiType': 'ent:AccountType', 'Text': s}
return {'XsiType': 'xs:string', 'Text': s}
def normalize_choice_value_t(value, type_str):
if type_str and isinstance(value, str) and '.' not in str(value):
ex = expand_choice_ref_value(value, type_str)
if ex:
return {'XsiType': 'xr:DesignTimeRef', 'Text': ex}
return normalize_choice_value(value)
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 build_choice_parameters_xml(indent, cp):
"""ChoiceParameters — [{name, type?, value?}] (порт Emit-ChoiceParameters). Требует xmlns:app в import_fragment."""
items = cp if isinstance(cp, list) else ([cp] if cp else [])
if not items:
return f"{indent}<ChoiceParameters/>"
parts = [f"{indent}<ChoiceParameters>"]
for item in items:
if isinstance(item, str):
item = convert_from_ch_param_shorthand(item)
name = get_ch_el_prop(item, ['name', 'имя'])
ptype = get_ch_el_prop(item, ['type', 'тип'])
has_val = False
val = None
if isinstance(item, dict):
if 'value' in item:
has_val = True
val = item['value']
elif 'значение' in item:
has_val = True
val = item['значение']
val_is_array = isinstance(val, list)
parts.append(f'{indent}\t<app:item name="{esc_xml(str(name) if name is not None else "")}">')
if not has_val:
parts.append(f'{indent}\t\t<app:value xsi:nil="true"/>')
elif val_is_array:
parts.append(f'{indent}\t\t<app:value xsi:type="v8:FixedArray">')
for v in val:
norm = normalize_choice_value_t(v, ptype)
if not norm['Text']:
parts.append(f'{indent}\t\t\t<v8:Value xsi:type="{norm["XsiType"]}"/>')
else:
parts.append(f'{indent}\t\t\t<v8:Value xsi:type="{norm["XsiType"]}">{esc_xml(norm["Text"])}</v8:Value>')
parts.append(f'{indent}\t\t</app:value>')
else:
norm = normalize_choice_value_t(val, ptype)
if not norm['Text']:
parts.append(f'{indent}\t\t<app:value xsi:type="{norm["XsiType"]}"/>')
else:
parts.append(f'{indent}\t\t<app:value xsi:type="{norm["XsiType"]}">{esc_xml(norm["Text"])}</app:value>')
parts.append(f'{indent}\t</app:item>')
parts.append(f"{indent}</ChoiceParameters>")
return "\r\n".join(parts)
def find_property_element(prop_name):
for child in properties_el:
if localname(child) == prop_name:
@@ -20,7 +20,11 @@
"Контр": {
"ChoiceForm": "Catalog.Спр.Form.ФормаВыбора",
"ChoiceParameterLinks": ["Отбор.Владелец=Ссылка"],
"LinkByType": { "dataPath": "Ссылка", "linkItem": 0 }
"LinkByType": { "dataPath": "Ссылка", "linkItem": 0 },
"ChoiceParameters": [
{ "name": "Отбор.ПометкаУдаления", "value": false },
{ "name": "Отбор.Ссылка", "value": "Catalog.Спр.EmptyRef" }
]
},
"Сумма": { "MinValue": 0, "MaxValue": 1000000 }
}
@@ -179,7 +179,14 @@
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ChoiceParameters />
<ChoiceParameters>
<app:item name="Отбор.ПометкаУдаления">
<app:value xsi:type="xs:boolean">false</app:value>
</app:item>
<app:item name="Отбор.Ссылка">
<app:value xsi:type="xr:DesignTimeRef">Catalog.Спр.EmptyRef</app:value>
</app:item>
</ChoiceParameters>
<QuickChoice>Auto</QuickChoice>
<CreateOnInput>Auto</CreateOnInput>
<ChoiceForm>Catalog.Спр.Form.ФормаВыбора</ChoiceForm>