feat(meta-compile,meta-decompile): ML-синоним + ToolTip стандартных реквизитов (v1.24/v0.9)

Раундтрип-находка: у стандартных реквизитов синоним мультиязычен (корпус: 340 multi + 206 ru),
ToolTip непустой у 722 — компилятор писал синоним ru-only и <xr:ToolTip/> пусто, декомпилятор брал
Get-MLru (ru). Расширение существующего механизма standardAttributes.

- meta-compile (дуал-порт): Emit-StandardAttribute — синоним и ToolTip через Emit-MLText (строка|{ru,en});
  DSL standardAttributes.X.tooltip + synonym без стрингификации. Заодно Emit-MLItems → Esc-XmlText
  (кавычки в тексте элемента raw, не только у predefined).
- meta-decompile: синоним/tooltip станд. реквизита через Get-MLValue; Get-MLValue → $null при пустом
  ru-содержимом (пустой ML-item ≡ отсутствие значения, не tooltip:"").
- spec §7.1.1 (synonym/tooltip — ML); тест-кейс catalog-standard-attributes расширен.

Валидация: PS==PY; ToolFip станд.реквизита 5903→404 (остаток — косметика пустой-item≡self-close);
−9049 (79620→70571); регресс 37/37; 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-03 15:57:36 +03:00
parent 03de2dc86d
commit 40cdd57619
6 changed files with 47 additions and 38 deletions
@@ -341,14 +341,14 @@ function Emit-MLItems {
param([string]$indent, $val)
if ($val -is [System.Collections.IDictionary]) {
foreach ($k in $val.Keys) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
}
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
foreach ($p in $val.PSObject.Properties) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
}
} else {
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>"
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
}
}
function Emit-MLText {
@@ -885,6 +885,7 @@ function Emit-StandardAttribute {
$dh = OvOr 'DataHistory' 'Use'
$fts = OvOr 'FullTextSearch' 'Use'
$syn = OvOr 'Synonym' ''
$tt = OvOr 'ToolTip' ''
X "$indent<xr:StandardAttribute name=`"$attrName`">"
X "$indent`t<xr:LinkByType/>"
X "$indent`t<xr:FillChecking>$fc</xr:FillChecking>"
@@ -892,7 +893,7 @@ function Emit-StandardAttribute {
X "$indent`t<xr:FillFromFillingValue>$ffv</xr:FillFromFillingValue>"
X "$indent`t<xr:CreateOnInput>Auto</xr:CreateOnInput>"
X "$indent`t<xr:MaxValue xsi:nil=`"true`"/>"
X "$indent`t<xr:ToolTip/>"
Emit-MLText "$indent`t" "xr:ToolTip" $tt
X "$indent`t<xr:ExtendedEdit>false</xr:ExtendedEdit>"
X "$indent`t<xr:Format/>"
X "$indent`t<xr:ChoiceForm/>"
@@ -903,16 +904,7 @@ function Emit-StandardAttribute {
X "$indent`t<xr:DataHistory>$dh</xr:DataHistory>"
X "$indent`t<xr:MarkNegatives>false</xr:MarkNegatives>"
X "$indent`t<xr:MinValue xsi:nil=`"true`"/>"
if ($syn) {
X "$indent`t<xr:Synonym>"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml $syn)</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</xr:Synonym>"
} else {
X "$indent`t<xr:Synonym/>"
}
Emit-MLText "$indent`t" "xr:Synonym" $syn
X "$indent`t<xr:Comment/>"
X "$indent`t<xr:FullTextSearch>$fts</xr:FullTextSearch>"
X "$indent`t<xr:ChoiceParameterLinks/>"
@@ -944,7 +936,8 @@ function Emit-StandardAttributes {
if ($conditional -and $sa) {
$d = $sa.$a
if ($d) {
if ($null -ne $d.synonym) { $ov['Synonym'] = "$($d.synonym)" }
if ($null -ne $d.synonym) { $ov['Synonym'] = $d.synonym } # строка ИЛИ {ru,en}
if ($null -ne $d.tooltip) { $ov['ToolTip'] = $d.tooltip } # строка ИЛИ {ru,en}
if ($d.fillChecking) { $ov['FillChecking'] = "$($d.fillChecking)" }
if ($null -ne $d.fillFromFillingValue) { $ov['FillFromFillingValue'] = if ($d.fillFromFillingValue) { 'true' } else { 'false' } }
if ($d.fullTextSearch) { $ov['FullTextSearch'] = "$($d.fullTextSearch)" }
@@ -207,12 +207,12 @@ def emit_ml_items(indent, val):
for k, v in val.items():
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>{k}</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml(str(v))}</v8:content>')
X(f'{indent}\t<v8:content>{esc_xml_text(str(v))}</v8:content>')
X(f'{indent}</v8:item>')
else:
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>ru</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml(str(val))}</v8:content>')
X(f'{indent}\t<v8:content>{esc_xml_text(str(val))}</v8:content>')
X(f'{indent}</v8:item>')
def emit_mltext(indent, tag, text):
@@ -881,6 +881,7 @@ def emit_standard_attribute(indent, attr_name, ov=None):
dh = ov.get('DataHistory', 'Use')
fts = ov.get('FullTextSearch', 'Use')
syn = ov.get('Synonym', '')
tt = ov.get('ToolTip', '')
X(f'{indent}<xr:StandardAttribute name="{attr_name}">')
X(f'{indent}\t<xr:LinkByType/>')
X(f'{indent}\t<xr:FillChecking>{fc}</xr:FillChecking>')
@@ -888,7 +889,7 @@ def emit_standard_attribute(indent, attr_name, ov=None):
X(f'{indent}\t<xr:FillFromFillingValue>{ffv}</xr:FillFromFillingValue>')
X(f'{indent}\t<xr:CreateOnInput>Auto</xr:CreateOnInput>')
X(f'{indent}\t<xr:MaxValue xsi:nil="true"/>')
X(f'{indent}\t<xr:ToolTip/>')
emit_mltext(f'{indent}\t', 'xr:ToolTip', tt)
X(f'{indent}\t<xr:ExtendedEdit>false</xr:ExtendedEdit>')
X(f'{indent}\t<xr:Format/>')
X(f'{indent}\t<xr:ChoiceForm/>')
@@ -899,15 +900,7 @@ def emit_standard_attribute(indent, attr_name, ov=None):
X(f'{indent}\t<xr:DataHistory>{dh}</xr:DataHistory>')
X(f'{indent}\t<xr:MarkNegatives>false</xr:MarkNegatives>')
X(f'{indent}\t<xr:MinValue xsi:nil="true"/>')
if syn:
X(f'{indent}\t<xr:Synonym>')
X(f'{indent}\t\t<v8:item>')
X(f'{indent}\t\t\t<v8:lang>ru</v8:lang>')
X(f'{indent}\t\t\t<v8:content>{esc_xml(syn)}</v8:content>')
X(f'{indent}\t\t</v8:item>')
X(f'{indent}\t</xr:Synonym>')
else:
X(f'{indent}\t<xr:Synonym/>')
emit_mltext(f'{indent}\t', 'xr:Synonym', syn)
X(f'{indent}\t<xr:Comment/>')
X(f'{indent}\t<xr:FullTextSearch>{fts}</xr:FullTextSearch>')
X(f'{indent}\t<xr:ChoiceParameterLinks/>')
@@ -936,7 +929,9 @@ def emit_standard_attributes(indent, object_type):
d = sa.get(a)
if d:
if d.get('synonym') is not None:
ov['Synonym'] = str(d['synonym'])
ov['Synonym'] = d['synonym'] # строка ИЛИ {ru,en}
if d.get('tooltip') is not None:
ov['ToolTip'] = d['tooltip'] # строка ИЛИ {ru,en}
if d.get('fillChecking'):
ov['FillChecking'] = str(d['fillChecking'])
if d.get('fillFromFillingValue') is not None:
@@ -1,4 +1,4 @@
# meta-decompile v0.8 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.9 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Пилот: только Catalog. Инверс meta-compile (omit-on-default: ключ эмитим только
@@ -115,7 +115,8 @@ function Get-MLValue {
if ($items.Count -eq 1) {
$lang = $items[0].SelectSingleNode('v8:lang', $nsm).InnerText
$content = $items[0].SelectSingleNode('v8:content', $nsm).InnerText
if ($lang -eq 'ru') { return $content }
# Единственный ru-item: пустое содержимое ≡ отсутствие значения → $null (иначе tooltip:"" ≠ self-close).
if ($lang -eq 'ru') { if ($content -eq '') { return $null } else { return $content } }
}
$o = [ordered]@{}
foreach ($it in $items) {
@@ -296,9 +297,11 @@ if ($saNode) {
$ffvN = $sa.SelectSingleNode('xr:FillFromFillingValue', $nsm); $ffv = ($ffvN -and $ffvN.InnerText -eq 'true')
$profFfv = ($prof['fillFromFillingValue'] -eq $true)
if ($ffv -ne $profFfv) { $ov['fillFromFillingValue'] = $ffv }
# Synonym (профиль пуст)
$syn = Get-MLru ($sa.SelectSingleNode('xr:Synonym', $nsm))
if ($syn) { $ov['synonym'] = $syn }
# Synonym / ToolTip (профиль пуст) — строка ru | {ru,en}
$syn = Get-MLValue ($sa.SelectSingleNode('xr:Synonym', $nsm))
if ($null -ne $syn) { $ov['synonym'] = $syn }
$tt = Get-MLValue ($sa.SelectSingleNode('xr:ToolTip', $nsm))
if ($null -ne $tt) { $ov['tooltip'] = $tt }
# FullTextSearch / DataHistory (профиль = Use)
$ftsN = $sa.SelectSingleNode('xr:FullTextSearch', $nsm); if ($ftsN -and $ftsN.InnerText -ne 'Use') { $ov['fullTextSearch'] = $ftsN.InnerText }
$dhN = $sa.SelectSingleNode('xr:DataHistory', $nsm); if ($dhN -and $dhN.InnerText -ne 'Use') { $ov['dataHistory'] = $dhN.InnerText }
+1 -1
View File
@@ -285,7 +285,7 @@ JSON DSL для описания объектов метаданных конф
Формат: `standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена реквизитов — как в XML: `PredefinedDataName`, `Predefined`, `Ref`, `DeletionMark`, `IsFolder`, `Owner`, `Parent`, `Description`, `Code`.
Переопределяемые поля реквизита: `synonym` (строка`<xr:Synonym>` ru), `fillChecking` (`DontCheck`|`ShowError`|`ShowWarning`), `fillFromFillingValue` (bool), `fullTextSearch` (`Use`|`DontUse`), `dataHistory` (`Use`|`DontUse`).
Переопределяемые поля реквизита: `synonym` (ML — строка/`{ru,en}`), `tooltip` (ML), `fillChecking` (`DontCheck`|`ShowError`|`ShowWarning`), `fillFromFillingValue` (bool), `fullTextSearch` (`Use`|`DontUse`), `dataHistory` (`Use`|`DontUse`).
**Профиль материализованного блока** (значения, которые платформа заполняет автоматически — задавать их в DSL не нужно):
@@ -4,8 +4,8 @@
"type": "Catalog",
"name": "Контрагенты",
"standardAttributes": {
"Description": { "synonym": "Наименование контрагента" },
"Code": { "fillChecking": "ShowError" }
"Description": { "synonym": { "ru": "Наименование контрагента", "en": "Counterparty name" }, "tooltip": "Полное наименование" },
"Code": { "fillChecking": "ShowError", "tooltip": { "ru": "Код", "en": "Code" } }
}
},
"validatePath": "Catalogs/Контрагенты",
@@ -238,7 +238,12 @@
<xr:FillFromFillingValue>false</xr:FillFromFillingValue>
<xr:CreateOnInput>Auto</xr:CreateOnInput>
<xr:MaxValue xsi:nil="true"/>
<xr:ToolTip/>
<xr:ToolTip>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Полное наименование</v8:content>
</v8:item>
</xr:ToolTip>
<xr:ExtendedEdit>false</xr:ExtendedEdit>
<xr:Format/>
<xr:ChoiceForm/>
@@ -254,6 +259,10 @@
<v8:lang>ru</v8:lang>
<v8:content>Наименование контрагента</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Counterparty name</v8:content>
</v8:item>
</xr:Synonym>
<xr:Comment/>
<xr:FullTextSearch>Use</xr:FullTextSearch>
@@ -269,7 +278,16 @@
<xr:FillFromFillingValue>false</xr:FillFromFillingValue>
<xr:CreateOnInput>Auto</xr:CreateOnInput>
<xr:MaxValue xsi:nil="true"/>
<xr:ToolTip/>
<xr:ToolTip>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Код</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Code</v8:content>
</v8:item>
</xr:ToolTip>
<xr:ExtendedEdit>false</xr:ExtendedEdit>
<xr:Format/>
<xr:ChoiceForm/>