feat(form-decompile,form-compile): доступ по ролям — userVisible/view/edit/use (единый xr-механизм)

Кластер «доступ по ролям»: единый role-adjustable boolean платформы
(xr:Common + 0..N xr:Value name="Role.X") для четырёх владельцев одним
грамматиком значения:
  - элемент   → userVisible  (<UserVisible>)
  - реквизит  → view, edit    (<View>/<Edit>)
  - команда   → use           (<Use>)

Значение DSL: скаляр false/true → голый <xr:Common>; объект
{ common, roles:{ Имя: bool } } → пер-ролевые исключения (три-state как в
конфигураторе: роль не указана → наследует common; указана → явный bool).
Имя роли forgiving: без префикса / Role. / Роль. → нормализуется в Role.
Отсутствие ключа = полный доступ (платформа тег не пишет) — дефолт не эмитим.

Декомпилятор инвертирует: голый Common → скаляр, есть Value → объект.
Компилятор: общий хелпер Emit-XrFlag / emit_xr_flag (ps1+py).
Порядок схемы: View → Edit после MainAttribute; Use после ToolTip до Action.

Раньше: userVisible умел только голый false (компилятор), декомпилятор не
захватывал ничего; view/edit/use не умел никто.

TOTAL diff lines выборки 2.17: 7911 → 7560 (-351), match 9 → 11.
Снапшоты attributes-types/commands сертифицированы в 1С (8.3.24);
регресс form-compile 33/33 зелёный на ps + python.
decompile v0.28, compile v1.47.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-06 20:40:01 +03:00
parent f34993b805
commit d484a5b7ec
8 changed files with 151 additions and 16 deletions
@@ -1,4 +1,4 @@
# form-compile v1.46 — Compile 1C managed form from JSON or object metadata
# form-compile v1.47 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -2430,14 +2430,39 @@ function Emit-Element {
}
}
# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X").
# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки).
# Значение DSL: скаляр bool → только <xr:Common>; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения.
# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом.
function Emit-XrFlag {
param([string]$tag, $val, [string]$indent)
if ($null -eq $val) { return }
if ($val -is [bool]) {
X "$indent<$tag>"
X "$indent`t<xr:Common>$(if ($val){'true'}else{'false'})</xr:Common>"
X "$indent</$tag>"
return
}
# объектная форма { common, roles }
$common = if ($null -ne $val.common) { [bool]$val.common } else { $false }
X "$indent<$tag>"
X "$indent`t<xr:Common>$(if ($common){'true'}else{'false'})</xr:Common>"
if ($val.roles) {
foreach ($r in $val.roles.PSObject.Properties) {
# Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role."
$rname = "$($r.Name)" -replace '^(Role|Роль)\.', ''
$rname = "Role.$rname"
$rval = if ([bool]$r.Value) { 'true' } else { 'false' }
X "$indent`t<xr:Value name=`"$rname`">$rval</xr:Value>"
}
}
X "$indent</$tag>"
}
function Emit-CommonFlags {
param($el, [string]$indent)
if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" }
if ($el.userVisible -eq $false) {
X "$indent<UserVisible>"
X "$indent`t<xr:Common>false</xr:Common>"
X "$indent</UserVisible>"
}
if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent }
if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" }
if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" }
}
@@ -3526,6 +3551,9 @@ function Emit-Attributes {
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
}
# Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute)
if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner }
if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner }
$mainSaved = $false
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\.'
@@ -3647,6 +3675,9 @@ function Emit-Commands {
Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner
}
# Доступность команды по ролям (после ToolTip, до Action)
if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner }
if ($cmd.action) {
X "$inner<Action>$($cmd.action)</Action>"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.46 — Compile 1C managed form from JSON or object metadata
# form-compile v1.47 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -2059,13 +2059,36 @@ def emit_table_addition(lines, tag, table_name, name_suffix, src_type, indent):
lines.append(f'{indent}</{tag}>')
# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X").
# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки).
# Значение DSL: скаляр bool → только <xr:Common>; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения.
# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом.
def emit_xr_flag(lines, tag, val, indent):
if val is None:
return
if isinstance(val, bool):
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<xr:Common>{'true' if val else 'false'}</xr:Common>")
lines.append(f"{indent}</{tag}>")
return
# объектная форма { common, roles }
common = bool(val.get('common')) if val.get('common') is not None else False
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<xr:Common>{'true' if common else 'false'}</xr:Common>")
roles = val.get('roles')
if roles:
for rname, rval in roles.items():
# Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role."
rn = "Role." + re.sub(r'^(Role|Роль)\.', '', rname)
lines.append(f"{indent}\t<xr:Value name=\"{rn}\">{'true' if rval else 'false'}</xr:Value>")
lines.append(f"{indent}</{tag}>")
def emit_common_flags(lines, el, indent):
if el.get('visible') is False or el.get('hidden') is True:
lines.append(f"{indent}<Visible>false</Visible>")
if el.get('userVisible') is False:
lines.append(f"{indent}<UserVisible>")
lines.append(f"{indent}\t<xr:Common>false</xr:Common>")
lines.append(f"{indent}</UserVisible>")
if el.get('userVisible') is not None:
emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent)
if el.get('enabled') is False or el.get('disabled') is True:
lines.append(f"{indent}<Enabled>false</Enabled>")
if el.get('readOnly') is True:
@@ -3180,6 +3203,11 @@ def emit_attributes(lines, attrs, indent):
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')
# Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute)
if attr.get('view') is not None:
emit_xr_flag(lines, 'View', attr.get('view'), inner)
if attr.get('edit') is not None:
emit_xr_flag(lines, 'Edit', attr.get('edit'), inner)
main_saved = False
if attr.get('main') is True and attr.get('type'):
t = str(attr['type'])
@@ -3285,6 +3313,10 @@ def emit_commands(lines, cmds, indent):
if cmd.get('tooltip'):
emit_mltext(lines, inner, 'ToolTip', cmd['tooltip'])
# Доступность команды по ролям (после ToolTip, до Action)
if cmd.get('use') is not None:
emit_xr_flag(lines, 'Use', cmd.get('use'), inner)
if cmd.get('action'):
lines.append(f'{inner}<Action>{cmd["action"]}</Action>')
@@ -1,4 +1,4 @@
# form-decompile v0.27 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.28 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -779,12 +779,36 @@ function Get-Events {
return $events
}
# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use).
# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }.
# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует.
function Decompile-XrFlag {
param($node, [string]$tag)
$el = $node.SelectSingleNode("*[local-name()='$tag']")
if (-not $el) { return $null }
$commonNode = $el.SelectSingleNode("*[local-name()='Common']")
$common = ($commonNode -and $commonNode.InnerText -eq 'true')
$valNodes = @($el.SelectNodes("*[local-name()='Value']"))
if ($valNodes.Count -eq 0) { return $common }
$roles = [ordered]@{}
foreach ($v in $valNodes) {
$rn = $v.GetAttribute("name")
if ($rn -match '^Role\.') { $rn = $rn.Substring(5) }
$roles[$rn] = ($v.InnerText -eq 'true')
}
$o = [ordered]@{}
$o['common'] = $common
$o['roles'] = $roles
return $o
}
# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash
function Add-CommonProps {
param($obj, $node, [string]$elName)
if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true }
if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true }
if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true }
$uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv }
$titleNode = $node.SelectSingleNode("lf:Title", $ns)
if ($titleNode) {
$t = Get-LangText $titleNode
@@ -1186,6 +1210,8 @@ if ($attrsNode) {
$ao['name'] = $a.GetAttribute("name")
$ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty }
if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true }
$vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw }
$ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed }
$tNode = $a.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $ao['title'] = $t } }
if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true }
$fc = Get-Child $a 'FillChecking'; if ($fc) { $ao['fillChecking'] = $fc }
@@ -1279,6 +1305,7 @@ if ($cmdsNode) {
$act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act }
$tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } }
$ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } }
$us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us }
$cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru }
$sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc }
$ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText }
+33
View File
@@ -114,6 +114,7 @@
| `hidden` | bool | `true``<Visible>false</Visible>` |
| `disabled` | bool | `true``<Enabled>false</Enabled>` |
| `readOnly` | bool | `true``<ReadOnly>true</ReadOnly>` |
| `userVisible` | bool/object | Пользовательская видимость по ролям (`<UserVisible>`). См. §4.1c. Отсутствие = виден всем |
| `events` | object | Обработчики событий: `{ "ИмяСобытия": "ИмяОбработчика" }` — тот же формат, что у событий формы (§3). Значение `null` → имя по конвенции (§4.2). См. §4.2 |
| `titleLocation` | string | Расположение заголовка: `none`/`left`/`right`/`top`/`bottom`/`auto`. Эмитится при наличии (input, labelField, picField, table, calendar). У `check`/`radio` — особая семантика с умным дефолтом (см. их разделы) |
| `tooltip` | string/object | Всплывающая подсказка элемента (`<ToolTip>`). Строка → ru, объект `{ "ru": …, "en": … }` → мультиязычный (как `title`). Эмитится сразу после `title` |
@@ -131,6 +132,35 @@
Флаг авто-детектится по наличию известной разметки/`</>`: для plain-строки объект не нужен. Явная форма `{text, formatted}` — только когда авто-детект неверен (formatted-текст без разметки, либо буквальные `<…>`-плейсхолдеры в неформатированном).
### 4.1c. Доступ по ролям (`userVisible` / `view` / `edit` / `use`)
Единый механизм платформы (role-adjustable boolean): «общее значение + исключения по ролям».
Один и тот же грамматик-значения у разных ключей на разных владельцах:
| Ключ | Владелец | XML-тег | Смысл |
|------|----------|---------|-------|
| `userVisible` | элемент (§4.1) | `<UserVisible>` | пользовательская видимость |
| `view` | реквизит (§5) | `<View>` | просмотр |
| `edit` | реквизит (§5) | `<Edit>` | редактирование |
| `use` | команда (§7) | `<Use>` | доступность команды |
**Значение** (общее для всех четырёх):
- скаляр `false`/`true` → только `<xr:Common>`, без ролей (массовый случай, особенно `userVisible: false`);
- объект `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }``<xr:Common>` + по `<xr:Value name="Role.ИмяРоли">` на каждое исключение.
Семантика как в конфигураторе (три состояния флага роли): роль, **не указанная** в `roles`, наследует `common`; указанная — задаёт явный `true`/`false` (может совпадать с `common`).
**Имя роли** — forgiving: принимается без префикса (`ПолныеПрава`), с `Role.` или кириллическим `Роль.`; нормализуется в `Role.ИмяРоли`.
**Отсутствие ключа** = полный доступ (платформа тег не пишет) — дефолт не эмитим.
```jsonc
{ "inputField": "Поле", "userVisible": false } // скрыт у всех
{ "name": "Реквизит", "view": false, // не виден…
"edit": { "common": false, "roles": { "ПолныеПрава": true } } } // …и редактируем только Полными правами
{ "name": "Команда", "use": { "common": false, "roles": { "Роль.Бухгалтер": true } } }
```
### 4.1a. Общие layout-свойства
Применимы к любому элементу (размеры, растягивание, выравнивание внутри родителя). Эмитятся только при указании.
@@ -542,6 +572,8 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
| `type` | string | Тип (shorthand) |
| `main` | bool | Основной реквизит формы |
| `title` | string | Заголовок |
| `view` | bool/object | Просмотр по ролям (`<View>`). См. §4.1c |
| `edit` | bool/object | Редактирование по ролям (`<Edit>`). См. §4.1c |
| `savedData` | bool | Сохраняемые данные |
| `fillChecking` | string | `Show`, `DontShow` |
| `columns` | array | Колонки для ValueTable/ValueTree |
@@ -630,6 +662,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
| `action` | string | Имя процедуры-обработчика |
| `title` | string | Заголовок |
| `tooltip` | string/object | Всплывающая подсказка команды (`<ToolTip>`) |
| `use` | bool/object | Доступность команды по ролям (`<Use>`). См. §4.1c |
| `currentRowUse` | string | Использование текущей строки: `Auto`, `DontUse`, `Use` |
| `shortcut` | string | Клавиатурное сочетание |
| `picture` | string | Ссылка на картинку |
@@ -17,14 +17,14 @@
"title": "Разные типы",
"elements": [
{ "input": "Строка", "path": "Строка" },
{ "input": "Число", "path": "Число" },
{ "input": "Число", "path": "Число", "userVisible": false },
{ "input": "Дата", "path": "Дата" },
{ "input": "Булево", "path": "Булево" }
],
"attributes": [
{ "name": "Объект", "type": "DataProcessorObject.Типы", "main": true },
{ "name": "Строка", "type": "string(200)" },
{ "name": "Число", "type": "decimal(10,0,nonneg)" },
{ "name": "Строка", "type": "string(200)", "view": false },
{ "name": "Число", "type": "decimal(10,0,nonneg)", "edit": false },
{ "name": "Дата", "type": "dateTime" },
{ "name": "Булево", "type": "boolean" }
]
@@ -27,7 +27,7 @@
{ "name": "Результат", "type": "string" }
],
"commands": [
{ "name": "Выполнить", "action": "ВыполнитьОбработка", "shortcut": "Ctrl+Enter" }
{ "name": "Выполнить", "action": "ВыполнитьОбработка", "shortcut": "Ctrl+Enter", "use": false }
]
}
}
@@ -16,6 +16,9 @@
</InputField>
<InputField name="Число" id="4">
<DataPath>Число</DataPath>
<UserVisible>
<xr:Common>false</xr:Common>
</UserVisible>
<ContextMenu name="ЧислоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="ЧислоРасширеннаяПодсказка" id="6"/>
</InputField>
@@ -51,6 +54,9 @@
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</Type>
<View>
<xr:Common>false</xr:Common>
</View>
</Attribute>
<Attribute name="Число" id="15">
<Title>
@@ -67,6 +73,9 @@
<v8:AllowedSign>Nonnegative</v8:AllowedSign>
</v8:NumberQualifiers>
</Type>
<Edit>
<xr:Common>false</xr:Common>
</Edit>
</Attribute>
<Attribute name="Дата" id="16">
<Title>
@@ -68,6 +68,9 @@
<v8:content>Выполнить</v8:content>
</v8:item>
</Title>
<Use>
<xr:Common>false</xr:Common>
</Use>
<Action>ВыполнитьОбработка</Action>
<Shortcut>Ctrl+Enter</Shortcut>
</Command>