diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index a5ae63c3..0b7101f1 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -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 "@ $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" } + $sb = New-Object System.Text.StringBuilder + $sb.Append("$indent") | 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") | Out-Null + if (-not $hasVal) { + $sb.Append("`r`n$indent`t`t") | Out-Null + } elseif ($valIsArray) { + $sb.Append("`r`n$indent`t`t") | Out-Null + foreach ($v in $val) { + $norm = Normalize-ChoiceValueT $v $ptype + if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t`t") | Out-Null } + else { $sb.Append("`r`n$indent`t`t`t$(Esc-Xml $norm.Text)") | Out-Null } + } + $sb.Append("`r`n$indent`t`t") | Out-Null + } else { + $norm = Normalize-ChoiceValueT $val $ptype + if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t") | Out-Null } + else { $sb.Append("`r`n$indent`t`t$(Esc-Xml $norm.Text)") | Out-Null } + } + $sb.Append("`r`n$indent`t") | Out-Null + } + $sb.Append("`r`n$indent") | 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) { diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index 037a3927..21c67487 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -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}" ) @@ -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}" + parts = [f"{indent}"] + 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') + if not has_val: + parts.append(f'{indent}\t\t') + elif val_is_array: + parts.append(f'{indent}\t\t') + for v in val: + norm = normalize_choice_value_t(v, ptype) + if not norm['Text']: + parts.append(f'{indent}\t\t\t') + else: + parts.append(f'{indent}\t\t\t{esc_xml(norm["Text"])}') + parts.append(f'{indent}\t\t') + else: + norm = normalize_choice_value_t(val, ptype) + if not norm['Text']: + parts.append(f'{indent}\t\t') + else: + parts.append(f'{indent}\t\t{esc_xml(norm["Text"])}') + parts.append(f'{indent}\t') + parts.append(f"{indent}") + return "\r\n".join(parts) + + def find_property_element(prop_name): for child in properties_el: if localname(child) == prop_name: diff --git a/tests/skills/cases/meta-edit/modify-attribute-structural.json b/tests/skills/cases/meta-edit/modify-attribute-structural.json index 92003953..48ecc214 100644 --- a/tests/skills/cases/meta-edit/modify-attribute-structural.json +++ b/tests/skills/cases/meta-edit/modify-attribute-structural.json @@ -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 } } diff --git a/tests/skills/cases/meta-edit/snapshots/modify-attribute-structural/Catalogs/Спр.xml b/tests/skills/cases/meta-edit/snapshots/modify-attribute-structural/Catalogs/Спр.xml index 536ab406..bc8edd86 100644 --- a/tests/skills/cases/meta-edit/snapshots/modify-attribute-structural/Catalogs/Спр.xml +++ b/tests/skills/cases/meta-edit/snapshots/modify-attribute-structural/Catalogs/Спр.xml @@ -179,7 +179,14 @@ Clear - + + + false + + + Catalog.Спр.EmptyRef + + Auto Auto Catalog.Спр.Form.ФормаВыбора