feat(form-decompile,form-compile): Chart design-time Settings из ring-3 (Фаза 2 кластера Chart-Settings)

Реквизит-диаграмма несёт <Settings xsi:type="d4p1:Chart"> — встроенный конфиг
(~110-130 версионно-вариативных полей: тип/серии/легенда/заголовок/шкалы/цвета/
оси, глубокая вложенность с повторяющимися именами). Корпус 8.3.24: 5 форм.

Подход (с пользователем): ГЕНЕРИК-движок. Ключ chart на реквизите; рекурсивный
захват/эмит поддерева d4p1, ключи = локальные имена тегов, порядок ключей =
порядок эмиссии → раундтрип ЛЮБОЙ версии/набора полей бит-в-бит (платформа
добавляет поля, не переставляет). Структуры распознаются по форме узла
(line {width,gap,style} / border {width,style} / font {kind} / ML / области
{left,right,top,bottom} / серии-массивы); малые name-set'ы: ML-поля, серии,
attrs-узлы (gaugeQualityBands). Расширяемость: любое из ~127 свойств — по
каноничному имени.

Авторинг с нуля: декомпиль рабочей диаграммы как шаблон + правка ядра
(chartType/серии/легенда/цвета). Default-fill через merge НЕ делаем — конфликт
с байт-точностью неполных форм (см. docs/form-dsl-spec.md).

Результат: 4 из 5 форм корпуса — байт-в-байт (включая версионно-вариативные).
5-я (точки/оси realPointData/realDataItems с типизир. значениями xsi:type,
xsi:nil, ML с префиксом d4p1:) → честный fail-ring3 (редкий вариант, не
поддержан генериком). Снят fail-ring3 для d4p1:Chart (GanttChart — Фаза 3).
Заодно фикс: d5p1:Dendrogram отсутствовал в specialTypeNs (ps1+py).

Декомпилятор ps1-only (генерик-рекурсия); компилятор зеркало py (ps1==py
байт-в-байт). Кейс chart-settings (полная диаграмма из эталона
ПроверкаКонтрагента) сертифицирован загрузкой в 1С. Регресс 42/42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-10 16:01:15 +03:00
parent f534add7b4
commit 1333e09ff8
14 changed files with 1287 additions and 4 deletions
@@ -1,4 +1,4 @@
# form-compile v1.105 — Compile 1C managed form from JSON or object metadata
# form-compile v1.106 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -3233,6 +3233,66 @@ function Emit-PlannerSettings {
X "$ind</Settings>"
}
# ─────────────────────────────────────────────────────────────────────────────
# Chart design-time <Settings xsi:type="d4p1:Chart"> — генерик-эмиттер (зеркало
# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии
# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands),
# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип).
$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 }
$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 }
$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')
function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } }
function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } }
function Emit-ChartNode {
param([string]$name, $val, [string]$ind)
if ($script:CHART_ML_FIELDS.Contains($name)) {
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
X "$ind<d4p1:$name>"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind</d4p1:$name>"; return
}
if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) {
foreach ($e in $val) { Emit-ChartNode $name $e $ind }
return
}
if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) {
$keys = Get-Keys $val
if ($script:CHART_ATTR_FIELDS.Contains($name)) {
$attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" }
X "$ind<d4p1:$name $($attrs -join ' ')/>"; return
}
if ($keys -contains 'gap') {
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return
}
if (($keys -contains 'style') -and ($keys -contains 'width')) {
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return
}
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
if ($isFont) {
$attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } }
X "$ind<d4p1:$name $($attrs -join ' ')/>"; return
}
if (@($keys).Count -eq 0) { X "$ind<d4p1:$name/>"; return }
X "$ind<d4p1:$name>"
foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" }
X "$ind</d4p1:$name>"
return
}
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
X "$ind<d4p1:$name>$(Esc-Xml "$val")</d4p1:$name>"
}
function Emit-ChartSettings {
param($chart, [string]$ind)
X "$ind<Settings xmlns:d4p1=`"$($script:CHART_NS)`" xsi:type=`"d4p1:Chart`">"
foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" }
X "$ind</Settings>"
}
function Emit-Appearance {
param($el, [string]$indent, [string]$profile = 'field')
if ($null -eq $el) { return }
@@ -4982,6 +5042,10 @@ function Emit-Attributes {
if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) {
Emit-PlannerSettings -pl $attr.planner -ind $inner
}
# Chart design-time <Settings xsi:type="d4p1:Chart"> (встроенный конфиг диаграммы).
if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) {
Emit-ChartSettings -chart $attr.chart -ind $inner
}
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.105 — Compile 1C managed form from JSON or object metadata
# form-compile v1.106 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -2939,6 +2939,71 @@ def emit_planner_settings(lines, pl, ind):
lines.append(f'{ind}</Settings>')
# ─────────────────────────────────────────────────────────────────────────────
# Chart design-time <Settings xsi:type="d4p1:Chart"> — генерик-эмиттер (зеркало
# Build-ChartNode декомпилятора + Emit-ChartNode ps1).
CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'}
CHART_ATTR_FIELDS = {'gaugeQualityBands'}
CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale')
def emit_chart_node(lines, name, val, ind):
if name in CHART_ML_FIELDS:
if val is None or str(val) == '':
lines.append(f'{ind}<d4p1:{name}/>')
return
lines.append(f'{ind}<d4p1:{name}>')
emit_ml_items(lines, f'{ind}\t', val)
lines.append(f'{ind}</d4p1:{name}>')
return
if isinstance(val, list):
for e in val:
emit_chart_node(lines, name, e, ind)
return
if isinstance(val, dict):
keys = list(val.keys())
if name in CHART_ATTR_FIELDS:
attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys)
lines.append(f'{ind}<d4p1:{name} {attrs}/>')
return
if 'gap' in val:
lines.append(f'{ind}<d4p1:{name} width="{val.get("width")}" gap="{_pl_bool(val.get("gap"))}">')
lines.append(f'{ind}\t<v8ui:style xsi:type="v8ui:ChartLineType">{esc_xml(str(val.get("style")))}</v8ui:style>')
lines.append(f'{ind}</d4p1:{name}>')
return
if 'style' in val and 'width' in val:
lines.append(f'{ind}<d4p1:{name} width="{val.get("width")}">')
lines.append(f'{ind}\t<v8ui:style xsi:type="v8ui:ControlBorderType">{esc_xml(str(val.get("style")))}</v8ui:style>')
lines.append(f'{ind}</d4p1:{name}>')
return
if any(fk in val for fk in CHART_FONT_KEYS):
attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val)
lines.append(f'{ind}<d4p1:{name} {attrs}/>')
return
if not keys:
lines.append(f'{ind}<d4p1:{name}/>')
return
lines.append(f'{ind}<d4p1:{name}>')
for k in keys:
emit_chart_node(lines, k, val[k], f'{ind}\t')
lines.append(f'{ind}</d4p1:{name}>')
return
if val is None or str(val) == '':
lines.append(f'{ind}<d4p1:{name}/>')
return
if isinstance(val, bool):
lines.append(f'{ind}<d4p1:{name}>{_pl_bool(val)}</d4p1:{name}>')
return
lines.append(f'{ind}<d4p1:{name}>{esc_xml(str(val))}</d4p1:{name}>')
def emit_chart_settings(lines, chart, ind):
lines.append(f'{ind}<Settings xmlns:d4p1="{CHART_NS}" xsi:type="d4p1:Chart">')
for k in list(chart.keys()):
emit_chart_node(lines, k, chart[k], f'{ind}\t')
lines.append(f'{ind}</Settings>')
def emit_appearance(lines, el, indent, profile='field'):
if not isinstance(el, dict):
return
@@ -4709,6 +4774,9 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None):
# Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика).
if attr.get('planner') is not None:
emit_planner_settings(lines, attr['planner'], inner)
# Chart design-time <Settings xsi:type="d4p1:Chart"> (встроенный конфиг диаграммы).
if attr.get('chart') is not None:
emit_chart_settings(lines, attr['chart'], inner)
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')
@@ -1,4 +1,4 @@
# form-decompile v0.81 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.82 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -152,9 +152,15 @@ foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*
# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча.
foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) {
$st = $s.GetAttribute("type", $NS_XSI)
if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$') {
if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:Chart$') {
Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings"
}
# Chart с точками/осями (realPointData/realDataItems): типизированные значения (xsi:type),
# xsi:nil и ML с префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип.
# Частые дашборд-диаграммы (серии/легенда/оформление) поддержаны.
elseif ($st -match 'd4p1:Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) {
Fail-Ring3 -kind "Attribute>Settings d4p1:Chart с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings"
}
}
# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) ---
@@ -2002,6 +2008,69 @@ function Build-PlannerSettings {
return $pl
}
# ─────────────────────────────────────────────────────────────────────────────
# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок:
# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии)
# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML),
# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип).
$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 }
$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 }
$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 }
function Conv-ChartScalar {
param([string]$v)
if ($v -eq 'true') { return $true }
if ($v -eq 'false') { return $false }
return $v
}
function Build-ChartNode {
param($n, [string]$name)
# ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени)
if ($CHART_ML_FIELDS.Contains($name)) {
$ml = Get-LangText $n
if ($null -eq $ml) { return '' } else { return $ml }
}
$kids = @($n.SelectNodes("*"))
if ($kids.Count -eq 0) {
# лист: attrs-only (шрифт/gaugeQualityBands) или текст
$attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' })
if ($attrs.Count -gt 0) {
$o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o
}
return (Conv-ChartScalar $n.InnerText)
}
# line/border: дочерний v8ui:style (+ width[/gap])
$styleChild = $n.SelectSingleNode("*[local-name()='style']")
if ($styleChild) {
$o = [ordered]@{}
$w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w }
$g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') }
$o['style'] = $styleChild.InnerText
return $o
}
# вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени
$o = [ordered]@{}
foreach ($c in $kids) {
$ln = $c.LocalName
$val = Build-ChartNode $c $ln
if ($CHART_SERIES_FIELDS.Contains($ln)) {
if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList }
[void]$o[$ln].Add($val)
} elseif ($o.Contains($ln)) {
if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp }
[void]$o[$ln].Add($val)
} else {
$o[$ln] = $val
}
}
# нормализуем ArrayList → @() для сериализации
foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } }
return $o
}
function Build-ChartSettings {
param($setNode)
return (Build-ChartNode $setNode '')
}
# --- 5. Form-level assembly ---
$dsl = [ordered]@{}
@@ -2137,6 +2206,10 @@ if ($attrsNode) {
elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') {
$ao['planner'] = Build-PlannerSettings $setNode
}
# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart (генерик-захват).
elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:Chart$') {
$ao['chart'] = Build-ChartSettings $setNode
}
if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true }
elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false }
$vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw }