feat(form-decompile,form-compile): унификация событий элементов на events-мапу (кластер Events DSL)

Несогласованность DSL: события ФОРМЫ описывались интуитивной мапой
events:{Событие:Обработчик}, а события ЭЛЕМЕНТА — двумя сущностями
on:[...] + handlers:{...}. Два способа для одного понятия путали модель.

Унифицировано на единую мапу events:{Событие:ИмяОбработчика} на форме И
элементах (как form-level). Декомпилятор эмитит только её, с явными именами
обработчиков (прозрачно, консистентно с form-level).

Компилятор (ps1+py):
- Emit-Events читает events-мапу (основной формат); значение null/"" →
  имя по конвенции ИмяЭлемента+суффикс (прощающий fallback).
- legacy on/handlers по-прежнему принимаются ради совместимости (не эмитятся).
- choiceButton: проверка StartChoice через оба формата (Test-ElementEvent).
- events добавлен в whitelist ключей элемента.

Декомпилятор: Get-Events → упорядоченная мапа {Событие:Обработчик} в порядке
документа; убраны on/handlers и инверсия авто-имён.

spec/SKILL.md: events как единственный рекомендованный формат, on/handlers
помечены legacy. В SKILL.md только явные имена (null-сахар — деталь spec,
инструкцию не раздуваем).

Корпус acc_8.3.24: 190 элементов в 114/400 форм теряли Events до фикса (баг
on/handlers разобран отдельным коммитом). Раундтрип 2.17: Events ушли из топа
LOST, match 4→6, 0 compile-fail. Регресс ps+py 32/32, снэпшот events (добавлен
блок Events у поля с переименованным обработчиком) сертифицирован в 1С 8.3.24.

Follow-up: form-edit использует расширенный on с {event,callType} —
унификация отдельным решением (см. BACKLOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-05 12:46:56 +03:00
co-authored by Claude Opus 4.8
parent a38874280c
commit 4c2c72abce
7 changed files with 127 additions and 82 deletions
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.31 — Compile 1C managed form from JSON or object metadata
# form-compile v1.32 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1356,7 +1356,7 @@ KNOWN_KEYS = {
"radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode",
"name", "path", "title",
"visible", "hidden", "enabled", "disabled", "readOnly", "userVisible",
"on", "handlers",
"events", "on", "handlers",
"titleLocation", "representation", "width", "height",
"horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
"maxWidth", "maxHeight",
@@ -1533,26 +1533,53 @@ def get_element_name(el, type_key):
return str(el.get(type_key, ''))
# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL.
# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции).
# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён).
def get_event_pairs(el, element_name):
pairs = []
events = el.get('events')
if events:
for ev_name, val in events.items():
handler = '' if val is None else str(val)
if not handler:
handler = get_handler_name(element_name, ev_name)
pairs.append((ev_name, handler))
elif el.get('on'):
handlers = el.get('handlers') or {}
for evt in el['on']:
evt_name = str(evt)
if handlers.get(evt_name):
handler = str(handlers[evt_name])
else:
handler = get_handler_name(element_name, evt_name)
pairs.append((evt_name, handler))
return pairs
# Проверить, подключено ли событие к элементу (в любом из форматов).
def test_element_event(el, event_name):
events = el.get('events')
if events and event_name in events:
return True
return event_name in (el.get('on') or [])
def emit_events(lines, el, element_name, indent, type_key):
if not el.get('on'):
pairs = get_event_pairs(el, element_name)
if not pairs:
return
# Validate event names
if type_key and type_key in KNOWN_EVENTS:
allowed = KNOWN_EVENTS[type_key]
for evt in el['on']:
if allowed and str(evt) not in allowed:
print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}")
for ev_name, _ in pairs:
if allowed and str(ev_name) not in allowed:
print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}")
lines.append(f"{indent}<Events>")
for evt in el['on']:
evt_name = str(evt)
handlers = el.get('handlers')
if handlers and handlers.get(evt_name):
handler = str(handlers[evt_name])
else:
handler = get_handler_name(element_name, evt_name)
lines.append(f'{indent}\t<Event name="{evt_name}">{handler}</Event>')
for ev_name, handler in pairs:
lines.append(f'{indent}\t<Event name="{ev_name}">{handler}</Event>')
lines.append(f"{indent}</Events>")
@@ -2015,7 +2042,7 @@ def emit_input(lines, el, name, eid, indent):
lines.append(f'{inner}<PasswordMode>true</PasswordMode>')
if el.get('choiceButton') is False:
lines.append(f'{inner}<ChoiceButton>false</ChoiceButton>')
elif el.get('choiceButton') is True and 'StartChoice' in (el.get('on') or []):
elif el.get('choiceButton') is True and test_element_event(el, 'StartChoice'):
lines.append(f'{inner}<ChoiceButton>true</ChoiceButton>')
if el.get('clearButton') is True:
lines.append(f'{inner}<ClearButton>true</ClearButton>')