mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
1322 lines
51 KiB
Python
1322 lines
51 KiB
Python
# form-edit v1.0 — Edit 1C managed form elements (Python port)
|
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
from lxml import etree
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
# ── arg parsing ──────────────────────────────────────────────
|
|
|
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
parser.add_argument("-FormPath", "-Path", required=True)
|
|
parser.add_argument("-JsonPath", required=True)
|
|
args = parser.parse_args()
|
|
|
|
form_path = args.FormPath
|
|
json_path = args.JsonPath
|
|
|
|
# ── namespaces ───────────────────────────────────────────────
|
|
|
|
FORM_NS = "http://v8.1c.ru/8.3/xcf/logform"
|
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|
NS = {
|
|
"f": FORM_NS,
|
|
"v8": V8_NS,
|
|
}
|
|
|
|
ALL_NS_DECL = (
|
|
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
' xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema"'
|
|
)
|
|
|
|
|
|
def local_name(node):
|
|
return etree.QName(node.tag).localname
|
|
|
|
|
|
# ── helpers ──────────────────────────────────────────────────
|
|
|
|
def esc_xml(s):
|
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
|
|
|
|
# ── 1. Load Form.xml ────────────────────────────────────────
|
|
|
|
if not os.path.exists(form_path):
|
|
print(f"File not found: {form_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if not os.path.exists(json_path):
|
|
print(f"File not found: {json_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
resolved_form_path = os.path.abspath(form_path)
|
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
try:
|
|
tree = etree.parse(resolved_form_path, xml_parser)
|
|
except etree.XMLSyntaxError as e:
|
|
print(f"[ERROR] XML parse error: {e}")
|
|
sys.exit(1)
|
|
|
|
root = tree.getroot()
|
|
|
|
# ── 2. Load JSON ────────────────────────────────────────────
|
|
|
|
with open(json_path, "r", encoding="utf-8-sig") as f:
|
|
defn = json.load(f)
|
|
|
|
# ── 3. Form name + header ───────────────────────────────────
|
|
|
|
form_name = os.path.splitext(os.path.basename(form_path))[0]
|
|
parent_dir = os.path.dirname(resolved_form_path)
|
|
if parent_dir:
|
|
ext_dir = os.path.basename(parent_dir)
|
|
if ext_dir == "Ext":
|
|
form_dir = os.path.dirname(parent_dir)
|
|
if form_dir:
|
|
form_name = os.path.basename(form_dir)
|
|
|
|
print(f"=== form-edit: {form_name} ===")
|
|
print()
|
|
|
|
# ── 4. Scan max IDs per pool ────────────────────────────────
|
|
|
|
next_elem_id = 0
|
|
next_attr_id = 0
|
|
next_cmd_id = 0
|
|
|
|
|
|
def _scan_id(node, attr="id"):
|
|
val = node.get(attr)
|
|
if val and val != "-1":
|
|
try:
|
|
return int(val)
|
|
except ValueError:
|
|
pass
|
|
return -1
|
|
|
|
|
|
# Scan element IDs
|
|
root_ci = root.find("f:ChildItems", NS)
|
|
if root_ci is not None:
|
|
for elem in root_ci.iter():
|
|
v = _scan_id(elem)
|
|
if v > next_elem_id:
|
|
next_elem_id = v
|
|
|
|
acb = root.find("f:AutoCommandBar", NS)
|
|
if acb is not None:
|
|
v = _scan_id(acb)
|
|
if v > next_elem_id:
|
|
next_elem_id = v
|
|
|
|
# Scan attribute IDs (including column IDs - same pool)
|
|
for attr_el in root.findall("f:Attributes/f:Attribute", NS):
|
|
v = _scan_id(attr_el)
|
|
if v > next_attr_id:
|
|
next_attr_id = v
|
|
for col_el in attr_el.findall("f:Columns/f:Column", NS):
|
|
v = _scan_id(col_el)
|
|
if v > next_attr_id:
|
|
next_attr_id = v
|
|
|
|
# Scan command IDs
|
|
for cmd_el in root.findall("f:Commands/f:Command", NS):
|
|
v = _scan_id(cmd_el)
|
|
if v > next_cmd_id:
|
|
next_cmd_id = v
|
|
|
|
next_elem_id += 1
|
|
next_attr_id += 1
|
|
next_cmd_id += 1
|
|
|
|
# --- 4b. Auto-detect extension mode (BaseForm present) ---
|
|
is_extension = False
|
|
base_form = root.find("f:BaseForm", NS)
|
|
if base_form is not None:
|
|
is_extension = True
|
|
if next_attr_id < 1000000:
|
|
next_attr_id = 1000000
|
|
if next_cmd_id < 1000000:
|
|
next_cmd_id = 1000000
|
|
if next_elem_id < 1000000:
|
|
next_elem_id = 1000000
|
|
|
|
|
|
def new_elem_id():
|
|
global next_elem_id
|
|
_id = next_elem_id
|
|
next_elem_id += 1
|
|
return _id
|
|
|
|
|
|
def new_attr_id():
|
|
global next_attr_id
|
|
_id = next_attr_id
|
|
next_attr_id += 1
|
|
return _id
|
|
|
|
|
|
def new_cmd_id():
|
|
global next_cmd_id
|
|
_id = next_cmd_id
|
|
next_cmd_id += 1
|
|
return _id
|
|
|
|
|
|
new_id = new_elem_id # alias for element emitters
|
|
|
|
# ── 5. Fragment helpers (StringBuilder + Emit-* from form-compile) ──
|
|
|
|
xml_lines = []
|
|
|
|
|
|
def X(text):
|
|
xml_lines.append(text)
|
|
|
|
|
|
# --- Type emitter ---
|
|
|
|
_FORM_TYPE_SYNONYMS = {
|
|
"строка": "string", "число": "decimal", "булево": "boolean",
|
|
"дата": "date", "датавремя": "dateTime",
|
|
"number": "decimal", "bool": "boolean",
|
|
"справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject",
|
|
"документссылка": "DocumentRef", "документобъект": "DocumentObject",
|
|
"перечислениессылка": "EnumRef",
|
|
"плансчетовссылка": "ChartOfAccountsRef",
|
|
"планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef",
|
|
"планвидоврасчётассылка": "ChartOfCalculationTypesRef",
|
|
"планвидоврасчетассылка": "ChartOfCalculationTypesRef",
|
|
"планобменассылка": "ExchangePlanRef",
|
|
"бизнеспроцессссылка": "BusinessProcessRef",
|
|
"задачассылка": "TaskRef",
|
|
"определяемыйтип": "DefinedType",
|
|
}
|
|
|
|
|
|
def resolve_type_str(type_str):
|
|
if not type_str:
|
|
return type_str
|
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
|
if m:
|
|
base, params = m.group(1).strip(), m.group(2)
|
|
r = _FORM_TYPE_SYNONYMS.get(base.lower())
|
|
return f"{r}({params})" if r else type_str
|
|
if '.' in type_str:
|
|
i = type_str.index('.')
|
|
prefix, suffix = type_str[:i], type_str[i:]
|
|
r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
|
|
return f"{r}{suffix}" if r else type_str
|
|
r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
|
|
return r if r else type_str
|
|
|
|
|
|
def emit_type(type_str, indent):
|
|
if not type_str:
|
|
X(f"{indent}<Type/>")
|
|
return
|
|
type_string = str(type_str)
|
|
parts = [p.strip() for p in re.split(r'[|+]', type_string)]
|
|
X(f"{indent}<Type>")
|
|
for part in parts:
|
|
emit_single_type(part, indent + "\t")
|
|
X(f"{indent}</Type>")
|
|
|
|
|
|
def emit_single_type(type_str, indent):
|
|
type_str = resolve_type_str(type_str)
|
|
if type_str == "boolean":
|
|
X(f"{indent}<v8:Type>xs:boolean</v8:Type>")
|
|
return
|
|
|
|
m = re.match(r'^string(\((\d+)\))?$', type_str)
|
|
if m:
|
|
length = m.group(2) if m.group(2) else "0"
|
|
X(f"{indent}<v8:Type>xs:string</v8:Type>")
|
|
X(f"{indent}<v8:StringQualifiers>")
|
|
X(f"{indent}\t<v8:Length>{length}</v8:Length>")
|
|
X(f"{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>")
|
|
X(f"{indent}</v8:StringQualifiers>")
|
|
return
|
|
|
|
m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
|
|
if m:
|
|
digits = m.group(1)
|
|
fraction = m.group(2)
|
|
sign = "Nonnegative" if m.group(3) else "Any"
|
|
X(f"{indent}<v8:Type>xs:decimal</v8:Type>")
|
|
X(f"{indent}<v8:NumberQualifiers>")
|
|
X(f"{indent}\t<v8:Digits>{digits}</v8:Digits>")
|
|
X(f"{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>")
|
|
X(f"{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>")
|
|
X(f"{indent}</v8:NumberQualifiers>")
|
|
return
|
|
|
|
m = re.match(r'^(date|dateTime|time)$', type_str)
|
|
if m:
|
|
fractions_map = {"date": "Date", "dateTime": "DateTime", "time": "Time"}
|
|
fractions = fractions_map[type_str]
|
|
X(f"{indent}<v8:Type>xs:dateTime</v8:Type>")
|
|
X(f"{indent}<v8:DateQualifiers>")
|
|
X(f"{indent}\t<v8:DateFractions>{fractions}</v8:DateFractions>")
|
|
X(f"{indent}</v8:DateQualifiers>")
|
|
return
|
|
|
|
v8_types = {
|
|
"ValueTable": "v8:ValueTable", "ValueTree": "v8:ValueTree", "ValueList": "v8:ValueListType",
|
|
"TypeDescription": "v8:TypeDescription", "Universal": "v8:Universal",
|
|
"FixedArray": "v8:FixedArray", "FixedStructure": "v8:FixedStructure",
|
|
}
|
|
if type_str in v8_types:
|
|
X(f"{indent}<v8:Type>{v8_types[type_str]}</v8:Type>")
|
|
return
|
|
|
|
ui_types = {"FormattedString": "v8ui:FormattedString", "Picture": "v8ui:Picture", "Color": "v8ui:Color", "Font": "v8ui:Font"}
|
|
if type_str in ui_types:
|
|
X(f"{indent}<v8:Type>{ui_types[type_str]}</v8:Type>")
|
|
return
|
|
|
|
if type_str == "DynamicList":
|
|
X(f"{indent}<v8:Type>cfg:DynamicList</v8:Type>")
|
|
return
|
|
|
|
if type_str.startswith("DataComposition"):
|
|
dcs_map = {
|
|
"DataCompositionSettings": "dcsset:DataCompositionSettings",
|
|
"DataCompositionSchema": "dcssch:DataCompositionSchema",
|
|
"DataCompositionComparisonType": "dcscor:DataCompositionComparisonType",
|
|
}
|
|
if type_str in dcs_map:
|
|
X(f"{indent}<v8:Type>{dcs_map[type_str]}</v8:Type>")
|
|
return
|
|
|
|
if re.match(r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.', type_str):
|
|
X(f"{indent}<v8:Type>cfg:{type_str}</v8:Type>")
|
|
return
|
|
|
|
if "." in type_str:
|
|
X(f"{indent}<v8:Type>cfg:{type_str}</v8:Type>")
|
|
else:
|
|
X(f"{indent}<v8:Type>{type_str}</v8:Type>")
|
|
|
|
|
|
def emit_mltext(tag, text, indent):
|
|
X(f"{indent}<{tag}>")
|
|
X(f"{indent}\t<v8:item>")
|
|
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
|
X(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
|
X(f"{indent}\t</v8:item>")
|
|
X(f"{indent}</{tag}>")
|
|
|
|
|
|
# --- Event handler name generator ---
|
|
|
|
event_suffix_map = {
|
|
"OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438",
|
|
"StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430",
|
|
"ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430",
|
|
"AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440",
|
|
"Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430",
|
|
"Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435",
|
|
"Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435",
|
|
"OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438",
|
|
"BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
|
|
"BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c",
|
|
"BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f",
|
|
"OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
|
|
"OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
|
|
"Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438",
|
|
"OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b",
|
|
"TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430",
|
|
"URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438",
|
|
"DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
|
|
"Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435",
|
|
"DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
|
|
"Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
|
|
"AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f",
|
|
}
|
|
|
|
|
|
def get_handler_name(element_name, event_name):
|
|
suffix = event_suffix_map.get(event_name)
|
|
if suffix:
|
|
return f"{element_name}{suffix}"
|
|
return f"{element_name}{event_name}"
|
|
|
|
|
|
# --- Element helpers ---
|
|
|
|
def get_element_name(el, type_key):
|
|
if "name" in el and el["name"]:
|
|
return str(el["name"])
|
|
return str(el[type_key])
|
|
|
|
|
|
known_events = {
|
|
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
|
|
"check": ["OnChange"],
|
|
"label": ["Click", "URLProcessing"],
|
|
"labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"],
|
|
"table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"],
|
|
"pages": ["OnCurrentPageChange"],
|
|
"page": ["OnCurrentPageChange"],
|
|
"button": ["Click"],
|
|
"picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"],
|
|
"calendar": ["OnChange", "OnActivate"],
|
|
"picture": ["Click"],
|
|
"cmdBar": [],
|
|
"popup": [],
|
|
"group": [],
|
|
}
|
|
|
|
|
|
def emit_events(el, element_name, indent, type_key):
|
|
on_list = el.get("on")
|
|
if not on_list:
|
|
return
|
|
|
|
# Validate event names
|
|
if type_key and type_key in known_events:
|
|
allowed = known_events[type_key]
|
|
for evt in on_list:
|
|
evt_str = evt if isinstance(evt, str) else str(evt.get("event", ""))
|
|
if allowed and evt_str not in allowed:
|
|
print(f"[WARN] Unknown event '{evt_str}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}")
|
|
|
|
X(f"{indent}<Events>")
|
|
handlers_map = el.get("handlers", {}) or {}
|
|
for evt in on_list:
|
|
if isinstance(evt, str):
|
|
evt_name = evt
|
|
handler = handlers_map.get(evt_name) or get_handler_name(element_name, evt_name)
|
|
X(f'{indent}\t<Event name="{evt_name}">{handler}</Event>')
|
|
elif not evt.get("event"):
|
|
evt_name = str(evt)
|
|
handler = handlers_map.get(evt_name) or get_handler_name(element_name, evt_name)
|
|
X(f'{indent}\t<Event name="{evt_name}">{handler}</Event>')
|
|
else:
|
|
evt_name = str(evt["event"])
|
|
handler = evt.get("handler") or handlers_map.get(evt_name) or get_handler_name(element_name, evt_name)
|
|
call_type_attr = f' callType="{evt["callType"]}"' if evt.get("callType") else ""
|
|
X(f'{indent}\t<Event name="{evt_name}"{call_type_attr}>{handler}</Event>')
|
|
X(f"{indent}</Events>")
|
|
|
|
|
|
def emit_companion(tag, name, indent):
|
|
_id = new_id()
|
|
X(f'{indent}<{tag} name="{name}" id="{_id}"/>')
|
|
|
|
|
|
def emit_common_flags(el, indent):
|
|
if el.get("visible") is False or el.get("hidden") is True:
|
|
X(f"{indent}<Visible>false</Visible>")
|
|
if el.get("enabled") is False or el.get("disabled") is True:
|
|
X(f"{indent}<Enabled>false</Enabled>")
|
|
if el.get("readOnly") is True:
|
|
X(f"{indent}<ReadOnly>true</ReadOnly>")
|
|
|
|
|
|
def emit_title(el, name, indent):
|
|
if el.get("title"):
|
|
emit_mltext("Title", str(el["title"]), indent)
|
|
|
|
|
|
# --- Element emitters ---
|
|
|
|
def emit_group(el, name, _id, indent):
|
|
X(f'{indent}<UsualGroup name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
emit_title(el, name, inner)
|
|
group_val = str(el.get("group", ""))
|
|
orientation_map = {"horizontal": "Horizontal", "vertical": "Vertical", "alwaysHorizontal": "AlwaysHorizontal", "alwaysVertical": "AlwaysVertical"}
|
|
orientation = orientation_map.get(group_val)
|
|
if orientation:
|
|
X(f"{inner}<Group>{orientation}</Group>")
|
|
if group_val == "collapsible":
|
|
X(f"{inner}<Group>Vertical</Group>")
|
|
X(f"{inner}<Behavior>Collapsible</Behavior>")
|
|
if el.get("representation"):
|
|
repr_map = {"none": "None", "normal": "NormalSeparation", "weak": "WeakSeparation", "strong": "StrongSeparation"}
|
|
repr_val = repr_map.get(str(el["representation"]), str(el["representation"]))
|
|
X(f"{inner}<Representation>{repr_val}</Representation>")
|
|
if el.get("showTitle") is False:
|
|
X(f"{inner}<ShowTitle>false</ShowTitle>")
|
|
if el.get("united") is False:
|
|
X(f"{inner}<United>false</United>")
|
|
emit_common_flags(el, inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
children = el.get("children")
|
|
if children and len(children) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for child in children:
|
|
emit_element(child, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
X(f"{indent}</UsualGroup>")
|
|
|
|
|
|
def emit_input(el, name, _id, indent):
|
|
X(f'{indent}<InputField name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("titleLocation"):
|
|
loc_map = {"none": "None", "left": "Left", "right": "Right", "top": "Top", "bottom": "Bottom"}
|
|
loc = loc_map.get(str(el["titleLocation"]), str(el["titleLocation"]))
|
|
X(f"{inner}<TitleLocation>{loc}</TitleLocation>")
|
|
if el.get("multiLine") is True:
|
|
X(f"{inner}<MultiLine>true</MultiLine>")
|
|
if el.get("passwordMode") is True:
|
|
X(f"{inner}<PasswordMode>true</PasswordMode>")
|
|
if el.get("choiceButton") is False:
|
|
X(f"{inner}<ChoiceButton>false</ChoiceButton>")
|
|
if el.get("clearButton") is True:
|
|
X(f"{inner}<ClearButton>true</ClearButton>")
|
|
if el.get("spinButton") is True:
|
|
X(f"{inner}<SpinButton>true</SpinButton>")
|
|
if el.get("dropListButton") is True:
|
|
X(f"{inner}<DropListButton>true</DropListButton>")
|
|
if el.get("markIncomplete") is True:
|
|
X(f"{inner}<AutoMarkIncomplete>true</AutoMarkIncomplete>")
|
|
if el.get("skipOnInput") is True:
|
|
X(f"{inner}<SkipOnInput>true</SkipOnInput>")
|
|
if el.get("autoMaxWidth") is False:
|
|
X(f"{inner}<AutoMaxWidth>false</AutoMaxWidth>")
|
|
if el.get("autoMaxHeight") is False:
|
|
X(f"{inner}<AutoMaxHeight>false</AutoMaxHeight>")
|
|
if el.get("width"):
|
|
X(f"{inner}<Width>{el['width']}</Width>")
|
|
if el.get("height"):
|
|
X(f"{inner}<Height>{el['height']}</Height>")
|
|
if el.get("horizontalStretch") is True:
|
|
X(f"{inner}<HorizontalStretch>true</HorizontalStretch>")
|
|
if el.get("verticalStretch") is True:
|
|
X(f"{inner}<VerticalStretch>true</VerticalStretch>")
|
|
if el.get("inputHint"):
|
|
emit_mltext("InputHint", str(el["inputHint"]), inner)
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "input")
|
|
X(f"{indent}</InputField>")
|
|
|
|
|
|
def emit_check(el, name, _id, indent):
|
|
X(f'{indent}<CheckBoxField name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("titleLocation"):
|
|
X(f"{inner}<TitleLocation>{el['titleLocation']}</TitleLocation>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "check")
|
|
X(f"{indent}</CheckBoxField>")
|
|
|
|
|
|
def emit_label(el, name, _id, indent):
|
|
X(f'{indent}<LabelDecoration name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("title"):
|
|
formatted = "true" if el.get("hyperlink") is True else "false"
|
|
X(f'{inner}<Title formatted="{formatted}">')
|
|
X(f"{inner}\t<v8:item>")
|
|
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
|
|
X(f"{inner}\t\t<v8:content>{esc_xml(str(el['title']))}</v8:content>")
|
|
X(f"{inner}\t</v8:item>")
|
|
X(f"{inner}</Title>")
|
|
emit_common_flags(el, inner)
|
|
if el.get("hyperlink") is True:
|
|
X(f"{inner}<Hyperlink>true</Hyperlink>")
|
|
if el.get("autoMaxWidth") is False:
|
|
X(f"{inner}<AutoMaxWidth>false</AutoMaxWidth>")
|
|
if el.get("autoMaxHeight") is False:
|
|
X(f"{inner}<AutoMaxHeight>false</AutoMaxHeight>")
|
|
if el.get("width"):
|
|
X(f"{inner}<Width>{el['width']}</Width>")
|
|
if el.get("height"):
|
|
X(f"{inner}<Height>{el['height']}</Height>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "label")
|
|
X(f"{indent}</LabelDecoration>")
|
|
|
|
|
|
def emit_label_field(el, name, _id, indent):
|
|
X(f'{indent}<LabelField name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("hyperlink") is True:
|
|
X(f"{inner}<Hyperlink>true</Hyperlink>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "labelField")
|
|
X(f"{indent}</LabelField>")
|
|
|
|
|
|
def emit_table(el, name, _id, indent):
|
|
X(f'{indent}<Table name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("representation"):
|
|
X(f"{inner}<Representation>{el['representation']}</Representation>")
|
|
if el.get("changeRowSet") is True:
|
|
X(f"{inner}<ChangeRowSet>true</ChangeRowSet>")
|
|
if el.get("changeRowOrder") is True:
|
|
X(f"{inner}<ChangeRowOrder>true</ChangeRowOrder>")
|
|
if el.get("height"):
|
|
X(f"{inner}<HeightInTableRows>{el['height']}</HeightInTableRows>")
|
|
if el.get("header") is False:
|
|
X(f"{inner}<Header>false</Header>")
|
|
if el.get("footer") is True:
|
|
X(f"{inner}<Footer>true</Footer>")
|
|
if el.get("commandBarLocation"):
|
|
X(f"{inner}<CommandBarLocation>{el['commandBarLocation']}</CommandBarLocation>")
|
|
if el.get("searchStringLocation"):
|
|
X(f"{inner}<SearchStringLocation>{el['searchStringLocation']}</SearchStringLocation>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("AutoCommandBar", f"{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c", inner)
|
|
emit_companion("SearchStringAddition", f"{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430", inner)
|
|
emit_companion("ViewStatusAddition", f"{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430", inner)
|
|
emit_companion("SearchControlAddition", f"{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c", inner)
|
|
columns = el.get("columns")
|
|
if columns and len(columns) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for col in columns:
|
|
emit_element(col, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
emit_events(el, name, inner, "table")
|
|
X(f"{indent}</Table>")
|
|
|
|
|
|
def emit_pages(el, name, _id, indent):
|
|
X(f'{indent}<Pages name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("pagesRepresentation"):
|
|
X(f"{inner}<PagesRepresentation>{el['pagesRepresentation']}</PagesRepresentation>")
|
|
emit_common_flags(el, inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "pages")
|
|
children = el.get("children")
|
|
if children and len(children) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for child in children:
|
|
emit_element(child, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
X(f"{indent}</Pages>")
|
|
|
|
|
|
def emit_page(el, name, _id, indent):
|
|
X(f'{indent}<Page name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("group"):
|
|
orientation_map = {"horizontal": "Horizontal", "vertical": "Vertical", "alwaysHorizontal": "AlwaysHorizontal", "alwaysVertical": "AlwaysVertical"}
|
|
orientation = orientation_map.get(str(el["group"]))
|
|
if orientation:
|
|
X(f"{inner}<Group>{orientation}</Group>")
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
children = el.get("children")
|
|
if children and len(children) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for child in children:
|
|
emit_element(child, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
X(f"{indent}</Page>")
|
|
|
|
|
|
def emit_button(el, name, _id, indent):
|
|
X(f'{indent}<Button name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("type"):
|
|
btn_map = {"usual": "UsualButton", "hyperlink": "Hyperlink", "commandBar": "CommandBarButton"}
|
|
btn_type = btn_map.get(str(el["type"]), str(el["type"]))
|
|
X(f"{inner}<Type>{btn_type}</Type>")
|
|
if el.get("command"):
|
|
X(f"{inner}<CommandName>Form.Command.{el['command']}</CommandName>")
|
|
if el.get("stdCommand"):
|
|
sc = str(el["stdCommand"])
|
|
m = re.match(r'^(.+)\.(.+)$', sc)
|
|
if m:
|
|
X(f"{inner}<CommandName>Form.Item.{m.group(1)}.StandardCommand.{m.group(2)}</CommandName>")
|
|
else:
|
|
X(f"{inner}<CommandName>Form.StandardCommand.{sc}</CommandName>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("defaultButton") is True:
|
|
X(f"{inner}<DefaultButton>true</DefaultButton>")
|
|
if el.get("picture"):
|
|
X(f"{inner}<Picture>")
|
|
X(f"{inner}\t<xr:Ref>{el['picture']}</xr:Ref>")
|
|
X(f"{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>")
|
|
X(f"{inner}</Picture>")
|
|
if el.get("representation"):
|
|
X(f"{inner}<Representation>{el['representation']}</Representation>")
|
|
if el.get("locationInCommandBar"):
|
|
X(f"{inner}<LocationInCommandBar>{el['locationInCommandBar']}</LocationInCommandBar>")
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "button")
|
|
X(f"{indent}</Button>")
|
|
|
|
|
|
def emit_picture_decoration(el, name, _id, indent):
|
|
X(f'{indent}<PictureDecoration name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
ref = el.get("src") or el.get("picture")
|
|
if ref:
|
|
X(f"{inner}<Picture>")
|
|
X(f"{inner}\t<xr:Ref>{ref}</xr:Ref>")
|
|
X(f"{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>")
|
|
X(f"{inner}</Picture>")
|
|
if el.get("hyperlink") is True:
|
|
X(f"{inner}<Hyperlink>true</Hyperlink>")
|
|
if el.get("width"):
|
|
X(f"{inner}<Width>{el['width']}</Width>")
|
|
if el.get("height"):
|
|
X(f"{inner}<Height>{el['height']}</Height>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "picture")
|
|
X(f"{indent}</PictureDecoration>")
|
|
|
|
|
|
def emit_picture_field(el, name, _id, indent):
|
|
X(f'{indent}<PictureField name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("width"):
|
|
X(f"{inner}<Width>{el['width']}</Width>")
|
|
if el.get("height"):
|
|
X(f"{inner}<Height>{el['height']}</Height>")
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "picField")
|
|
X(f"{indent}</PictureField>")
|
|
|
|
|
|
def emit_calendar(el, name, _id, indent):
|
|
X(f'{indent}<CalendarField name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("path"):
|
|
X(f"{inner}<DataPath>{el['path']}</DataPath>")
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
emit_companion("ContextMenu", f"{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e", inner)
|
|
emit_companion("ExtendedTooltip", f"{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430", inner)
|
|
emit_events(el, name, inner, "calendar")
|
|
X(f"{indent}</CalendarField>")
|
|
|
|
|
|
def emit_command_bar_el(el, name, _id, indent):
|
|
X(f'{indent}<CommandBar name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
if el.get("autofill") is True:
|
|
X(f"{inner}<Autofill>true</Autofill>")
|
|
emit_common_flags(el, inner)
|
|
children = el.get("children")
|
|
if children and len(children) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for child in children:
|
|
emit_element(child, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
X(f"{indent}</CommandBar>")
|
|
|
|
|
|
def emit_popup(el, name, _id, indent):
|
|
X(f'{indent}<Popup name="{name}" id="{_id}">')
|
|
inner = indent + "\t"
|
|
emit_title(el, name, inner)
|
|
emit_common_flags(el, inner)
|
|
if el.get("picture"):
|
|
X(f"{inner}<Picture>")
|
|
X(f"{inner}\t<xr:Ref>{el['picture']}</xr:Ref>")
|
|
X(f"{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>")
|
|
X(f"{inner}</Picture>")
|
|
if el.get("representation"):
|
|
X(f"{inner}<Representation>{el['representation']}</Representation>")
|
|
children = el.get("children")
|
|
if children and len(children) > 0:
|
|
X(f"{inner}<ChildItems>")
|
|
for child in children:
|
|
emit_element(child, inner + "\t")
|
|
X(f"{inner}</ChildItems>")
|
|
X(f"{indent}</Popup>")
|
|
|
|
|
|
# --- Element dispatcher ---
|
|
|
|
ELEMENT_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup"]
|
|
|
|
KNOWN_KEYS = {
|
|
"group", "input", "check", "label", "labelField", "table", "pages", "page",
|
|
"button", "picture", "picField", "calendar", "cmdBar", "popup",
|
|
"name", "path", "title",
|
|
"visible", "hidden", "enabled", "disabled", "readOnly",
|
|
"on", "handlers",
|
|
"titleLocation", "representation", "width", "height",
|
|
"horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
|
|
"multiLine", "passwordMode", "choiceButton", "clearButton",
|
|
"spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
|
|
"hyperlink", "showTitle", "united", "children", "columns",
|
|
"changeRowSet", "changeRowOrder", "header", "footer",
|
|
"commandBarLocation", "searchStringLocation", "pagesRepresentation",
|
|
"type", "command", "stdCommand", "defaultButton", "locationInCommandBar",
|
|
"src", "autofill",
|
|
}
|
|
|
|
EMITTER_MAP = {
|
|
"group": emit_group,
|
|
"input": emit_input,
|
|
"check": emit_check,
|
|
"label": emit_label,
|
|
"labelField": emit_label_field,
|
|
"table": emit_table,
|
|
"pages": emit_pages,
|
|
"page": emit_page,
|
|
"button": emit_button,
|
|
"picture": emit_picture_decoration,
|
|
"picField": emit_picture_field,
|
|
"calendar": emit_calendar,
|
|
"cmdBar": emit_command_bar_el,
|
|
"popup": emit_popup,
|
|
}
|
|
|
|
|
|
def emit_element(el, indent):
|
|
type_key = None
|
|
for key in ELEMENT_KEYS:
|
|
if key in el and el[key] is not None:
|
|
type_key = key
|
|
break
|
|
if not type_key:
|
|
print("[WARN] Unknown element type, skipping")
|
|
return
|
|
|
|
# Validate known keys
|
|
for p in el:
|
|
if p not in KNOWN_KEYS:
|
|
print(f"[WARN] Element '{el[type_key]}': unknown key '{p}' -- ignored.")
|
|
|
|
name = get_element_name(el, type_key)
|
|
_id = new_id()
|
|
|
|
emitter = EMITTER_MAP.get(type_key)
|
|
if emitter:
|
|
emitter(el, name, _id, indent)
|
|
|
|
|
|
# ── 6. Find element by name recursively ─────────────────────
|
|
|
|
def find_element(start_node, target_name):
|
|
for child in start_node:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
child_name = child.get("name")
|
|
if child_name == target_name:
|
|
return child
|
|
ci = child.find("f:ChildItems", NS)
|
|
if ci is not None:
|
|
found = find_element(ci, target_name)
|
|
if found is not None:
|
|
return found
|
|
return None
|
|
|
|
|
|
# ── 7. Detect indent level of a container's children ────────
|
|
|
|
def get_child_indent(container):
|
|
for child_node in container:
|
|
if not isinstance(child_node.tag, str):
|
|
# text nodes - check preceding/following text
|
|
pass
|
|
# Check text content of container (tail/text)
|
|
for i, child in enumerate(container):
|
|
# Check text before this child
|
|
if i == 0:
|
|
txt = container.text
|
|
else:
|
|
txt = container[i - 1].tail
|
|
if txt:
|
|
m = re.search(r'\n(\t+)$', txt)
|
|
if m:
|
|
return m.group(1)
|
|
|
|
# Fallback: count depth from root
|
|
depth = 0
|
|
current = container
|
|
while current is not None:
|
|
parent = current.getparent()
|
|
if parent is None:
|
|
break
|
|
depth += 1
|
|
current = parent
|
|
return "\t" * (depth + 1)
|
|
|
|
|
|
# ── 8. Insert node into container ───────────────────────────
|
|
|
|
def insert_into_container(container, new_node, after_name, child_indent):
|
|
ref_idx = None
|
|
|
|
if after_name:
|
|
# Find the after-element
|
|
after_elem = None
|
|
for i, child in enumerate(container):
|
|
if isinstance(child.tag, str) and child.get("name") == after_name:
|
|
after_elem = child
|
|
ref_idx = i + 1
|
|
break
|
|
if after_elem is None:
|
|
print(f"[WARN] after='{after_name}' not found in target container, appending at end")
|
|
|
|
children = list(container)
|
|
if ref_idx is not None:
|
|
# Insert after the after-element
|
|
if ref_idx < len(children):
|
|
children[ref_idx - 1].tail = "\n" + child_indent
|
|
children[ref_idx - 1].addnext(new_node)
|
|
new_node.tail = "\n" + child_indent
|
|
else:
|
|
# Append at end
|
|
if len(children) > 0:
|
|
children[-1].tail = "\n" + child_indent
|
|
container.append(new_node)
|
|
parent_indent = child_indent[:-1] if len(child_indent) > 1 else ""
|
|
new_node.tail = "\n" + parent_indent
|
|
else:
|
|
# Append at end
|
|
if len(children) > 0:
|
|
# Insert before trailing whitespace (append after last child)
|
|
children[-1].tail = "\n" + child_indent
|
|
container.append(new_node)
|
|
parent_indent = child_indent[:-1] if len(child_indent) > 1 else ""
|
|
new_node.tail = "\n" + parent_indent
|
|
else:
|
|
# Container is empty
|
|
container.text = "\n" + child_indent
|
|
container.append(new_node)
|
|
parent_indent = child_indent[:-1] if len(child_indent) > 1 else ""
|
|
new_node.tail = "\n" + parent_indent
|
|
|
|
|
|
# ── 9. Generate fragment, parse, import nodes ────────────────
|
|
|
|
def parse_fragment(xml_text):
|
|
frag_parser = etree.XMLParser(remove_blank_text=False)
|
|
frag_doc = etree.fromstring(xml_text.encode("utf-8"), frag_parser)
|
|
return frag_doc
|
|
|
|
|
|
def import_element_nodes(frag_root):
|
|
nodes = []
|
|
for child in frag_root:
|
|
if isinstance(child.tag, str):
|
|
nodes.append(child)
|
|
return nodes
|
|
|
|
|
|
# ── 10. Add elements ────────────────────────────────────────
|
|
|
|
added_elems = []
|
|
companion_count = 0
|
|
|
|
elements_list = defn.get("elements") or []
|
|
if elements_list:
|
|
# Resolve target container
|
|
target_ci = None
|
|
into_name = defn.get("into")
|
|
after_name = defn.get("after")
|
|
|
|
if into_name:
|
|
target_group = find_element(root_ci, into_name)
|
|
if target_group is None:
|
|
print(f"[ERROR] Target group '{into_name}' not found")
|
|
sys.exit(1)
|
|
target_ci = target_group.find("f:ChildItems", NS)
|
|
if target_ci is None:
|
|
# Create ChildItems for the group
|
|
target_ci = etree.SubElement(target_group, f"{{{FORM_NS}}}ChildItems")
|
|
elif after_name:
|
|
after_elem = find_element(root_ci, after_name)
|
|
if after_elem is None:
|
|
print(f"[ERROR] Element '{after_name}' not found")
|
|
sys.exit(1)
|
|
target_ci = after_elem.getparent()
|
|
else:
|
|
target_ci = root_ci
|
|
|
|
if target_ci is None:
|
|
# Create ChildItems section in form — insert after Events or AutoCommandBar
|
|
target_ci = etree.Element(f"{{{FORM_NS}}}ChildItems")
|
|
insert_after = root.find("f:Events", NS)
|
|
if insert_after is None:
|
|
insert_after = root.find("f:AutoCommandBar", NS)
|
|
if insert_after is not None:
|
|
idx = list(root).index(insert_after) + 1
|
|
root.insert(idx, target_ci)
|
|
else:
|
|
root.append(target_ci)
|
|
root_ci = target_ci
|
|
|
|
# Detect indent level
|
|
child_indent = get_child_indent(target_ci)
|
|
|
|
# Check for duplicate element names
|
|
for el in elements_list:
|
|
type_key = None
|
|
for key in ELEMENT_KEYS:
|
|
if key in el and el[key] is not None:
|
|
type_key = key
|
|
break
|
|
if type_key:
|
|
el_name = get_element_name(el, type_key)
|
|
existing = find_element(root_ci, el_name) if root_ci is not None else None
|
|
if existing is not None:
|
|
print(f"[WARN] Element '{el_name}' already exists in form (id={existing.get('id')})")
|
|
|
|
# Remember starting element ID for companion counting
|
|
start_elem_id = next_elem_id
|
|
|
|
# Generate fragment
|
|
xml_lines.clear()
|
|
X(f"<_F {ALL_NS_DECL}>")
|
|
for el in elements_list:
|
|
emit_element(el, child_indent)
|
|
X("</_F>")
|
|
|
|
frag_text = "\n".join(xml_lines)
|
|
frag_root = parse_fragment(frag_text)
|
|
imported_nodes = import_element_nodes(frag_root)
|
|
|
|
# Count actual elements for reporting
|
|
tag_map = {
|
|
"group": "Group", "input": "Input", "check": "Check", "label": "Label", "labelField": "LabelField",
|
|
"table": "Table", "pages": "Pages", "page": "Page", "button": "Button",
|
|
"picture": "Picture", "picField": "PicField", "calendar": "Calendar", "cmdBar": "CmdBar", "popup": "Popup",
|
|
}
|
|
for el in elements_list:
|
|
type_key = None
|
|
for key in ELEMENT_KEYS:
|
|
if key in el and el[key] is not None:
|
|
type_key = key
|
|
break
|
|
name = get_element_name(el, type_key)
|
|
path_str = f" -> {el['path']}" if el.get("path") else ""
|
|
on_list = el.get("on")
|
|
evt_str = f" {{{', '.join(str(e) for e in on_list)}}}" if on_list else ""
|
|
added_elems.append(f" + [{tag_map.get(type_key, type_key)}] {name}{path_str}{evt_str}")
|
|
|
|
# Insert each imported node
|
|
for node in imported_nodes:
|
|
insert_into_container(target_ci, node, after_name, child_indent)
|
|
after_name = node.get("name")
|
|
|
|
total_new_elem_ids = next_elem_id - start_elem_id
|
|
companion_count = total_new_elem_ids - len(elements_list)
|
|
|
|
# ── 11. Add attributes ──────────────────────────────────────
|
|
|
|
added_attrs = []
|
|
|
|
attrs_list = defn.get("attributes") or []
|
|
if attrs_list:
|
|
attrs_section = root.find("f:Attributes", NS)
|
|
if attrs_section is None:
|
|
attrs_section = etree.SubElement(root, f"{{{FORM_NS}}}Attributes")
|
|
|
|
attr_child_indent = get_child_indent(attrs_section)
|
|
if not attr_child_indent:
|
|
attr_child_indent = "\t\t"
|
|
|
|
# Generate attribute fragments
|
|
xml_lines.clear()
|
|
X(f"<_F {ALL_NS_DECL}>")
|
|
for attr in attrs_list:
|
|
attr_id = new_attr_id()
|
|
attr_name = str(attr["name"])
|
|
X(f'{attr_child_indent}<Attribute name="{attr_name}" id="{attr_id}">')
|
|
inner = attr_child_indent + "\t"
|
|
|
|
if attr.get("title"):
|
|
emit_mltext("Title", str(attr["title"]), inner)
|
|
if attr.get("type"):
|
|
emit_type(str(attr["type"]), inner)
|
|
else:
|
|
X(f"{inner}<Type/>")
|
|
if attr.get("main") is True:
|
|
X(f"{inner}<MainAttribute>true</MainAttribute>")
|
|
if attr.get("savedData") is True:
|
|
X(f"{inner}<SavedData>true</SavedData>")
|
|
if attr.get("fillChecking"):
|
|
X(f"{inner}<FillChecking>{attr['fillChecking']}</FillChecking>")
|
|
|
|
columns = attr.get("columns")
|
|
if columns and len(columns) > 0:
|
|
X(f"{inner}<Columns>")
|
|
col_id = 1
|
|
for col in columns:
|
|
X(f'{inner}\t<Column name="{col["name"]}" id="{col_id}">')
|
|
if col.get("title"):
|
|
emit_mltext("Title", str(col["title"]), inner + "\t\t")
|
|
emit_type(str(col["type"]), inner + "\t\t")
|
|
X(f'{inner}\t</Column>')
|
|
col_id += 1
|
|
X(f"{inner}</Columns>")
|
|
|
|
X(f"{attr_child_indent}</Attribute>")
|
|
type_str = str(attr["type"]) if attr.get("type") else "(no type)"
|
|
added_attrs.append(f" + {attr_name}: {type_str} (id={attr_id})")
|
|
X("</_F>")
|
|
|
|
frag_text = "\n".join(xml_lines)
|
|
frag_root = parse_fragment(frag_text)
|
|
imported_attrs = import_element_nodes(frag_root)
|
|
|
|
for node in imported_attrs:
|
|
insert_into_container(attrs_section, node, None, attr_child_indent)
|
|
|
|
# ── 12. Add commands ────────────────────────────────────────
|
|
|
|
added_cmds = []
|
|
|
|
cmds_list = defn.get("commands") or []
|
|
if cmds_list:
|
|
cmds_section = root.find("f:Commands", NS)
|
|
if cmds_section is None:
|
|
cmds_section = etree.SubElement(root, f"{{{FORM_NS}}}Commands")
|
|
|
|
cmd_child_indent = get_child_indent(cmds_section)
|
|
if not cmd_child_indent:
|
|
cmd_child_indent = "\t\t"
|
|
|
|
xml_lines.clear()
|
|
X(f"<_F {ALL_NS_DECL}>")
|
|
for cmd in cmds_list:
|
|
cmd_id = new_cmd_id()
|
|
cmd_name = str(cmd["name"])
|
|
X(f'{cmd_child_indent}<Command name="{cmd_name}" id="{cmd_id}">')
|
|
inner = cmd_child_indent + "\t"
|
|
|
|
if cmd.get("title"):
|
|
emit_mltext("Title", str(cmd["title"]), inner)
|
|
|
|
if cmd.get("actions"):
|
|
for act in cmd["actions"]:
|
|
act_handler = str(act["handler"])
|
|
call_type_attr = f' callType="{act["callType"]}"' if act.get("callType") else ""
|
|
X(f"{inner}<Action{call_type_attr}>{act_handler}</Action>")
|
|
elif cmd.get("action"):
|
|
call_type_attr = f' callType="{cmd["callType"]}"' if cmd.get("callType") else ""
|
|
X(f"{inner}<Action{call_type_attr}>{cmd['action']}</Action>")
|
|
|
|
if cmd.get("shortcut"):
|
|
X(f"{inner}<Shortcut>{cmd['shortcut']}</Shortcut>")
|
|
if cmd.get("picture"):
|
|
X(f"{inner}<Picture>")
|
|
X(f"{inner}\t<xr:Ref>{cmd['picture']}</xr:Ref>")
|
|
X(f"{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>")
|
|
X(f"{inner}</Picture>")
|
|
if cmd.get("representation"):
|
|
X(f"{inner}<Representation>{cmd['representation']}</Representation>")
|
|
|
|
X(f"{cmd_child_indent}</Command>")
|
|
action_str = ""
|
|
if cmd.get("action"):
|
|
action_str = f" -> {cmd['action']}"
|
|
elif cmd.get("actions"):
|
|
action_str = f" -> {len(cmd['actions'])} action(s)"
|
|
added_cmds.append(f" + {cmd_name}{action_str} (id={cmd_id})")
|
|
X("</_F>")
|
|
|
|
frag_text = "\n".join(xml_lines)
|
|
frag_root = parse_fragment(frag_text)
|
|
imported_cmds = import_element_nodes(frag_root)
|
|
|
|
for node in imported_cmds:
|
|
insert_into_container(cmds_section, node, None, cmd_child_indent)
|
|
|
|
# ── 12b. Add form-level events ──────────────────────────────
|
|
|
|
added_form_events = []
|
|
|
|
form_events_list = defn.get("formEvents") or []
|
|
if form_events_list:
|
|
events_section = root.find("f:Events", NS)
|
|
if events_section is None:
|
|
events_section = etree.Element(f"{{{FORM_NS}}}Events")
|
|
# Insert after AutoCommandBar (Events come after AutoCommandBar in 1C)
|
|
acb_node = root.find("f:AutoCommandBar", NS)
|
|
if acb_node is not None:
|
|
acb_idx = list(root).index(acb_node)
|
|
acb_node.tail = (acb_node.tail or "") + "\r\n\t"
|
|
root.insert(acb_idx + 1, events_section)
|
|
else:
|
|
root.append(events_section)
|
|
|
|
evt_child_indent = get_child_indent(events_section)
|
|
if not evt_child_indent:
|
|
evt_child_indent = "\t\t"
|
|
|
|
xml_lines.clear()
|
|
X(f"<_F {ALL_NS_DECL}>")
|
|
for fe in form_events_list:
|
|
fe_name = str(fe["name"])
|
|
fe_handler = str(fe["handler"])
|
|
call_type_attr = f' callType="{fe["callType"]}"' if fe.get("callType") else ""
|
|
X(f'{evt_child_indent}<Event name="{fe_name}"{call_type_attr}>{fe_handler}</Event>')
|
|
ct_str = f"[{fe['callType']}]" if fe.get("callType") else ""
|
|
added_form_events.append(f" + {fe_name}{ct_str} -> {fe_handler}")
|
|
X("</_F>")
|
|
|
|
frag_text = "\n".join(xml_lines)
|
|
frag_root = parse_fragment(frag_text)
|
|
imported_events = import_element_nodes(frag_root)
|
|
|
|
for node in imported_events:
|
|
insert_into_container(events_section, node, None, evt_child_indent)
|
|
|
|
# ── 12c. Add element-level events ───────────────────────────
|
|
|
|
added_elem_events = []
|
|
|
|
elem_events_list = defn.get("elementEvents") or []
|
|
if elem_events_list:
|
|
if root_ci is None:
|
|
root_ci = root.find("f:ChildItems", NS)
|
|
|
|
for ee in elem_events_list:
|
|
target_name = str(ee["element"])
|
|
target_el = find_element(root_ci, target_name)
|
|
if target_el is None:
|
|
print(f"[WARN] Element '{target_name}' not found -- skipping elementEvent")
|
|
continue
|
|
|
|
# Find or create Events element within the target
|
|
target_events = target_el.find("f:Events", NS)
|
|
if target_events is None:
|
|
target_events = etree.SubElement(target_el, f"{{{FORM_NS}}}Events")
|
|
|
|
ee_child_indent = get_child_indent(target_events)
|
|
if not ee_child_indent:
|
|
parent_indent = get_child_indent(target_el)
|
|
ee_child_indent = parent_indent + "\t"
|
|
|
|
ee_name = str(ee["name"])
|
|
ee_handler = str(ee["handler"])
|
|
call_type_attr = f' callType="{ee["callType"]}"' if ee.get("callType") else ""
|
|
|
|
xml_lines.clear()
|
|
X(f"<_F {ALL_NS_DECL}>")
|
|
X(f'{ee_child_indent}<Event name="{ee_name}"{call_type_attr}>{ee_handler}</Event>')
|
|
X("</_F>")
|
|
|
|
frag_text = "\n".join(xml_lines)
|
|
frag_root = parse_fragment(frag_text)
|
|
imported_ee = import_element_nodes(frag_root)
|
|
|
|
for node in imported_ee:
|
|
insert_into_container(target_events, node, None, ee_child_indent)
|
|
|
|
ct_str = f"[{ee['callType']}]" if ee.get("callType") else ""
|
|
added_elem_events.append(f" + {target_name}.{ee_name}{ct_str} -> {ee_handler}")
|
|
|
|
# ── 13. Save ────────────────────────────────────────────────
|
|
|
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
# Fix XML declaration quotes
|
|
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
if not xml_bytes.endswith(b"\n"):
|
|
xml_bytes += b"\n"
|
|
# Write with BOM
|
|
with open(resolved_form_path, "wb") as f:
|
|
f.write(b'\xef\xbb\xbf')
|
|
f.write(xml_bytes)
|
|
|
|
# ── 14. Summary ─────────────────────────────────────────────
|
|
|
|
if is_extension:
|
|
print("[EXTENSION] BaseForm detected -- IDs start at 1000000+")
|
|
print()
|
|
|
|
if added_form_events:
|
|
print("Added form events:")
|
|
for line in added_form_events:
|
|
print(line)
|
|
print()
|
|
|
|
if added_elem_events:
|
|
print("Added element events:")
|
|
for line in added_elem_events:
|
|
print(line)
|
|
print()
|
|
|
|
if added_elems:
|
|
pos_str = ""
|
|
if defn.get("into"):
|
|
pos_str += f"into {defn['into']}"
|
|
if defn.get("after"):
|
|
if pos_str:
|
|
pos_str += ", "
|
|
pos_str += f"after {defn['after']}"
|
|
if pos_str:
|
|
pos_str = f" ({pos_str})"
|
|
print(f"Added elements{pos_str}:")
|
|
for line in added_elems:
|
|
print(line)
|
|
print()
|
|
|
|
if added_attrs:
|
|
print("Added attributes:")
|
|
for line in added_attrs:
|
|
print(line)
|
|
print()
|
|
|
|
if added_cmds:
|
|
print("Added commands:")
|
|
for line in added_cmds:
|
|
print(line)
|
|
print()
|
|
|
|
print("---")
|
|
total_parts = []
|
|
if added_form_events:
|
|
total_parts.append(f"{len(added_form_events)} form event(s)")
|
|
if added_elem_events:
|
|
total_parts.append(f"{len(added_elem_events)} element event(s)")
|
|
if added_elems:
|
|
comp_str = f" (+{companion_count} companions)" if companion_count > 0 else ""
|
|
total_parts.append(f"{len(added_elems)} element(s){comp_str}")
|
|
if added_attrs:
|
|
total_parts.append(f"{len(added_attrs)} attribute(s)")
|
|
if added_cmds:
|
|
total_parts.append(f"{len(added_cmds)} command(s)")
|
|
print(f"Total: {', '.join(total_parts)}")
|
|
print("Run /form-validate to verify.")
|