mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-14 18:04:58 +03:00
feat(form-decompile,form-compile): суппресс авто-вывода (MainAttribute/SavedData/AutoTitle) + AutoFillAvailableFields + SaveWindowSettings
Раундтрип TOTAL 40→25, match 138→146. Три класса «компилятор додумывает на main/titled формах» (декомпилятор не давал суппресс-маркера) + два непокрытых свойства. - MainAttribute: эвристика 11b.3 (нет явного main + ровно 1 объектный реквизит → помечает main). Декомпилятор зеркалит условие → ставит main:false (компилятор уже исключает такие кандидаты). Объектные реквизиты часто НЕ main (DynamicList 1207 без, RecordSet 226 без, и т.д.). Decompiler-only. - SavedData: эвристика $mainSaved (main + Catalog/Document/ChartOf*/ExchangePlan/ BusinessProcess/Task Object + RecordManager → SavedData=true). Часто отсутствует (DocumentObject 332 без = 23%). Компилятор: явный savedData:false побеждает; декомпилятор ставит savedData:false для main-реквизита saved-типа без <SavedData>. - AutoTitle: компилятор инъектит false при наличии title (~95% форм). Редкие 5% (Title есть, AutoTitle нет) → декомпилятор ставит autoTitle:"", компилятор пропускает пустую строку в Emit-Properties (общий ""-суппресс). - AutoFillAvailableFields: свойство <Settings> дин-списка (дефолт true, эмит только отклонение false; ключ settings.autoFillAvailableFields). - SaveWindowSettings: форменный bool (KNOWN_FORM_PROPS + auto-PascalCase). Зеркало py (компилятор). Кейс dynamic-list-form +saveWindowSettings (сертифицирован). Формы ЗадачаИсполнителя/Дополнительно, БизнесСеть/*, АнализПравДоступа → чисто. Регресс 39/39 в обоих рантаймах. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# form-compile v1.90 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.91 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$JsonPath,
|
||||
@@ -4555,7 +4555,9 @@ function Emit-Attributes {
|
||||
if ($attr.main -eq $true -and $attr.type) {
|
||||
$mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.'
|
||||
}
|
||||
if ($attr.savedData -eq $true -or $mainSaved) {
|
||||
# Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто.
|
||||
$emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved }
|
||||
if ($emitSaved) {
|
||||
X "$inner<SavedData>true</SavedData>"
|
||||
}
|
||||
# Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>;
|
||||
@@ -4655,7 +4657,9 @@ function Emit-Attributes {
|
||||
$st = $attr.settings
|
||||
X "$inner<Settings xsi:type=`"DynamicList`">"
|
||||
$si = "$inner`t"
|
||||
# Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
# Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
# AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение).
|
||||
if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" }
|
||||
$hasQuery = $st.query -and "$($st.query)".Trim()
|
||||
$mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" }
|
||||
X "$si<ManualQuery>$mq</ManualQuery>"
|
||||
@@ -4830,6 +4834,8 @@ function Emit-Properties {
|
||||
}
|
||||
# Convert boolean to lowercase string (PS renders as True/False)
|
||||
$val = $p.Value
|
||||
# Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать)
|
||||
if ($val -is [string] -and $val -eq '') { continue }
|
||||
if ($val -is [bool]) {
|
||||
$val = if ($val) { "true" } else { "false" }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-compile v1.90 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.91 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import copy
|
||||
@@ -4273,7 +4273,9 @@ def emit_attributes(lines, attrs, indent):
|
||||
if attr.get('main') is True and attr.get('type'):
|
||||
t = str(attr['type'])
|
||||
main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t)
|
||||
if attr.get('savedData') is True or main_saved:
|
||||
# Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто.
|
||||
emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved
|
||||
if emit_saved:
|
||||
lines.append(f'{inner}<SavedData>true</SavedData>')
|
||||
# Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>;
|
||||
# строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть).
|
||||
@@ -4361,6 +4363,10 @@ def emit_attributes(lines, attrs, indent):
|
||||
s = attr['settings']
|
||||
lines.append(f'{inner}<Settings xsi:type="DynamicList">')
|
||||
si = f'{inner}\t'
|
||||
# Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
# AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение).
|
||||
if s.get('autoFillAvailableFields') is not None:
|
||||
lines.append(f'{si}<AutoFillAvailableFields>{"true" if s["autoFillAvailableFields"] else "false"}</AutoFillAvailableFields>')
|
||||
# Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
|
||||
has_query = bool(s.get('query') and str(s['query']).strip())
|
||||
mq = 'true' if (has_query or s.get('manualQuery')) else 'false'
|
||||
@@ -4519,6 +4525,9 @@ def emit_properties(lines, props, indent):
|
||||
# Auto PascalCase
|
||||
xml_name = p_name[0].upper() + p_name[1:]
|
||||
|
||||
# Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать)
|
||||
if isinstance(p_value, str) and p_value == '':
|
||||
continue
|
||||
# Convert boolean to lowercase
|
||||
if isinstance(p_value, bool):
|
||||
val = 'true' if p_value else 'false'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-decompile v0.66 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# form-decompile v0.68 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||
param(
|
||||
@@ -1658,7 +1658,7 @@ $titleNode = $root.SelectSingleNode("lf:Title", $ns)
|
||||
if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } }
|
||||
|
||||
# properties (прямые скаляры под <Form>, PascalCase → camelCase)
|
||||
$KNOWN_FORM_PROPS = @('AutoTitle','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems')
|
||||
$KNOWN_FORM_PROPS = @('AutoTitle','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings')
|
||||
$props = [ordered]@{}
|
||||
foreach ($pn in $KNOWN_FORM_PROPS) {
|
||||
$v = Get-Child $root $pn
|
||||
@@ -1670,8 +1670,13 @@ foreach ($pn in $KNOWN_FORM_PROPS) {
|
||||
else { $props[$camel] = $v }
|
||||
}
|
||||
}
|
||||
# autoTitle=false при наличии title — это инъекция компилятора, опускаем (валидируем раундтрипом)
|
||||
if ($dsl.Contains('title') -and $props.Contains('autoTitle') -and $props['autoTitle'] -eq $false) { $props.Remove('autoTitle') }
|
||||
# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим:
|
||||
# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title);
|
||||
# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить).
|
||||
if ($dsl.Contains('title')) {
|
||||
if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' }
|
||||
elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') }
|
||||
}
|
||||
if ($props.Count -gt 0) { $dsl['properties'] = $props }
|
||||
|
||||
# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок
|
||||
@@ -1746,10 +1751,27 @@ if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } }
|
||||
if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) }
|
||||
|
||||
# attributes
|
||||
# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3.
|
||||
function Test-IsObjectLikeTypeDec([string]$type) {
|
||||
if ([string]::IsNullOrEmpty($type)) { return $false }
|
||||
if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true }
|
||||
return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.')
|
||||
}
|
||||
$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns)
|
||||
if ($attrsNode) {
|
||||
$attrs = New-Object System.Collections.ArrayList
|
||||
foreach ($a in @($attrsNode.SelectNodes("lf:Attribute", $ns))) {
|
||||
# Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно
|
||||
# один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет)
|
||||
# → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно.
|
||||
$allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns))
|
||||
$anyMainAttr = $false; $objLikeNodes = @()
|
||||
foreach ($an in $allAttrNodes) {
|
||||
if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true }
|
||||
$atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns))
|
||||
if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an }
|
||||
}
|
||||
$suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null }
|
||||
foreach ($a in $allAttrNodes) {
|
||||
$ao = [ordered]@{}
|
||||
$ao['name'] = $a.GetAttribute("name")
|
||||
$ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty }
|
||||
@@ -1761,12 +1783,13 @@ if ($attrsNode) {
|
||||
$ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер ""
|
||||
}
|
||||
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 }
|
||||
$ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed }
|
||||
# Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок
|
||||
# из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем
|
||||
# ключ (компилятор воспроизведёт); иначе → явный заголовок.
|
||||
$isMain = $ao.Contains('main')
|
||||
$isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title
|
||||
$tNode = $a.SelectSingleNode("lf:Title", $ns)
|
||||
if ($tNode) {
|
||||
$t = Get-LangText $tNode
|
||||
@@ -1776,7 +1799,11 @@ if ($attrsNode) {
|
||||
} elseif (-not $isMain) {
|
||||
$ao['title'] = ''
|
||||
}
|
||||
# SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved:
|
||||
# Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал
|
||||
# тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute).
|
||||
if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true }
|
||||
elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false }
|
||||
# Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true;
|
||||
# иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив.
|
||||
$saveNode = $a.SelectSingleNode("lf:Save", $ns)
|
||||
@@ -1848,6 +1875,8 @@ if ($attrsNode) {
|
||||
$setNode = $a.SelectSingleNode("lf:Settings", $ns)
|
||||
if ($setNode) {
|
||||
$so = [ordered]@{}
|
||||
# AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть».
|
||||
$afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') }
|
||||
$mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt }
|
||||
$qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns)
|
||||
if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" }
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
|
||||
| DSL ключ | XML элемент | Значения |
|
||||
|----------|-------------|----------|
|
||||
| `autoTitle` | `<AutoTitle>` | `true` / `false` |
|
||||
| `autoTitle` | `<AutoTitle>` | `true` / `false`. **При наличии `title` компилятор сам инъектит `false`** (≈95% форм). Маркер `""` подавляет инъекцию (редкие формы с title, но без `<AutoTitle>`) |
|
||||
| `saveWindowSettings` | `<SaveWindowSettings>` | `true` / `false` |
|
||||
| `windowOpeningMode` | `<WindowOpeningMode>` | `LockOwnerWindow`, `Modeless` |
|
||||
| `commandBarLocation` | `<CommandBarLocation>` | `Top`, `Bottom`, `None` |
|
||||
| `saveDataInSettings` | `<SaveDataInSettings>` | `UseList`, `Use`, `DontUse` |
|
||||
@@ -768,14 +769,14 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
|
||||
|----------|-----|----------|
|
||||
| `name` | string | Имя реквизита (обязательно) |
|
||||
| `type` | string | Тип (shorthand) |
|
||||
| `main` | bool | Основной реквизит формы |
|
||||
| `main` | bool | Основной реквизит формы (`<MainAttribute>`). **`true`** → пометить главным. **`false`** → суппресс: подавить авто-вывод компилятора (эвристика «нет явного main + ровно 1 реквизит объектного типа → пометить его»). Нет ключа → авто-вывод |
|
||||
| `title` | string/object | Заголовок. **Нет ключа** → авто-вывод из имени (как у элементов; кроме `main`). **`""`** → подавить (`<Title>` не эмитится — так платформа и хранит реквизит без синонима). Строка → ru; объект `{ru,en}` → мультиязычный. Декомпилятор опускает ключ, когда ru-заголовок совпадает с авто-выводом из имени |
|
||||
| `view` | bool/object | Просмотр по ролям (`<View>`). См. §4.1c |
|
||||
| `edit` | bool/object | Редактирование по ролям (`<Edit>`). См. §4.1c |
|
||||
| `functionalOptions` | array | Функциональные опции (`<FunctionalOptions><Item>FunctionalOption.X</Item>…`). Массив имён; forgiving: `"X"`/`"FunctionalOption.X"`. Также у колонок (`columns[*]`) и команд (§7) |
|
||||
| `useAlways` | array | Поля, всегда читаемые (`<UseAlways><Field>Имя.Поле</Field>…`). Массив коротких имён полей (forgiving: с/без префикса `Имя.`). **Маркер `~`** (query-поля дин-списка): `~Остановлен` → `<Field>~Список.Остановлен</Field>` (префикс ставится ПОСЛЕ `~`; полная форма `~Список.Остановлен` тоже принимается verbatim). **Две формы**: этот массив на реквизите ИЛИ `useAlways: true` на колонке (`columns[*]`) — компилятор сливает. Для дин-списка — только массив (колонки не эмитятся, но формируют `<UseAlways>`) |
|
||||
| `valueType` | string | Тип значений у реквизита типа `ValueList` (`<Settings xsi:type="v8:TypeDescription">`). Грамматика — как у `type`, включая составной `A \| B`. **Три состояния**: нет ключа → нет `<Settings>`; `""` → пустой `<Settings…/>` (список без ограничения типа); тип → с типом. Forgiving-синонимы: `typeDescription` (≈1С «ОписаниеТипов» / XML), `описаниеТипов`, `типЗначений`. Пример: `"valueType": "CatalogRef.Контрагенты"` |
|
||||
| `savedData` | bool | Сохраняемые данные (`<SavedData>`) |
|
||||
| `savedData` | bool | Сохраняемые данные (`<SavedData>`). **`false`** → суппресс авто-вывода компилятора (main-реквизит объектного типа Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager → `SavedData=true`). Нет ключа → авто-вывод |
|
||||
| `save` | bool/string/array | Сохранение значения в пользовательских настройках (`<Save><Field>…`). `true` → `<Field>имя</Field>`; строка/массив строк → под-поля с авто-префиксом `имя.` (путь с точкой / UUID `1/0:…` / совпадающее с именем — берётся как есть). Нет ключа или `false` → не эмитится. Пример периода: `["Период","EndDate","StartDate","Variant"]` |
|
||||
| `fillCheck` | bool/string | Проверка заполнения реквизита (`<FillCheck>`). `true` → `ShowError` (единственное значение в схеме); строка → verbatim. Синоним `fillChecking`. (`<FillChecking>` в схеме нет — был багом) |
|
||||
| `columns` | array | Колонки для ValueTable/ValueTree (`{ name, type, title?, functionalOptions?, useAlways? }`) |
|
||||
@@ -801,6 +802,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
|
||||
| `mainTable` | string | Основная таблица. Принимает рус-имена метаданных (`Справочник.X` → `Catalog.X`) |
|
||||
| `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) |
|
||||
| `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) |
|
||||
| `autoFillAvailableFields` | bool | Автозаполнение доступных полей (`<AutoFillAvailableFields>`). **Умолчание `true`** — указывать только для отключения (`false`; тогда поля берутся из явного запроса, не авто). Эмитится первым в `<Settings>` |
|
||||
| `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title? }` — для переопределения заголовка. Обычно поля выводятся из запроса автоматически |
|
||||
| `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже |
|
||||
| `order` | array | Сортировка списка (см. ниже) |
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"validatePath": "Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml",
|
||||
"input": {
|
||||
"title": "Товары",
|
||||
"properties": { "saveWindowSettings": false },
|
||||
"attributes": [
|
||||
{ "name": "Список", "type": "DynamicList", "useAlways": ["~Артикул", "Список.Code", "Description"], "settings": {
|
||||
"mainTable": "Catalog.Товары", "dynamicDataRead": true,
|
||||
|
||||
+1
@@ -7,6 +7,7 @@
|
||||
</v8:item>
|
||||
</Title>
|
||||
<AutoTitle>false</AutoTitle>
|
||||
<SaveWindowSettings>false</SaveWindowSettings>
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
|
||||
<ChildItems>
|
||||
<Table name="Список" id="1">
|
||||
|
||||
Reference in New Issue
Block a user