Files
cc-1c-skills/.github/skills/skd-edit/scripts/skd-edit.py
T
2026-05-07 18:27:38 +00:00

2478 lines
102 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# skd-edit v1.11 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
import uuid
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
VALID_OPS = [
"add-field", "add-total", "add-calculated-field", "add-parameter", "add-filter",
"add-dataParameter", "add-order", "add-selection", "add-dataSetLink",
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
"set-query", "patch-query", "set-outputParameter", "set-structure",
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter",
"rename-parameter", "reorder-parameters",
"clear-selection", "clear-order", "clear-filter",
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
]
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-TemplatePath", "-Path", required=True)
parser.add_argument("-Operation", required=True, choices=VALID_OPS)
parser.add_argument("-Value", required=True)
parser.add_argument("-DataSet", default="")
parser.add_argument("-Variant", default="")
parser.add_argument("-NoSelection", action="store_true")
args = parser.parse_args()
template_path = args.TemplatePath
operation = args.Operation
value_arg = args.Value
data_set_arg = args.DataSet
variant_arg = args.Variant
no_selection = args.NoSelection
# ── namespaces ───────────────────────────────────────────────
SCH_NS = "http://v8.1c.ru/8.1/data-composition-system/schema"
SET_NS = "http://v8.1c.ru/8.1/data-composition-system/settings"
COR_NS = "http://v8.1c.ru/8.1/data-composition-system/core"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
NS_MAP = {
"sch": SCH_NS,
"dcsset": SET_NS,
"dcscor": COR_NS,
"xsi": XSI_NS,
"v8": V8_NS,
}
WRAPPER_NS = (
f'xmlns="{SCH_NS}"'
f' xmlns:xsi="{XSI_NS}"'
f' xmlns:v8="{V8_NS}"'
' xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"'
f' xmlns:dcscor="{COR_NS}"'
f' xmlns:dcsset="{SET_NS}"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
)
XSI_TYPE = f"{{{XSI_NS}}}type"
def local_name(node):
return etree.QName(node.tag).localname
# ── helpers ──────────────────────────────────────────────────
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def resolve_query_value(val, base_dir):
if not val.startswith("@"):
return val
file_path = val[1:]
if os.path.isabs(file_path):
candidates = [file_path]
else:
candidates = [
os.path.join(base_dir, file_path),
os.path.join(os.getcwd(), file_path),
]
for c in candidates:
if os.path.exists(c):
with open(c, 'r', encoding='utf-8-sig') as f:
return f.read().rstrip()
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
sys.exit(1)
def new_uuid():
return str(uuid.uuid4())
# ── 1. Resolve path ─────────────────────────────────────────
if not template_path.endswith(".xml"):
candidate = os.path.join(template_path, "Ext", "Template.xml")
if os.path.exists(candidate):
template_path = candidate
if not os.path.exists(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(template_path)
query_base_dir = os.path.dirname(resolved_path)
# ── 2. Type system ──────────────────────────────────────────
type_synonyms = {
"\u0447\u0438\u0441\u043b\u043e": "decimal",
"\u0441\u0442\u0440\u043e\u043a\u0430": "string",
"\u0431\u0443\u043b\u0435\u0432\u043e": "boolean",
"\u0434\u0430\u0442\u0430": "date",
"\u0434\u0430\u0442\u0430\u0432\u0440\u0435\u043c\u044f": "dateTime",
"\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439\u043f\u0435\u0440\u0438\u043e\u0434": "StandardPeriod",
"bool": "boolean",
"str": "string",
"int": "decimal",
"integer": "decimal",
"number": "decimal",
"num": "decimal",
"\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "CatalogRef",
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0441\u0441\u044b\u043b\u043a\u0430": "DocumentRef",
"\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435\u0441\u0441\u044b\u043b\u043a\u0430": "EnumRef",
"\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfAccountsRef",
"\u043f\u043b\u0430\u043d\u0432\u0438\u0434\u043e\u0432\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfCharacteristicTypesRef",
}
output_param_types = {
"\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "mltext",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "dcsset:DataCompositionTextOutputType",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0414\u0430\u043d\u043d\u044b\u0445": "dcsset:DataCompositionTextOutputType",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041e\u0442\u0431\u043e\u0440": "dcsset:DataCompositionTextOutputType",
"\u041c\u0430\u043a\u0435\u0442\u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f": "xs:string",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041f\u043e\u043b\u0435\u0439\u0413\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438": "dcsset:DataCompositionGroupFieldsPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432": "dcsset:DataCompositionAttributesPlacement",
"\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
"\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
}
def resolve_type_str(type_str):
if not type_str:
return type_str
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base_name = m.group(1).strip()
params = m.group(2)
resolved = type_synonyms.get(base_name.lower())
if resolved:
return f"{resolved}({params})"
return type_str
if "." in type_str:
dot_idx = type_str.index(".")
prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:]
resolved = type_synonyms.get(prefix.lower())
if resolved:
return f"{resolved}{suffix}"
return type_str
resolved = type_synonyms.get(type_str.lower())
if resolved:
return resolved
return type_str
# ── 3. Parsers ──────────────────────────────────────────────
def parse_field_shorthand(s):
result = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": []}
m = re.search(r'\[([^\]]+)\]', s)
if m:
result["title"] = m.group(1)
s = re.sub(r'\s*\[[^\]]+\]', '', s)
role_matches = re.findall(r'@(\w+)', s)
result["roles"] = role_matches
s = re.sub(r'\s*@\w+', '', s)
restrict_matches = re.findall(r'#(\w+)', s)
result["restrict"] = restrict_matches
s = re.sub(r'\s*#\w+', '', s)
s = s.strip()
if ":" in s:
parts = s.split(":", 1)
result["dataPath"] = parts[0].strip()
result["type"] = resolve_type_str(parts[1].strip())
else:
result["dataPath"] = s
result["field"] = result["dataPath"]
return result
def read_field_properties(field_el):
props = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": [], "_rawTypeText": ""}
for ch in field_el:
if not isinstance(ch.tag, str):
continue
ln = local_name(ch)
if ln == "dataPath":
props["dataPath"] = (ch.text or "").strip()
elif ln == "field":
props["field"] = (ch.text or "").strip()
elif ln == "title":
for item in ch:
if isinstance(item.tag, str) and local_name(item) == "item":
for gc in item:
if isinstance(gc.tag, str) and local_name(gc) == "content":
props["title"] = (gc.text or "").strip()
elif ln == "valueType":
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "Type":
props["_rawTypeText"] = (gc.text or "").strip()
break
elif ln == "role":
for gc in ch:
if isinstance(gc.tag, str):
gcn = local_name(gc)
if gcn == "periodNumber":
props["roles"].append("period")
elif (gc.text or "").strip() == "true":
props["roles"].append(gcn)
elif ln == "useRestriction":
rev_map = {"field": "noField", "condition": "noFilter", "group": "noGroup", "order": "noOrder"}
for gc in ch:
if isinstance(gc.tag, str) and (gc.text or "").strip() == "true":
mapped = rev_map.get(local_name(gc))
if mapped:
props["restrict"].append(mapped)
return props
def parse_total_shorthand(s):
parts = s.split(":", 1)
data_path = parts[0].strip()
func_part = parts[1].strip()
if re.match(r'^\w+\(', func_part):
return {"dataPath": data_path, "expression": func_part}
else:
return {"dataPath": data_path, "expression": f"{func_part}({data_path})"}
def parse_calc_shorthand(s):
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
restrict_pattern = r'#(noField|noFilter|noCondition|noGroup|noOrder)\b'
restrict_matches = re.findall(restrict_pattern, s)
s = re.sub(r'\s*' + restrict_pattern, '', s)
eq_idx = s.find("=")
if eq_idx > 0:
lhs = s[:eq_idx]
rhs = s[eq_idx + 1:].strip()
has_rhs = True
else:
lhs = s
rhs = ""
has_rhs = False
title = ""
m = re.search(r'\[([^\]]+)\]', lhs)
if m:
title = m.group(1)
lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
lhs = lhs.strip()
if has_rhs:
if ":" in lhs:
colon_idx = lhs.index(":")
data_path = lhs[:colon_idx].strip()
type_str = resolve_type_str(lhs[colon_idx + 1:].strip())
return {"dataPath": data_path, "expression": rhs, "type": type_str, "title": title, "restrict": restrict_matches}
return {"dataPath": lhs, "expression": rhs, "type": "", "title": title, "restrict": restrict_matches}
return {"dataPath": lhs, "expression": "", "type": "", "title": title, "restrict": restrict_matches}
def parse_param_shorthand(s):
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None}
if re.search(r'@autoDates', s):
result["autoDates"] = True
s = re.sub(r'\s*@autoDates', '', s)
# Extract optional [Title] (mirrors parse_field_shorthand)
m = re.search(r'\[([^\]]*)\]', s)
if m:
result["title"] = m.group(1).strip()
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
if m:
result["name"] = m.group(1).strip()
result["type"] = resolve_type_str(m.group(2).strip())
if m.group(4):
result["value"] = m.group(4).strip()
else:
result["name"] = s.strip()
return result
def parse_filter_shorthand(s):
# use is tristate: None = not specified (modify-* won't touch),
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
result = {"field": "", "op": "Equal", "value": None, "use": None, "userSettingID": None, "viewMode": None}
if re.search(r'@user', s):
result["userSettingID"] = "auto"
s = re.sub(r'\s*@user', '', s)
if re.search(r'@off', s):
result["use"] = False
s = re.sub(r'\s*@off', '', s)
if re.search(r'@on\b', s):
result["use"] = True
s = re.sub(r'\s*@on\b', '', s)
if re.search(r'@quickAccess', s):
result["viewMode"] = "QuickAccess"
s = re.sub(r'\s*@quickAccess', '', s)
if re.search(r'@normal', s):
result["viewMode"] = "Normal"
s = re.sub(r'\s*@normal', '', s)
if re.search(r'@inaccessible', s):
result["viewMode"] = "Inaccessible"
s = re.sub(r'\s*@inaccessible', '', s)
s = s.strip()
op_patterns = [r'<>', r'>=', r'<=', r'=', r'>', r'<',
r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b',
r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b',
r'notFilled\b', r'filled\b']
op_joined = "|".join(op_patterns)
m = re.match(rf'^(.+?)\s+({op_joined})\s*(.*)?$', s)
if m:
result["field"] = m.group(1).strip()
op_raw = m.group(2).strip()
val_part = (m.group(3) or "").strip()
op_map = {
"=": "Equal", "<>": "NotEqual", ">": "Greater", ">=": "GreaterOrEqual",
"<": "Less", "<=": "LessOrEqual", "in": "InList", "notIn": "NotInList",
"inHierarchy": "InHierarchy", "inListByHierarchy": "InListByHierarchy",
"contains": "Contains", "notContains": "NotContains",
"beginsWith": "BeginsWith", "notBeginsWith": "NotBeginsWith",
"filled": "Filled", "notFilled": "NotFilled",
}
result["op"] = op_map.get(op_raw, op_raw)
if val_part and val_part != "_":
if val_part in ("true", "false"):
result["value"] = val_part
result["valueType"] = "xs:boolean"
elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part):
result["value"] = val_part
result["valueType"] = "xs:dateTime"
elif re.match(r'^\d+(\.\d+)?$', val_part):
result["value"] = val_part
result["valueType"] = "xs:decimal"
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
result["value"] = val_part
result["valueType"] = "dcscor:DesignTimeValue"
else:
result["value"] = val_part
result["valueType"] = "xs:string"
else:
result["field"] = s
return result
def parse_data_param_shorthand(s):
# use is tristate: None = not specified (modify-* won't touch),
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
result = {"parameter": "", "value": None, "use": None, "userSettingID": None, "viewMode": None}
if re.search(r'@user', s):
result["userSettingID"] = "auto"
s = re.sub(r'\s*@user', '', s)
if re.search(r'@off', s):
result["use"] = False
s = re.sub(r'\s*@off', '', s)
if re.search(r'@on\b', s):
result["use"] = True
s = re.sub(r'\s*@on\b', '', s)
if re.search(r'@quickAccess', s):
result["viewMode"] = "QuickAccess"
s = re.sub(r'\s*@quickAccess', '', s)
if re.search(r'@normal', s):
result["viewMode"] = "Normal"
s = re.sub(r'\s*@normal', '', s)
s = s.strip()
m = re.match(r'^([^=]+)=\s*(.+)$', s)
if m:
result["parameter"] = m.group(1).strip()
val_str = m.group(2).strip()
period_variants = [
"Custom", "Today", "ThisWeek", "ThisTenDays", "ThisMonth", "ThisQuarter", "ThisHalfYear", "ThisYear",
"FromBeginningOfThisWeek", "FromBeginningOfThisTenDays", "FromBeginningOfThisMonth",
"FromBeginningOfThisQuarter", "FromBeginningOfThisHalfYear", "FromBeginningOfThisYear",
"LastWeek", "LastTenDays", "LastMonth", "LastQuarter", "LastHalfYear", "LastYear",
"NextDay", "NextWeek", "NextTenDays", "NextMonth", "NextQuarter", "NextHalfYear", "NextYear",
"TillEndOfThisWeek", "TillEndOfThisTenDays", "TillEndOfThisMonth",
"TillEndOfThisQuarter", "TillEndOfThisHalfYear", "TillEndOfThisYear",
]
if val_str in period_variants:
result["value"] = {"variant": val_str}
else:
result["value"] = val_str
else:
result["parameter"] = s
return result
def parse_order_shorthand(s):
s = s.strip()
if s == "Auto":
return {"field": "Auto", "direction": ""}
parts = s.split(None, 1)
field = parts[0]
direction = "Asc"
if len(parts) > 1 and re.match(r'^desc$', parts[1], re.IGNORECASE):
direction = "Desc"
return {"field": field, "direction": direction}
def parse_data_set_link_shorthand(s):
result = {"source": "", "dest": "", "sourceExpr": "", "destExpr": "", "parameter": ""}
m = re.search(r'\[param\s+([^\]]+)\]', s)
if m:
result["parameter"] = m.group(1).strip()
s = re.sub(r'\s*\[param\s+[^\]]+\]', '', s)
m = re.match(r'^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$', s)
if m:
result["source"] = m.group(1).strip()
result["dest"] = m.group(2).strip()
result["sourceExpr"] = m.group(3).strip()
result["destExpr"] = m.group(4).strip()
else:
print(f"Invalid dataSetLink shorthand: {s}. Expected: 'Source > Dest on FieldA = FieldB [param Name]'", file=sys.stderr)
sys.exit(1)
return result
def parse_data_set_shorthand(s):
s = s.strip()
m = re.match(r'^(\S+):\s(.+)$', s)
if m:
return {"name": m.group(1), "query": m.group(2)}
return {"name": "", "query": s}
def parse_variant_shorthand(s):
presentation = ""
m = re.search(r'\[([^\]]+)\]', s)
if m:
presentation = m.group(1)
s = re.sub(r'\s*\[[^\]]+\]', '', s)
name = s.strip()
if not presentation:
presentation = name
return {"name": name, "presentation": presentation}
def parse_conditional_appearance_shorthand(s):
result = {"param": "", "value": "", "filter": None, "fields": []}
when_idx = s.find(" when ")
for_idx = s.find(" for ")
main_end = len(s)
if when_idx >= 0 and for_idx >= 0:
main_end = min(when_idx, for_idx)
elif when_idx >= 0:
main_end = when_idx
elif for_idx >= 0:
main_end = for_idx
if for_idx >= 0:
for_end = len(s)
if when_idx > for_idx:
for_end = when_idx
for_part = s[for_idx + 5:for_end].strip()
result["fields"] = [f.strip() for f in for_part.split(",") if f.strip()]
if when_idx >= 0:
when_end = len(s)
if for_idx > when_idx:
when_end = for_idx
when_part = s[when_idx + 6:when_end].strip()
or_parts = re.split(r'\s+or\s+', when_part)
if len(or_parts) > 1:
result["filter"] = [parse_filter_shorthand(p.strip()) for p in or_parts]
else:
result["filter"] = parse_filter_shorthand(when_part)
main_part = s[:main_end].strip()
eq_idx = main_part.find("=")
if eq_idx > 0:
result["param"] = main_part[:eq_idx].strip()
result["value"] = main_part[eq_idx + 1:].strip()
else:
result["param"] = main_part
return result
def parse_structure_shorthand(s):
segments = [seg.strip() for seg in s.split(">")]
result = []
innermost = None
for i in range(len(segments) - 1, -1, -1):
seg = segments[i].strip()
group = {"type": "group"}
name_m = re.search(r'\s*@name=(.+)', seg)
if name_m:
group["name"] = name_m.group(1).strip()
seg = re.sub(r'\s*@name=.+', '', seg).strip()
if re.match(r'^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg, re.IGNORECASE):
group["groupBy"] = []
else:
group["groupBy"] = [seg]
if innermost is not None:
group["children"] = [innermost]
innermost = group
if innermost:
result.append(innermost)
return result
def parse_output_param_shorthand(s):
idx = s.find("=")
if idx > 0:
return {"key": s[:idx].strip(), "value": s[idx + 1:].strip()}
return {"key": s.strip(), "value": ""}
# ── 4. Build-* functions (XML fragment generators) ──────────
def build_value_type_xml(type_str, indent):
if not type_str:
return ""
type_str = resolve_type_str(type_str)
lines = []
if type_str == "boolean":
lines.append(f"{indent}<v8:Type>xs:boolean</v8:Type>")
return "\r\n".join(lines)
m = re.match(r'^string(\((\d+)\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else "0"
lines.append(f"{indent}<v8:Type>xs:string</v8:Type>")
lines.append(f"{indent}<v8:StringQualifiers>")
lines.append(f"{indent}\t<v8:Length>{length}</v8:Length>")
lines.append(f"{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>")
lines.append(f"{indent}</v8:StringQualifiers>")
return "\r\n".join(lines)
m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
if m:
digits, fraction = m.group(1), m.group(2)
sign = "Nonnegative" if m.group(3) else "Any"
lines.append(f"{indent}<v8:Type>xs:decimal</v8:Type>")
lines.append(f"{indent}<v8:NumberQualifiers>")
lines.append(f"{indent}\t<v8:Digits>{digits}</v8:Digits>")
lines.append(f"{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>")
lines.append(f"{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>")
lines.append(f"{indent}</v8:NumberQualifiers>")
return "\r\n".join(lines)
m = re.match(r'^(date|dateTime)$', type_str)
if m:
fractions = "Date" if type_str == "date" else "DateTime"
lines.append(f"{indent}<v8:Type>xs:dateTime</v8:Type>")
lines.append(f"{indent}<v8:DateQualifiers>")
lines.append(f"{indent}\t<v8:DateFractions>{fractions}</v8:DateFractions>")
lines.append(f"{indent}</v8:DateQualifiers>")
return "\r\n".join(lines)
if type_str == "StandardPeriod":
lines.append(f"{indent}<v8:Type>v8:StandardPeriod</v8:Type>")
return "\r\n".join(lines)
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.', type_str):
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return "\r\n".join(lines)
if "." in type_str:
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return "\r\n".join(lines)
lines.append(f"{indent}<v8:Type>{esc_xml(type_str)}</v8:Type>")
return "\r\n".join(lines)
def build_mltext_xml(tag, text, indent):
lines = [
f'{indent}<{tag} xsi:type="v8:LocalStringType">',
f"{indent}\t<v8:item>",
f"{indent}\t\t<v8:lang>ru</v8:lang>",
f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>",
f"{indent}\t</v8:item>",
f"{indent}</{tag}>",
]
return "\r\n".join(lines)
def build_role_xml(roles, indent):
if not roles:
return ""
lines = [f"{indent}<role>"]
for role in roles:
if role == "period":
lines.append(f"{indent}\t<dcscom:periodNumber>1</dcscom:periodNumber>")
lines.append(f"{indent}\t<dcscom:periodType>Main</dcscom:periodType>")
else:
lines.append(f"{indent}\t<dcscom:{role}>true</dcscom:{role}>")
lines.append(f"{indent}</role>")
return "\r\n".join(lines)
def build_restriction_xml(restrict, indent):
if not restrict:
return ""
restrict_map = {"noField": "field", "noFilter": "condition", "noCondition": "condition", "noGroup": "group", "noOrder": "order"}
lines = [f"{indent}<useRestriction>"]
for r in restrict:
xml_name = restrict_map.get(r)
if xml_name:
lines.append(f"{indent}\t<{xml_name}>true</{xml_name}>")
lines.append(f"{indent}</useRestriction>")
return "\r\n".join(lines)
def build_field_fragment(parsed, indent):
i = indent
lines = [f'{i}<field xsi:type="DataSetFieldField">']
lines.append(f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>")
lines.append(f"{i}\t<field>{esc_xml(parsed['field'])}</field>")
if parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("restrict"):
lines.append(build_restriction_xml(parsed["restrict"], f"{i}\t"))
role_xml = build_role_xml(parsed.get("roles"), f"{i}\t")
if role_xml:
lines.append(role_xml)
if parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
lines.append(f"{i}\t</valueType>")
lines.append(f"{i}</field>")
return "\r\n".join(lines)
def build_total_fragment(parsed, indent):
i = indent
lines = [
f"{i}<totalField>",
f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>",
f"{i}\t<expression>{esc_xml(parsed['expression'])}</expression>",
f"{i}</totalField>",
]
return "\r\n".join(lines)
def build_calc_field_fragment(parsed, indent):
i = indent
lines = [
f"{i}<calculatedField>",
f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>",
f"{i}\t<expression>{esc_xml(parsed['expression'])}</expression>",
]
if parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("restrict"):
lines.append(build_restriction_xml(parsed["restrict"], f"{i}\t"))
if parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
lines.append(f"{i}\t</valueType>")
lines.append(f"{i}</calculatedField>")
return "\r\n".join(lines)
def build_param_fragment(parsed, indent):
i = indent
fragments = []
lines = [f"{i}<parameter>", f"{i}\t<name>{esc_xml(parsed['name'])}</name>"]
if parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
lines.append(f"{i}\t</valueType>")
if parsed["value"] is not None:
val_str = str(parsed["value"])
if parsed.get("type") == "StandardPeriod":
lines.append(f'{i}\t<value xsi:type="v8:StandardPeriod">')
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
lines.append(f"{i}\t</value>")
elif parsed.get("type", "").startswith("date"):
lines.append(f'{i}\t<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
elif parsed.get("type") == "boolean":
lines.append(f'{i}\t<value xsi:type="xs:boolean">{esc_xml(val_str)}</value>')
elif parsed.get("type", "").startswith("decimal"):
lines.append(f'{i}\t<value xsi:type="xs:decimal">{esc_xml(val_str)}</value>')
else:
lines.append(f'{i}\t<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
lines.append(f"{i}</parameter>")
fragments.append("\r\n".join(lines))
if parsed.get("autoDates"):
param_name = parsed["name"]
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
b_lines = [
f"{i}<parameter>",
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430</name>",
build_mltext_xml("title", "\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
f"{i}\t<valueType>",
build_value_type_xml("date", f"{i}\t\t"),
f"{i}\t</valueType>",
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
f"{i}\t<useRestriction>true</useRestriction>",
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}</expression>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(b_lines))
e_lines = [
f"{i}<parameter>",
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f</name>",
build_mltext_xml("title", "\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
f"{i}\t<valueType>",
build_value_type_xml("date", f"{i}\t\t"),
f"{i}\t</valueType>",
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
f"{i}\t<useRestriction>true</useRestriction>",
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}</expression>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(e_lines))
return fragments
def build_filter_item_fragment(parsed, indent):
i = indent
lines = [f'{i}<dcsset:item xsi:type="dcsset:FilterItemComparison">']
if parsed.get("use") is False:
lines.append(f"{i}\t<dcsset:use>false</dcsset:use>")
lines.append(f'{i}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(parsed["field"])}</dcsset:left>')
lines.append(f"{i}\t<dcsset:comparisonType>{esc_xml(parsed['op'])}</dcsset:comparisonType>")
if parsed.get("value") is not None:
vt = parsed.get("valueType", "xs:string")
lines.append(f'{i}\t<dcsset:right xsi:type="{vt}">{esc_xml(str(parsed["value"]))}</dcsset:right>')
if parsed.get("viewMode"):
lines.append(f"{i}\t<dcsset:viewMode>{esc_xml(parsed['viewMode'])}</dcsset:viewMode>")
if parsed.get("userSettingID"):
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
def build_selection_item_fragment(field_name, indent):
i = indent
if field_name == "Auto":
return f'{i}<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>'
m = re.match(r'^Folder\((.+)\)$', field_name)
if m:
inner = m.group(1)
colon_idx = inner.find(':')
if colon_idx > 0:
title = inner[:colon_idx].strip()
items = [x.strip() for x in inner[colon_idx + 1:].split(',') if x.strip()]
else:
title = ""
items = [x.strip() for x in inner.split(',') if x.strip()]
lines = [f'{i}<dcsset:item xsi:type="dcsset:SelectedItemFolder">']
if title:
lines.append(f"{i}\t<dcsset:lwsTitle>")
lines.append(f"{i}\t\t<v8:item>")
lines.append(f"{i}\t\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{i}\t\t\t<v8:content>{esc_xml(title)}</v8:content>")
lines.append(f"{i}\t\t</v8:item>")
lines.append(f"{i}\t</dcsset:lwsTitle>")
for item in items:
lines.append(f'{i}\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f"{i}\t\t<dcsset:field>{esc_xml(item)}</dcsset:field>")
lines.append(f"{i}\t</dcsset:item>")
lines.append(f"{i}\t<dcsset:placement>Auto</dcsset:placement>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
lines = [
f'{i}<dcsset:item xsi:type="dcsset:SelectedItemField">',
f"{i}\t<dcsset:field>{esc_xml(field_name)}</dcsset:field>",
f"{i}</dcsset:item>",
]
return "\r\n".join(lines)
def build_data_param_fragment(parsed, indent):
i = indent
lines = [f'{i}<dcscor:item xsi:type="dcsset:SettingsParameterValue">']
if parsed.get("use") is False:
lines.append(f"{i}\t<dcscor:use>false</dcscor:use>")
lines.append(f"{i}\t<dcscor:parameter>{esc_xml(parsed['parameter'])}</dcscor:parameter>")
if parsed.get("value") is not None:
val = parsed["value"]
if isinstance(val, dict) and val.get("variant"):
lines.append(f'{i}\t<dcscor:value xsi:type="v8:StandardPeriod">')
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val["variant"])}</v8:variant>')
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
lines.append(f"{i}\t</dcscor:value>")
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
lines.append(f'{i}\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
elif str(val) in ("true", "false"):
lines.append(f'{i}\t<dcscor:value xsi:type="xs:boolean">{esc_xml(str(val))}</dcscor:value>')
else:
lines.append(f'{i}\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
if parsed.get("viewMode"):
lines.append(f"{i}\t<dcsset:viewMode>{esc_xml(parsed['viewMode'])}</dcsset:viewMode>")
if parsed.get("userSettingID"):
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
lines.append(f"{i}</dcscor:item>")
return "\r\n".join(lines)
def build_order_item_fragment(parsed, indent):
i = indent
if parsed["field"] == "Auto":
return f'{i}<dcsset:item xsi:type="dcsset:OrderItemAuto"/>'
lines = [
f'{i}<dcsset:item xsi:type="dcsset:OrderItemField">',
f"{i}\t<dcsset:field>{esc_xml(parsed['field'])}</dcsset:field>",
f"{i}\t<dcsset:orderType>{parsed['direction']}</dcsset:orderType>",
f"{i}</dcsset:item>",
]
return "\r\n".join(lines)
def build_data_set_link_fragment(parsed, indent):
i = indent
lines = [
f"{i}<dataSetLink>",
f"{i}\t<sourceDataSet>{esc_xml(parsed['source'])}</sourceDataSet>",
f"{i}\t<destinationDataSet>{esc_xml(parsed['dest'])}</destinationDataSet>",
f"{i}\t<sourceExpression>{esc_xml(parsed['sourceExpr'])}</sourceExpression>",
f"{i}\t<destinationExpression>{esc_xml(parsed['destExpr'])}</destinationExpression>",
]
if parsed.get("parameter"):
lines.append(f"{i}\t<parameter>{esc_xml(parsed['parameter'])}</parameter>")
lines.append(f"{i}</dataSetLink>")
return "\r\n".join(lines)
def build_data_set_query_fragment(parsed, indent):
i = indent
lines = [
f'{i}<dataSet xsi:type="DataSetQuery">',
f"{i}\t<name>{esc_xml(parsed['name'])}</name>",
f"{i}\t<dataSource>{esc_xml(parsed['dataSource'])}</dataSource>",
f"{i}\t<query>{esc_xml(parsed['query'])}</query>",
f"{i}</dataSet>",
]
return "\r\n".join(lines)
def build_variant_fragment(parsed, indent):
i = indent
lines = [
f"{i}<settingsVariant>",
f"{i}\t<dcsset:name>{esc_xml(parsed['name'])}</dcsset:name>",
build_mltext_xml("dcsset:presentation", parsed["presentation"], f"{i}\t"),
f'{i}\t<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">',
f"{i}\t\t<dcsset:selection>",
f'{i}\t\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>',
f"{i}\t\t</dcsset:selection>",
f'{i}\t\t<dcsset:item xsi:type="dcsset:StructureItemGroup">',
f"{i}\t\t\t<dcsset:groupItems/>",
f"{i}\t\t\t<dcsset:order>",
f'{i}\t\t\t\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>',
f"{i}\t\t\t</dcsset:order>",
f"{i}\t\t\t<dcsset:selection>",
f'{i}\t\t\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>',
f"{i}\t\t\t</dcsset:selection>",
f"{i}\t\t</dcsset:item>",
f"{i}\t</dcsset:settings>",
f"{i}</settingsVariant>",
]
return "\r\n".join(lines)
def _emit_filter_comparison(lines, f, indent):
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">')
lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(f["field"])}</dcsset:left>')
lines.append(f"{indent}\t<dcsset:comparisonType>{esc_xml(f['op'])}</dcsset:comparisonType>")
if f.get("value") is not None:
vt = f.get("valueType", "xs:string")
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{esc_xml(str(f["value"]))}</dcsset:right>')
lines.append(f"{indent}</dcsset:item>")
def build_conditional_appearance_item_fragment(parsed, indent):
i = indent
lines = [f"{i}<dcsset:item>"]
if parsed.get("fields"):
lines.append(f"{i}\t<dcsset:selection>")
for fld in parsed["fields"]:
lines.append(f"{i}\t\t<dcsset:item>")
lines.append(f"{i}\t\t\t<dcsset:field>{esc_xml(fld)}</dcsset:field>")
lines.append(f"{i}\t\t</dcsset:item>")
lines.append(f"{i}\t</dcsset:selection>")
else:
lines.append(f"{i}\t<dcsset:selection/>")
if parsed.get("filter"):
flt = parsed["filter"]
lines.append(f"{i}\t<dcsset:filter>")
if isinstance(flt, list):
# OrGroup
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:FilterItemGroup">')
lines.append(f"{i}\t\t\t<dcsset:groupType>OrGroup</dcsset:groupType>")
for f in flt:
_emit_filter_comparison(lines, f, f"{i}\t\t\t")
lines.append(f"{i}\t\t</dcsset:item>")
else:
_emit_filter_comparison(lines, flt, f"{i}\t\t")
lines.append(f"{i}\t</dcsset:filter>")
else:
lines.append(f"{i}\t<dcsset:filter/>")
# appearance
lines.append(f"{i}\t<dcsset:appearance>")
val = parsed["value"]
lines.append(f'{i}\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f"{i}\t\t\t<dcscor:parameter>{esc_xml(parsed['param'])}</dcscor:parameter>")
if re.match(r'^(web|style|win):', val):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(val)}</dcscor:value>')
elif val in ("true", "false"):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:boolean">{esc_xml(val)}</dcscor:value>')
elif parsed["param"] in ("Формат", "Текст", "Заголовок"):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8:LocalStringType">')
lines.append(f"{i}\t\t\t\t<v8:item>")
lines.append(f"{i}\t\t\t\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{i}\t\t\t\t\t<v8:content>{esc_xml(val)}</v8:content>")
lines.append(f"{i}\t\t\t\t</v8:item>")
lines.append(f"{i}\t\t\t</dcscor:value>")
else:
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:string">{esc_xml(val)}</dcscor:value>')
lines.append(f"{i}\t\t</dcscor:item>")
lines.append(f"{i}\t</dcsset:appearance>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
def build_structure_item_fragment(item, indent):
i = indent
lines = [f'{i}<dcsset:item xsi:type="dcsset:StructureItemGroup">']
if item.get("name"):
lines.append(f"{i}\t<dcsset:name>{esc_xml(item['name'])}</dcsset:name>")
group_by = item.get("groupBy", [])
if not group_by:
lines.append(f"{i}\t<dcsset:groupItems/>")
else:
lines.append(f"{i}\t<dcsset:groupItems>")
for field in group_by:
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:GroupItemField">')
lines.append(f"{i}\t\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>")
lines.append(f"{i}\t\t\t<dcsset:groupType>Items</dcsset:groupType>")
lines.append(f"{i}\t\t\t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>")
lines.append(f'{i}\t\t\t<dcsset:periodAdditionBegin xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionBegin>')
lines.append(f'{i}\t\t\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>')
lines.append(f"{i}\t\t</dcsset:item>")
lines.append(f"{i}\t</dcsset:groupItems>")
lines.append(f"{i}\t<dcsset:order>")
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
lines.append(f"{i}\t</dcsset:order>")
lines.append(f"{i}\t<dcsset:selection>")
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>')
lines.append(f"{i}\t</dcsset:selection>")
if item.get("children"):
for child in item["children"]:
lines.append(build_structure_item_fragment(child, f"{i}\t"))
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
def build_output_param_fragment(parsed, indent):
i = indent
key = parsed["key"]
val = parsed["value"]
ptype = output_param_types.get(key, "xs:string")
lines = [f'{i}<dcscor:item xsi:type="dcsset:SettingsParameterValue">']
lines.append(f"{i}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>")
if ptype == "mltext":
lines.append(f'{i}\t<dcscor:value xsi:type="v8:LocalStringType">')
lines.append(f"{i}\t\t<v8:item>")
lines.append(f"{i}\t\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{i}\t\t\t<v8:content>{esc_xml(val)}</v8:content>")
lines.append(f"{i}\t\t</v8:item>")
lines.append(f"{i}\t</dcscor:value>")
else:
lines.append(f'{i}\t<dcscor:value xsi:type="{ptype}">{esc_xml(val)}</dcscor:value>')
lines.append(f"{i}</dcscor:item>")
return "\r\n".join(lines)
# ── 5. XML helpers ──────────────────────────────────────────
def import_fragment(doc_root, xml_string):
wrapper = f"<_W {WRAPPER_NS}>{xml_string}</_W>"
frag_parser = etree.XMLParser(remove_blank_text=False)
frag = etree.fromstring(wrapper.encode("utf-8"), frag_parser)
nodes = []
for child in frag:
if isinstance(child.tag, str):
nodes.append(child)
return nodes
def get_child_indent(container):
for i, child in enumerate(container):
txt = container.text if i == 0 else container[i - 1].tail
if txt:
m = re.search(r'\n(\t+)$', txt)
if m:
return m.group(1)
# Fallback: count depth
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)
def insert_before_element(container, new_node, ref_node, child_indent):
if ref_node is not None:
idx = list(container).index(ref_node)
if idx == 0:
prev_text = container.text or ""
container.text = prev_text.rstrip("\n\t") + "\n" + child_indent
container.insert(idx, new_node)
new_node.tail = "\n" + child_indent
else:
prev = container[idx - 1]
prev.tail = (prev.tail or "").rstrip("\n\t") + "\n" + child_indent
container.insert(idx, new_node)
new_node.tail = "\n" + child_indent
else:
# Append at end
children = list(container)
if children:
last = children[-1]
last.tail = (last.tail or "").rstrip("\n\t") + "\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.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
def clear_container_children(container):
to_remove = [ch for ch in container if isinstance(ch.tag, str)]
for el in to_remove:
remove_node_with_whitespace(el)
def remove_node_with_whitespace(node):
parent = node.getparent()
idx = list(parent).index(node)
# Remove the node and adjust whitespace
if idx > 0:
prev = parent[idx - 1]
# Preserve the tail of the removed node's predecessor
prev.tail = node.tail
elif idx == 0:
parent.text = node.tail
parent.remove(node)
def find_first_element(container, local_names, ns_uri=None):
for child in container:
if not isinstance(child.tag, str):
continue
ln = local_name(child)
if ln in local_names:
if not ns_uri or etree.QName(child.tag).namespace == ns_uri:
return child
return None
def find_last_element(container, ln_name, ns_uri=None):
last = None
for child in container:
if not isinstance(child.tag, str):
continue
if local_name(child) == ln_name:
if not ns_uri or etree.QName(child.tag).namespace == ns_uri:
last = child
return last
def find_element_by_child_value(container, elem_name, child_name, child_value, ns_uri=None):
for child in container:
if not isinstance(child.tag, str):
continue
if local_name(child) != elem_name:
continue
if ns_uri and etree.QName(child.tag).namespace != ns_uri:
continue
for gc in child:
if isinstance(gc.tag, str) and local_name(gc) == child_name and (gc.text or "").strip() == child_value:
return child
return None
def set_or_create_child_element(parent, ln, ns_uri, value, indent):
existing = None
for ch in parent:
if isinstance(ch.tag, str) and local_name(ch) == ln and etree.QName(ch.tag).namespace == ns_uri:
existing = ch
break
if existing is not None:
existing.text = value
else:
prefix = None
for p, uri in parent.nsmap.items():
if uri == ns_uri:
prefix = p
break
qual_name = f"{prefix}:{ln}" if prefix else ln
frag_xml = f"{indent}<{qual_name}>{esc_xml(value)}</{qual_name}>"
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(parent, node, None, indent)
def set_or_create_child_element_with_attr(parent, ln, ns_uri, value, xsi_type, indent):
existing = None
for ch in parent:
if isinstance(ch.tag, str) and local_name(ch) == ln and etree.QName(ch.tag).namespace == ns_uri:
existing = ch
break
if existing is not None:
existing.text = value
if xsi_type:
existing.set(XSI_TYPE, xsi_type)
else:
prefix = None
for p, uri in parent.nsmap.items():
if uri == ns_uri:
prefix = p
break
qual_name = f"{prefix}:{ln}" if prefix else ln
type_attr = f' xsi:type="{xsi_type}"' if xsi_type else ""
frag_xml = f"{indent}<{qual_name}{type_attr}>{esc_xml(value)}</{qual_name}>"
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(parent, node, None, indent)
def resolve_data_set():
root_el = xml_doc
if data_set_arg:
for child in root_el:
if isinstance(child.tag, str) and local_name(child) == "dataSet" and etree.QName(child.tag).namespace == SCH_NS:
for gc in child:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
if gc.text == data_set_arg:
return child
print(f"DataSet '{data_set_arg}' not found", file=sys.stderr)
sys.exit(1)
for child in root_el:
if isinstance(child.tag, str) and local_name(child) == "dataSet" and etree.QName(child.tag).namespace == SCH_NS:
return child
print("No dataSet found in DCS", file=sys.stderr)
sys.exit(1)
def resolve_variant_settings():
root_el = xml_doc
sv = None
if variant_arg:
for child in root_el:
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
for gc in child:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS:
if gc.text == variant_arg:
sv = child
break
if sv:
break
if sv is None:
print(f"Variant '{variant_arg}' not found", file=sys.stderr)
sys.exit(1)
else:
for child in root_el:
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
sv = child
break
if sv is None:
print("No settingsVariant found in DCS", file=sys.stderr)
sys.exit(1)
for gc in sv:
if isinstance(gc.tag, str) and local_name(gc) == "settings" and etree.QName(gc.tag).namespace == SET_NS:
return gc
print("No <dcsset:settings> found in variant", file=sys.stderr)
sys.exit(1)
def ensure_settings_child(settings, child_name, after_siblings):
el = find_first_element(settings, [child_name], SET_NS)
if el is not None:
return el
indent = get_child_indent(settings)
frag_xml = f"{indent}<dcsset:{child_name}/>"
nodes = import_fragment(xml_doc, frag_xml)
ref_node = None
for sib_name in after_siblings:
sib = find_first_element(settings, [sib_name], SET_NS)
if sib is not None:
# Get next element sibling
found = False
for ch in settings:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is sib:
found = True
break
for node in nodes:
insert_before_element(settings, node, ref_node, indent)
return find_first_element(settings, [child_name], SET_NS)
def get_variant_name():
if variant_arg:
return variant_arg
root_el = xml_doc
for child in root_el:
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
for gc in child:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS:
return gc.text or "(unknown)"
return "(unknown)"
def get_data_set_name(ds_node):
for gc in ds_node:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
return gc.text or "(unknown)"
return "(unknown)"
def get_container_child_indent(container):
has_elements = any(isinstance(ch.tag, str) for ch in container)
if has_elements:
return get_child_indent(container)
else:
parent_indent = get_child_indent(container.getparent())
return parent_indent + "\t"
# ── 6. Load XML ─────────────────────────────────────────────
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_doc = tree.getroot()
# ── 7. Batch value splitting ────────────────────────────────
if operation in ("set-query", "set-structure", "add-dataSet"):
values = [value_arg]
elif operation == "patch-query":
values = [v for v in value_arg.split(";;") if v.strip()]
elif operation == "add-drilldown":
if ";;" in value_arg:
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
else:
values = [v.strip() for v in value_arg.split(",") if v.strip()]
else:
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
# ── 8. Main logic ───────────────────────────────────────────
if operation == "add-field":
ds_node = resolve_data_set()
ds_name = get_data_set_name(ds_node)
for val in values:
parsed = parse_field_shorthand(val)
child_indent = get_child_indent(ds_node)
existing = find_element_by_child_value(ds_node, "field", "dataPath", parsed["dataPath"], SCH_NS)
if existing is not None:
print(f'[WARN] Field "{parsed["dataPath"]}" already exists in dataset "{ds_name}" -- skipped')
continue
frag_xml = build_field_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
ref_node = find_first_element(ds_node, ["dataSource"], SCH_NS)
for node in nodes:
insert_before_element(ds_node, node, ref_node, child_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to dataset "{ds_name}"')
if not no_selection:
settings = resolve_variant_settings()
var_name = get_variant_name()
selection = ensure_settings_child(settings, "selection", [])
existing_sel = find_element_by_child_value(selection, "item", "field", parsed["dataPath"], SET_NS)
if existing_sel is not None:
print(f'[INFO] Field "{parsed["dataPath"]}" already in selection -- skipped')
else:
sel_indent = get_container_child_indent(selection)
sel_xml = build_selection_item_fragment(parsed["dataPath"], sel_indent)
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
elif operation == "add-total":
for val in values:
parsed = parse_total_shorthand(val)
child_indent = get_child_indent(xml_doc)
existing = find_element_by_child_value(xml_doc, "totalField", "dataPath", parsed["dataPath"], SCH_NS)
if existing is not None:
print(f'[WARN] TotalField "{parsed["dataPath"]}" already exists -- skipped')
continue
frag_xml = build_total_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
last_total = find_last_element(xml_doc, "totalField", SCH_NS)
if last_total is not None:
# Insert after last totalField - find next element
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_total:
found = True
else:
ref_node = find_first_element(xml_doc, ["parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] TotalField "{parsed["dataPath"]}" = {parsed["expression"]} added')
elif operation == "add-calculated-field":
for val in values:
parsed = parse_calc_shorthand(val)
child_indent = get_child_indent(xml_doc)
existing = find_element_by_child_value(xml_doc, "calculatedField", "dataPath", parsed["dataPath"], SCH_NS)
if existing is not None:
print(f'[WARN] CalculatedField "{parsed["dataPath"]}" already exists -- skipped')
continue
frag_xml = build_calc_field_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
last_calc = find_last_element(xml_doc, "calculatedField", SCH_NS)
if last_calc is not None:
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_calc:
found = True
else:
ref_node = find_first_element(xml_doc, ["totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] CalculatedField "{parsed["dataPath"]}" = {parsed["expression"]} added')
if not no_selection:
settings = resolve_variant_settings()
var_name = get_variant_name()
selection = ensure_settings_child(settings, "selection", [])
existing_sel = find_element_by_child_value(selection, "item", "field", parsed["dataPath"], SET_NS)
if existing_sel is not None:
print(f'[INFO] Field "{parsed["dataPath"]}" already in selection -- skipped')
else:
sel_indent = get_container_child_indent(selection)
sel_xml = build_selection_item_fragment(parsed["dataPath"], sel_indent)
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
elif operation == "add-parameter":
for val in values:
parsed = parse_param_shorthand(val)
child_indent = get_child_indent(xml_doc)
existing = find_element_by_child_value(xml_doc, "parameter", "name", parsed["name"], SCH_NS)
if existing is not None:
print(f'[WARN] Parameter "{parsed["name"]}" already exists -- skipped')
continue
fragments = build_param_fragment(parsed, child_indent)
last_param = find_last_element(xml_doc, "parameter", SCH_NS)
if last_param is not None:
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_param:
found = True
else:
ref_node = find_first_element(xml_doc, ["template", "groupTemplate", "settingsVariant"], SCH_NS)
for frag_xml in fragments:
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] Parameter "{parsed["name"]}" added')
if parsed.get("autoDates"):
print('[OK] Auto-parameters "\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430", "\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f" added')
elif operation == "modify-parameter":
for val in values:
# Extract optional [Title] first (mirrors parse_field_shorthand)
title_val = None
m_title = re.search(r'\[([^\]]*)\]', val)
if m_title:
title_val = m_title.group(1).strip()
val = re.sub(r'\s*\[[^\]]*\]\s*', ' ', val).strip()
parts = val.split(None, 1)
param_name = parts[0].strip()
rest = parts[1].strip() if len(parts) > 1 else ""
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
if param_el is None:
print(f'[WARN] Parameter "{param_name}" not found -- skipped')
continue
child_indent = get_child_indent(param_el)
# Set/replace title (must come right after <name>, before <valueType>)
if title_val is not None:
existing_title = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "title"), None)
if existing_title is not None:
remove_node_with_whitespace(existing_title)
# Insert before the first child after <name>
title_ref = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) != "name"), None)
title_frag = build_mltext_xml("title", title_val, child_indent)
for node in import_fragment(xml_doc, title_frag):
insert_before_element(param_el, node, title_ref, child_indent)
print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
# Separate availableValue=... from simple kv pairs
simple_rest = rest
av_part = None
av_idx = rest.find("availableValue=")
if av_idx >= 0:
simple_rest = rest[:av_idx].strip()
av_part = rest[av_idx:]
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
if simple_rest:
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
key, value = m.group(1), m.group(2)
existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key), None)
if existing is not None:
existing.text = value
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
else:
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
ref_node = None
if key == "denyIncompleteValues":
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use"), None)
frag_xml = f"{child_indent}<{key}>{esc_xml(value)}</{key}>"
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": {key}={value} added')
# Process availableValue
if av_part:
av_rest = av_part[len("availableValue="):]
# Parse: "Перечисление...X presentation=текст с пробелами"
av_parts = re.split(r'\s+presentation=', av_rest, 1)
av_value = av_parts[0].strip()
av_presentation = av_parts[1].strip() if len(av_parts) > 1 else ""
av_type = "xs:string"
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_value):
av_type = "dcscor:DesignTimeValue"
av_lines = [f"{child_indent}<availableValue>"]
av_lines.append(f'{child_indent}\t<value xsi:type="{av_type}">{esc_xml(av_value)}</value>')
if av_presentation:
av_lines.append(f'{child_indent}\t<presentation xsi:type="v8:LocalStringType">')
av_lines.append(f"{child_indent}\t\t<v8:item>")
av_lines.append(f"{child_indent}\t\t\t<v8:lang>ru</v8:lang>")
av_lines.append(f"{child_indent}\t\t\t<v8:content>{esc_xml(av_presentation)}</v8:content>")
av_lines.append(f"{child_indent}\t\t</v8:item>")
av_lines.append(f"{child_indent}\t</presentation>")
av_lines.append(f"{child_indent}</availableValue>")
frag_xml = "\r\n".join(av_lines)
# Insert before first of (denyIncompleteValues, use) in document order
ref_node = None
for child in param_el:
if isinstance(child.tag, str) and local_name(child) in ("denyIncompleteValues", "use"):
ref_node = child
break
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": availableValue added')
elif operation == "rename-parameter":
root = xml_doc
for val in values:
m_rn = re.match(r'^\s*(.+?)\s*=>\s*(.+?)\s*$', val)
if not m_rn:
print(f'[WARN] rename-parameter expects "OldName => NewName", got: {val}')
continue
old_name = m_rn.group(1).strip()
new_name = m_rn.group(2).strip()
if old_name == new_name:
print('[WARN] rename-parameter: old and new names are equal -- skipped')
continue
# 1. Rename <parameter><name>OldName</name>
param_el = find_element_by_child_value(root, "parameter", "name", old_name, SCH_NS)
if param_el is None:
print(f'[WARN] Parameter "{old_name}" not found -- skipped')
continue
for ch in param_el:
if isinstance(ch.tag, str) and local_name(ch) == "name" and etree.QName(ch.tag).namespace == SCH_NS:
ch.text = new_name
break
# 2. Update <expression> in other <parameter> elements.
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
esc_old = re.escape(old_name)
expr_regex = re.compile(rf'&{esc_old}(?=[^\w\u0400-\u04FF]|$)')
expr_updated = 0
for ch in root:
if not (isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS):
continue
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "expression" and etree.QName(gc.tag).namespace == SCH_NS:
old_expr = gc.text or ""
new_expr = expr_regex.sub(f'&{new_name}', old_expr)
if new_expr != old_expr:
gc.text = new_expr
expr_updated += 1
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
dp_updated = 0
for variant_node in root:
if not (isinstance(variant_node.tag, str) and local_name(variant_node) == "settingsVariant" and etree.QName(variant_node.tag).namespace == SCH_NS):
continue
settings_node = find_first_element(variant_node, ["settings"], SET_NS)
if settings_node is None:
continue
dp_el = find_first_element(settings_node, ["dataParameters"], SET_NS)
if dp_el is None:
continue
for item in dp_el:
if not (isinstance(item.tag, str) and local_name(item) == "item"):
continue
for gc in item:
if isinstance(gc.tag, str) and local_name(gc) == "parameter" and etree.QName(gc.tag).namespace == COR_NS:
if (gc.text or "").strip() == old_name:
gc.text = new_name
dp_updated += 1
print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
elif operation == "reorder-parameters":
root = xml_doc
for val in values:
order = [s.strip() for s in val.split(",") if s.strip()]
if not order:
print('[WARN] reorder-parameters: empty list -- skipped')
continue
all_params = []
for ch in root:
if isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS:
all_params.append(ch)
if not all_params:
print('[WARN] reorder-parameters: no parameters in schema')
continue
child_indent = get_child_indent(root)
by_name = {}
for pe in all_params:
for gc in pe:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
by_name[(gc.text or "").strip()] = pe
break
new_order = []
used = set()
for name in order:
if name in by_name:
new_order.append(by_name[name])
used.add(name)
else:
print(f'[WARN] reorder-parameters: parameter "{name}" not found -- skipped')
for pe in all_params:
pe_name = None
for gc in pe:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
pe_name = (gc.text or "").strip()
break
if pe_name and pe_name not in used:
new_order.append(pe)
# Anchor: element right after the last parameter in original order
last_param = all_params[-1]
anchor = last_param.getnext()
# Remove all parameters with surrounding whitespace
for pe in all_params:
remove_node_with_whitespace(pe)
# Re-insert in new order before anchor
for pe in new_order:
insert_before_element(root, pe, anchor, child_indent)
print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
elif operation == "add-filter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_filter_shorthand(val)
filter_el = ensure_settings_child(settings, "filter", ["selection"])
filter_indent = get_container_child_indent(filter_el)
frag_xml = build_filter_item_fragment(parsed, filter_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(filter_el, node, None, filter_indent)
print(f'[OK] Filter "{parsed["field"]} {parsed["op"]}" added to variant "{var_name}"')
elif operation == "add-dataParameter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_data_param_shorthand(val)
dp_el = ensure_settings_child(settings, "dataParameters", ["outputParameters", "conditionalAppearance", "order", "filter", "selection"])
dp_indent = get_container_child_indent(dp_el)
frag_xml = build_data_param_fragment(parsed, dp_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(dp_el, node, None, dp_indent)
print(f'[OK] DataParameter "{parsed["parameter"]}" added to variant "{var_name}"')
elif operation == "add-order":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_order_shorthand(val)
order_el = ensure_settings_child(settings, "order", ["filter", "selection"])
order_indent = get_container_child_indent(order_el)
if parsed["field"] == "Auto":
is_dup = False
for ch in order_el:
if isinstance(ch.tag, str) and local_name(ch) == "item":
type_attr = ch.get(XSI_TYPE, "")
if "OrderItemAuto" in type_attr:
is_dup = True
break
if is_dup:
print(f'[WARN] OrderItemAuto already exists in variant "{var_name}" -- skipped')
continue
else:
existing_ord = find_element_by_child_value(order_el, "item", "field", parsed["field"], SET_NS)
if existing_ord is not None:
print(f'[WARN] Order "{parsed["field"]}" already exists in variant "{var_name}" -- skipped')
continue
frag_xml = build_order_item_fragment(parsed, order_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(order_el, node, None, order_indent)
desc = "Auto" if parsed["field"] == "Auto" else f"{parsed['field']} {parsed['direction']}"
print(f'[OK] Order "{desc}" added to variant "{var_name}"')
elif operation == "add-selection":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
field_name = val.strip()
group_name = None
# Extract @group=Name
gm = re.search(r'\s*@group=(\S+)', field_name)
if gm:
group_name = gm.group(1)
field_name = re.sub(r'\s*@group=\S+', '', field_name).strip()
if group_name:
# Find named StructureItemGroup
target_el = None
for item in settings.iter(f"{{{SET_NS}}}item"):
xsi_type = item.get(f"{{{XSI_NS}}}type", "")
if "StructureItemGroup" in xsi_type:
name_el = item.find(f"{{{SET_NS}}}name")
if name_el is not None and name_el.text == group_name:
target_el = item
break
if target_el is None:
print(f'[WARN] StructureItemGroup "{group_name}" not found -- adding to variant level')
target_el = settings
else:
target_el = settings
selection = ensure_settings_child(target_el, "selection", [])
# Dedup: skip if SelectedItemAuto already exists
if field_name == "Auto":
is_dup = False
for ch in selection:
if isinstance(ch.tag, str) and local_name(ch) == "item":
type_attr = ch.get(XSI_TYPE, "")
if "SelectedItemAuto" in type_attr:
is_dup = True
break
if is_dup:
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
print(f'[WARN] SelectedItemAuto already exists in {target} -- skipped')
continue
sel_indent = get_container_child_indent(selection)
sel_xml = build_selection_item_fragment(field_name, sel_indent)
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
print(f'[OK] Selection "{field_name}" added to {target}')
elif operation == "set-query":
ds_node = resolve_data_set()
ds_name = get_data_set_name(ds_node)
query_el = find_first_element(ds_node, ["query"], SCH_NS)
if query_el is None:
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
sys.exit(1)
query_el.text = resolve_query_value(value_arg, query_base_dir)
print(f'[OK] Query replaced in dataset "{ds_name}"')
elif operation == "patch-query":
ds_node = resolve_data_set()
ds_name = get_data_set_name(ds_node)
query_el = find_first_element(ds_node, ["query"], SCH_NS)
if query_el is None:
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
sys.exit(1)
for val in values:
sep_idx = val.find(" => ")
if sep_idx < 0:
print("patch-query value must contain ' => ' separator: old => new", file=sys.stderr)
sys.exit(1)
old_str = val[:sep_idx]
new_str = val[sep_idx + 4:]
query_text = query_el.text or ""
if old_str not in query_text:
print(f"Substring not found in query of dataset '{ds_name}': {old_str}", file=sys.stderr)
sys.exit(1)
query_el.text = query_text.replace(old_str, new_str)
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'')
elif operation == "set-outputParameter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_output_param_shorthand(val)
output_el = ensure_settings_child(settings, "outputParameters", ["conditionalAppearance", "order", "filter", "selection"])
output_indent = get_container_child_indent(output_el)
existing_param = find_element_by_child_value(output_el, "item", "parameter", parsed["key"], COR_NS)
if existing_param is not None:
remove_node_with_whitespace(existing_param)
print(f'[OK] Replaced outputParameter "{parsed["key"]}" in variant "{var_name}"')
else:
print(f'[OK] OutputParameter "{parsed["key"]}" added to variant "{var_name}"')
frag_xml = build_output_param_fragment(parsed, output_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(output_el, node, None, output_indent)
elif operation == "set-structure":
settings = resolve_variant_settings()
var_name = get_variant_name()
to_remove = [ch for ch in settings if isinstance(ch.tag, str) and local_name(ch) == "item" and etree.QName(ch.tag).namespace == SET_NS]
for el in to_remove:
remove_node_with_whitespace(el)
struct_items = parse_structure_shorthand(value_arg)
settings_indent = get_child_indent(settings)
ref_node = find_first_element(settings, ["outputParameters", "dataParameters", "conditionalAppearance", "order", "filter", "selection", "item"], SET_NS)
for struct_item in struct_items:
frag_xml = build_structure_item_fragment(struct_item, settings_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(settings, node, ref_node, settings_indent)
print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
elif operation == "add-dataSetLink":
for val in values:
parsed = parse_data_set_link_shorthand(val)
child_indent = get_child_indent(xml_doc)
frag_xml = build_data_set_link_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
last_link = find_last_element(xml_doc, "dataSetLink", SCH_NS)
if last_link is not None:
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_link:
found = True
else:
ref_node = find_first_element(xml_doc, ["calculatedField", "totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
desc = f"{parsed['source']} > {parsed['dest']} on {parsed['sourceExpr']} = {parsed['destExpr']}"
if parsed.get("parameter"):
desc += f" [param {parsed['parameter']}]"
print(f'[OK] DataSetLink "{desc}" added')
elif operation == "add-dataSet":
child_indent = get_child_indent(xml_doc)
parsed = parse_data_set_shorthand(value_arg)
parsed["query"] = resolve_query_value(parsed["query"], query_base_dir)
if not parsed["name"]:
count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS)
parsed["name"] = f"\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445{count + 1}"
existing = find_element_by_child_value(xml_doc, "dataSet", "name", parsed["name"], SCH_NS)
if existing is not None:
print(f'[WARN] DataSet "{parsed["name"]}" already exists -- skipped')
else:
ds_source_el = find_first_element(xml_doc, ["dataSource"], SCH_NS)
ds_source_name = "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0414\u0430\u043d\u043d\u044b\u04451"
if ds_source_el is not None:
name_el = find_first_element(ds_source_el, ["name"], SCH_NS)
if name_el is not None:
ds_source_name = (name_el.text or "").strip()
parsed["dataSource"] = ds_source_name
frag_xml = build_data_set_query_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
last_ds = find_last_element(xml_doc, "dataSet", SCH_NS)
if last_ds is not None:
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_ds:
found = True
else:
ref_node = find_first_element(xml_doc, ["dataSetLink", "calculatedField", "totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] DataSet "{parsed["name"]}" added (dataSource={ds_source_name})')
elif operation == "add-variant":
child_indent = get_child_indent(xml_doc)
for val in values:
parsed = parse_variant_shorthand(val)
is_dup = False
for ch in xml_doc:
if isinstance(ch.tag, str) and local_name(ch) == "settingsVariant" and etree.QName(ch.tag).namespace == SCH_NS:
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS and gc.text == parsed["name"]:
is_dup = True
break
if is_dup:
break
if is_dup:
print(f'[WARN] Variant "{parsed["name"]}" already exists -- skipped')
continue
frag_xml = build_variant_fragment(parsed, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
last_sv = find_last_element(xml_doc, "settingsVariant", SCH_NS)
if last_sv is not None:
ref_node = None
found = False
for ch in xml_doc:
if found and isinstance(ch.tag, str):
ref_node = ch
break
if ch is last_sv:
found = True
else:
ref_node = None
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] Variant "{parsed["name"]}" ["{parsed["presentation"]}"] added')
elif operation == "add-conditionalAppearance":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_conditional_appearance_shorthand(val)
ca_el = ensure_settings_child(settings, "conditionalAppearance", ["outputParameters", "order", "filter", "selection"])
ca_indent = get_container_child_indent(ca_el)
frag_xml = build_conditional_appearance_item_fragment(parsed, ca_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(ca_el, node, None, ca_indent)
desc = f"{parsed['param']} = {parsed['value']}"
if parsed.get("filter"):
flt = parsed["filter"]
if isinstance(flt, list):
desc += f" when OrGroup({len(flt)} conditions)"
else:
desc += f" when {flt['field']} {flt['op']}"
if parsed.get("fields"):
desc += f" for {', '.join(parsed['fields'])}"
print(f'[OK] ConditionalAppearance "{desc}" added to variant "{var_name}"')
elif operation == "clear-selection":
settings = resolve_variant_settings()
var_name = get_variant_name()
selection = find_first_element(settings, ["selection"], SET_NS)
if selection is not None:
clear_container_children(selection)
print(f'[OK] Selection cleared in variant "{var_name}"')
else:
print(f'[INFO] No selection section in variant "{var_name}"')
elif operation == "clear-order":
settings = resolve_variant_settings()
var_name = get_variant_name()
order_el = find_first_element(settings, ["order"], SET_NS)
if order_el is not None:
clear_container_children(order_el)
print(f'[OK] Order cleared in variant "{var_name}"')
else:
print(f'[INFO] No order section in variant "{var_name}"')
elif operation == "clear-filter":
settings = resolve_variant_settings()
var_name = get_variant_name()
filter_el = find_first_element(settings, ["filter"], SET_NS)
if filter_el is not None:
clear_container_children(filter_el)
print(f'[OK] Filter cleared in variant "{var_name}"')
else:
print(f'[INFO] No filter section in variant "{var_name}"')
elif operation == "modify-filter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_filter_shorthand(val)
filter_el = find_first_element(settings, ["filter"], SET_NS)
if filter_el is None:
print(f'[WARN] No filter section in variant "{var_name}"')
continue
filter_item = find_element_by_child_value(filter_el, "item", "left", parsed["field"], SET_NS)
if filter_item is None:
print(f'[WARN] Filter for "{parsed["field"]}" not found in variant "{var_name}"')
continue
item_indent = get_child_indent(filter_item)
set_or_create_child_element(filter_item, "comparisonType", SET_NS, parsed["op"], item_indent)
if parsed.get("value") is not None:
vt = parsed.get("valueType", "xs:string")
set_or_create_child_element_with_attr(filter_item, "right", SET_NS, str(parsed["value"]), vt, item_indent)
# Update use (only when explicitly set via @off / @on)
if parsed.get("use") is False:
set_or_create_child_element(filter_item, "use", SET_NS, "false", item_indent)
elif parsed.get("use") is True:
# @on: remove existing use=false if any
for ch in filter_item:
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SET_NS:
if (ch.text or "").strip() == "false":
remove_node_with_whitespace(ch)
break
if parsed.get("viewMode"):
set_or_create_child_element(filter_item, "viewMode", SET_NS, parsed["viewMode"], item_indent)
if parsed.get("userSettingID"):
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
set_or_create_child_element(filter_item, "userSettingID", SET_NS, uid, item_indent)
print(f'[OK] Filter "{parsed["field"]}" modified in variant "{var_name}"')
elif operation == "modify-dataParameter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
parsed = parse_data_param_shorthand(val)
dp_el = find_first_element(settings, ["dataParameters"], SET_NS)
if dp_el is None:
print(f'[WARN] No dataParameters section in variant "{var_name}"')
continue
dp_item = find_element_by_child_value(dp_el, "item", "parameter", parsed["parameter"], COR_NS)
if dp_item is None:
print(f'[WARN] DataParameter "{parsed["parameter"]}" not found in variant "{var_name}"')
continue
item_indent = get_child_indent(dp_item)
if parsed.get("value") is not None:
existing_val = None
for ch in dp_item:
if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == COR_NS:
existing_val = ch
break
if existing_val is not None:
remove_node_with_whitespace(existing_val)
val_lines = []
pv = parsed["value"]
if isinstance(pv, dict) and pv.get("variant"):
val_lines.append(f'{item_indent}<dcscor:value xsi:type="v8:StandardPeriod">')
val_lines.append(f'{item_indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(pv["variant"])}</v8:variant>')
val_lines.append(f"{item_indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
val_lines.append(f"{item_indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
val_lines.append(f"{item_indent}</dcscor:value>")
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(pv)):
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(pv))}</dcscor:value>')
elif str(pv) in ("true", "false"):
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:boolean">{esc_xml(str(pv))}</dcscor:value>')
else:
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:string">{esc_xml(str(pv))}</dcscor:value>')
val_xml = "\r\n".join(val_lines)
val_nodes = import_fragment(xml_doc, val_xml)
for node in val_nodes:
insert_before_element(dp_item, node, None, item_indent)
# Update use (only when explicitly set via @off / @on)
if parsed.get("use") is False:
set_or_create_child_element(dp_item, "use", COR_NS, "false", item_indent)
elif parsed.get("use") is True:
# @on: remove existing use=false if any
for ch in dp_item:
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == COR_NS:
if (ch.text or "").strip() == "false":
remove_node_with_whitespace(ch)
break
if parsed.get("viewMode"):
set_or_create_child_element(dp_item, "viewMode", SET_NS, parsed["viewMode"], item_indent)
if parsed.get("userSettingID"):
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
set_or_create_child_element(dp_item, "userSettingID", SET_NS, uid, item_indent)
print(f'[OK] DataParameter "{parsed["parameter"]}" modified in variant "{var_name}"')
elif operation == "modify-field":
ds_node = resolve_data_set()
ds_name = get_data_set_name(ds_node)
for val in values:
parsed = parse_field_shorthand(val)
field_name = parsed["dataPath"]
field_el = find_element_by_child_value(ds_node, "field", "dataPath", field_name, SCH_NS)
if field_el is None:
print(f'[WARN] Field "{field_name}" not found in dataset "{ds_name}"')
continue
existing = read_field_properties(field_el)
merged = {
"dataPath": existing["dataPath"],
"field": existing["field"],
"title": parsed["title"] if parsed.get("title") else existing["title"],
"type": parsed["type"] if parsed.get("type") else existing["type"],
"roles": parsed["roles"] if parsed.get("roles") else existing["roles"],
"restrict": parsed["restrict"] if parsed.get("restrict") else existing["restrict"],
}
# Find next element sibling for position
next_sib = None
found = False
for ch in ds_node:
if found and isinstance(ch.tag, str):
next_sib = ch
break
if ch is field_el:
found = True
child_indent = get_child_indent(ds_node)
remove_node_with_whitespace(field_el)
frag_xml = build_field_fragment(merged, child_indent)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(ds_node, node, next_sib, child_indent)
print(f'[OK] Field "{field_name}" modified in dataset "{ds_name}"')
elif operation == "remove-field":
ds_node = resolve_data_set()
ds_name = get_data_set_name(ds_node)
for val in values:
field_name = val.strip()
field_el = find_element_by_child_value(ds_node, "field", "dataPath", field_name, SCH_NS)
if field_el is None:
print(f'[WARN] Field "{field_name}" not found in dataset "{ds_name}"')
continue
remove_node_with_whitespace(field_el)
print(f'[OK] Field "{field_name}" removed from dataset "{ds_name}"')
try:
settings = resolve_variant_settings()
var_name = get_variant_name()
selection = find_first_element(settings, ["selection"], SET_NS)
if selection is not None:
sel_item = find_element_by_child_value(selection, "item", "field", field_name, SET_NS)
if sel_item is not None:
remove_node_with_whitespace(sel_item)
print(f'[OK] Field "{field_name}" removed from selection of variant "{var_name}"')
except SystemExit:
pass
elif operation == "remove-total":
for val in values:
data_path = val.strip()
total_el = find_element_by_child_value(xml_doc, "totalField", "dataPath", data_path, SCH_NS)
if total_el is None:
print(f'[WARN] TotalField "{data_path}" not found')
continue
remove_node_with_whitespace(total_el)
print(f'[OK] TotalField "{data_path}" removed')
elif operation == "remove-calculated-field":
for val in values:
data_path = val.strip()
calc_el = find_element_by_child_value(xml_doc, "calculatedField", "dataPath", data_path, SCH_NS)
if calc_el is None:
print(f'[WARN] CalculatedField "{data_path}" not found')
continue
remove_node_with_whitespace(calc_el)
print(f'[OK] CalculatedField "{data_path}" removed')
try:
settings = resolve_variant_settings()
var_name = get_variant_name()
selection = find_first_element(settings, ["selection"], SET_NS)
if selection is not None:
sel_item = find_element_by_child_value(selection, "item", "field", data_path, SET_NS)
if sel_item is not None:
remove_node_with_whitespace(sel_item)
print(f'[OK] Field "{data_path}" removed from selection of variant "{var_name}"')
except SystemExit:
pass
elif operation == "remove-parameter":
for val in values:
param_name = val.strip()
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
if param_el is None:
print(f'[WARN] Parameter "{param_name}" not found')
continue
remove_node_with_whitespace(param_el)
print(f'[OK] Parameter "{param_name}" removed')
elif operation == "remove-filter":
settings = resolve_variant_settings()
var_name = get_variant_name()
for val in values:
field_name = val.strip()
filter_el = find_first_element(settings, ["filter"], SET_NS)
if filter_el is None:
print(f'[WARN] No filter section in variant "{var_name}"')
continue
filter_item = find_element_by_child_value(filter_el, "item", "left", field_name, SET_NS)
if filter_item is None:
print(f'[WARN] Filter for "{field_name}" not found in variant "{var_name}"')
continue
remove_node_with_whitespace(filter_item)
print(f'[OK] Filter for "{field_name}" removed from variant "{var_name}"')
elif operation == "add-drilldown":
# String-based manipulation — templates use dcsat namespace with inline xmlns
with open(resolved_path, "r", encoding="utf-8-sig") as f:
raw_text = f.read()
nl = "\r\n"
dcsat_ns_decl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"'
# Find all outer <template> blocks by nesting-aware scan
name_regex = re.compile(r'<template>\s*<name>([^<]+)</name>')
tpl_starts = [(m.start(), m.group(1)) for m in name_regex.finditer(raw_text)]
# For each start, find closing </template> at nesting depth 0
tpl_blocks = []
for ts_pos, ts_name in tpl_starts:
depth = 1
scan_pos = ts_pos + 10 # skip past opening <template>
while depth > 0 and scan_pos < len(raw_text):
next_open = raw_text.find("<template", scan_pos)
next_close = raw_text.find("</template>", scan_pos)
if next_close < 0:
break
if next_open >= 0 and next_open < next_close:
depth += 1
scan_pos = next_open + 10
else:
depth -= 1
if depth == 0:
end_pos = next_close + len("</template>")
tpl_blocks.append((ts_name, ts_pos, raw_text[ts_pos:end_pos]))
scan_pos = next_close + 11
if not tpl_blocks:
print("[WARN] No named templates found in schema")
# Collect all insertions as (position, text) — apply in reverse order
insertions = []
expr_regex = re.compile(
r'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*'
r'<dcsat:name>([^<]+)</dcsat:name>\s*'
r'<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>'
)
for tpl_name, tpl_start, tpl_text in tpl_blocks:
# Build map: expression → paramName from ExpressionAreaTemplateParameter
expr_map = {}
for em in expr_regex.finditer(tpl_text):
p_name = em.group(1)
p_expr = em.group(2)
expr_map[p_expr] = p_name
for resource in values:
drill_name = f"Расшифровка_{resource}"
# Idempotency: check if already exists
if drill_name in tpl_text:
print(f"[INFO] {drill_name} already exists in {tpl_name} — skipped")
continue
# Find ExpressionAreaTemplateParameter by expression
param_name = expr_map.get(resource)
if param_name is None:
print(f'[WARN] Expression "{resource}" not found in template {tpl_name} — skipped')
continue
cell_count = 0
# Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template
last_param_end_tag = "</parameter>"
last_param_pos = tpl_text.rfind(last_param_end_tag)
if last_param_pos >= 0:
insert_pos = tpl_start + last_param_pos + len(last_param_end_tag)
# Detect indent from context
prev_nl = tpl_text.rfind("\n", 0, last_param_pos)
indent = "\t\t"
if prev_nl >= 0:
line_start = prev_nl + 1
indent_match = re.match(r'^(\s*)', tpl_text[line_start:])
if indent_match:
indent = indent_match.group(1)
details_xml = (
f'{nl}{indent}<parameter {dcsat_ns_decl} xsi:type="dcsat:DetailsAreaTemplateParameter">'
f'{nl}{indent}\t<dcsat:name>{drill_name}</dcsat:name>'
f'{nl}{indent}\t<dcsat:fieldExpression>'
f'{nl}{indent}\t\t<dcsat:field>ИмяРесурса</dcsat:field>'
f'{nl}{indent}\t\t<dcsat:expression>"{resource}"</dcsat:expression>'
f'{nl}{indent}\t</dcsat:fieldExpression>'
f'{nl}{indent}\t<dcsat:mainAction>DrillDown</dcsat:mainAction>'
f'{nl}{indent}</parameter>'
)
insertions.append((insert_pos, details_xml))
# Step 2: Insert appearance binding in cells referencing this parameter
cell_tag = f'<dcsat:value xsi:type="dcscor:Parameter">{param_name}</dcsat:value>'
search_start = 0
while True:
cell_idx = tpl_text.find(cell_tag, search_start)
if cell_idx < 0:
break
cell_end = tpl_text.find("</dcsat:tableCell>", cell_idx)
if cell_end < 0:
break
app_end = tpl_text.rfind("</dcsat:appearance>", cell_idx, cell_end)
if app_end < cell_idx:
search_start = cell_end + 1
continue
# Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance>
app_prev_nl = tpl_text.rfind("\n", 0, app_end)
app_indent = "\t\t\t\t\t\t"
if app_prev_nl >= 0:
app_line_start = app_prev_nl + 1
app_indent_match = re.match(r'^(\s*)', tpl_text[app_line_start:])
if app_indent_match:
app_indent = app_indent_match.group(1)
item_indent = app_indent + "\t"
appearance_xml = (
f'{item_indent}<dcscor:item>{nl}'
f'{item_indent}\t<dcscor:parameter>Расшифровка</dcscor:parameter>{nl}'
f'{item_indent}\t<dcscor:value xsi:type="dcscor:Parameter">{drill_name}</dcscor:value>{nl}'
f'{item_indent}</dcscor:item>{nl}'
)
# Insert after \n (before indent of closing tag), not before the tag itself
insert_at = (tpl_start + app_prev_nl + 1) if app_prev_nl >= 0 else (tpl_start + app_end)
insertions.append((insert_at, appearance_xml))
cell_count += 1
search_start = cell_end + 1
print(f"[OK] {drill_name} \u2192 {tpl_name} (param + {cell_count} cell(s))")
# Apply insertions in reverse order to preserve offsets.
# For same position: reverse insertion order so first resource ends up first in file.
insertions = [(pos, text, seq) for seq, (pos, text) in enumerate(insertions)]
insertions.sort(key=lambda x: (x[0], x[2]), reverse=True)
for pos, text, _seq in insertions:
raw_text = raw_text[:pos] + text + raw_text[pos:]
# Write directly — skip lxml save
with open(resolved_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
f.write(raw_text.encode("utf-8"))
print(f"[OK] Saved {resolved_path}")
sys.exit(0)
# ── 9. Save ─────────────────────────────────────────────────
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
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"
with open(resolved_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
f.write(xml_bytes)
print(f"[OK] Saved {resolved_path}")