Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Safely change CommandName for an existing 1C managed form button."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from edit_1c_form_button import (
|
||||
attr_value,
|
||||
ensure_inside_working,
|
||||
is_void_or_self,
|
||||
load_json,
|
||||
local_name,
|
||||
manifest_record,
|
||||
normalize_command_name,
|
||||
read_text,
|
||||
render_reindented,
|
||||
safe_relative,
|
||||
tag_local,
|
||||
write_json,
|
||||
xml_text,
|
||||
TAG_RE,
|
||||
)
|
||||
from validate_1c_patch_workspace_semantics import child_text, validate_workspace
|
||||
|
||||
|
||||
def form_command_exists(path: Path, command_name: str) -> bool:
|
||||
normalized = normalize_command_name(command_name)
|
||||
if normalized.startswith("Form.StandardCommand."):
|
||||
return True
|
||||
local_command_name = normalized.removeprefix("Form.Command.")
|
||||
root = ET.parse(path).getroot()
|
||||
return any(local_name(element.tag) == "Command" and element.attrib.get("name") == local_command_name for element in root.iter())
|
||||
|
||||
|
||||
def button_state(path: Path, button_name: str) -> dict[str, Any] | None:
|
||||
root = ET.parse(path).getroot()
|
||||
for element in root.iter():
|
||||
if local_name(element.tag) == "Button" and element.attrib.get("name") == button_name:
|
||||
return {"name": button_name, "id": element.attrib.get("id"), "command_name": child_text(element, "CommandName")}
|
||||
return None
|
||||
|
||||
|
||||
def find_button_span(text: str, button_name: str) -> tuple[int, int, int, int, str]:
|
||||
for match in TAG_RE.finditer(text):
|
||||
if match.group("close") or is_void_or_self(match):
|
||||
continue
|
||||
if tag_local(match.group("tag")) != "Button":
|
||||
continue
|
||||
if attr_value(match.group("attrs") or "", "name") != button_name:
|
||||
continue
|
||||
stack = ["Button"]
|
||||
for next_match in TAG_RE.finditer(text, match.end()):
|
||||
next_tag = tag_local(next_match.group("tag"))
|
||||
if next_match.group("close"):
|
||||
if stack and stack[-1] == next_tag:
|
||||
stack.pop()
|
||||
if not stack:
|
||||
line_start = text.rfind("\n", 0, match.start()) + 1
|
||||
indent = re.match(r"^[ \t]*", text[line_start : match.start()]).group(0)
|
||||
return match.start(), next_match.end(), match.end(), next_match.start(), indent
|
||||
continue
|
||||
if not is_void_or_self(next_match):
|
||||
stack.append(next_tag)
|
||||
raise SystemExit(f"Could not find closing </Button> for: {button_name}")
|
||||
raise SystemExit(f"Form button not found by name: {button_name}")
|
||||
|
||||
|
||||
def replace_or_insert_command_name(text: str, *, button_name: str, command_name: str) -> tuple[str, str, str | None]:
|
||||
_button_start, _button_end, start_tag_end, end_tag_start, button_indent = find_button_span(text, button_name)
|
||||
body = text[start_tag_end:end_tag_start]
|
||||
normalized = normalize_command_name(command_name)
|
||||
command_re = re.compile(r"(?P<indent>^[ \t]*)<CommandName\b[^>]*>(?P<value>[\s\S]*?)</CommandName>[ \t]*(?:\r?\n)?", re.MULTILINE)
|
||||
match = command_re.search(body)
|
||||
if match:
|
||||
old = html.unescape(match.group("value").strip())
|
||||
replacement = f"{match.group('indent')}<CommandName>{xml_text(normalized)}</CommandName>\n"
|
||||
updated_body = body[: match.start()] + replacement + body[match.end() :]
|
||||
return text[:start_tag_end] + updated_body + text[end_tag_start:], "replaced", old
|
||||
insert = render_reindented(f"<CommandName>{xml_text(normalized)}</CommandName>", button_indent + "\t") + "\n"
|
||||
return text[:start_tag_end] + "\n" + insert + text[start_tag_end:], "inserted", None
|
||||
|
||||
|
||||
def validate_form_xml(path: Path, before: str, *, keep_on_failure: bool) -> tuple[bool, bool, list[dict[str, Any]]]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
rolled_back = False
|
||||
try:
|
||||
ET.parse(path)
|
||||
return True, False, findings
|
||||
except ET.ParseError as exc:
|
||||
findings.append({"severity": "error", "message": str(exc), "path": str(path)})
|
||||
if not keep_on_failure:
|
||||
path.write_text(before, encoding="utf-8")
|
||||
rolled_back = True
|
||||
return False, rolled_back, findings
|
||||
|
||||
|
||||
def edit_form_file(
|
||||
path: Path,
|
||||
*,
|
||||
button_name: str,
|
||||
command_name: str,
|
||||
keep_on_failure: bool,
|
||||
) -> dict[str, Any]:
|
||||
form_path = path.resolve()
|
||||
if not form_path.exists():
|
||||
raise SystemExit(f"Form.xml does not exist: {form_path}")
|
||||
if not form_command_exists(form_path, command_name):
|
||||
raise SystemExit(f"Form command does not exist: {command_name}")
|
||||
state = button_state(form_path, button_name)
|
||||
if not state:
|
||||
raise SystemExit(f"Form button does not exist: {button_name}")
|
||||
before = read_text(form_path)
|
||||
updated, status, old = replace_or_insert_command_name(before, button_name=button_name, command_name=command_name)
|
||||
form_path.write_text(updated, encoding="utf-8")
|
||||
passed, rolled_back, findings = validate_form_xml(form_path, before, keep_on_failure=keep_on_failure)
|
||||
return {
|
||||
"schema": "onec_form_button_command_edit.v1",
|
||||
"path": str(form_path),
|
||||
"edit": {
|
||||
"status": status,
|
||||
"button": {
|
||||
"name": button_name,
|
||||
"id": state.get("id"),
|
||||
"old_command_name": old if old is not None else state.get("command_name"),
|
||||
"command_name": normalize_command_name(command_name),
|
||||
},
|
||||
},
|
||||
"rolled_back": rolled_back,
|
||||
"semantic_validation": {
|
||||
"schema": "onec_form_xml_direct_validation.v1",
|
||||
"passed": passed,
|
||||
"counts": {"findings": len(findings)},
|
||||
"findings": findings,
|
||||
},
|
||||
"passed": passed and not rolled_back,
|
||||
}
|
||||
|
||||
|
||||
def edit_workspace(
|
||||
workspace: Path,
|
||||
relative_path: str,
|
||||
*,
|
||||
button_name: str,
|
||||
command_name: str,
|
||||
keep_on_failure: bool,
|
||||
) -> dict[str, Any]:
|
||||
record = manifest_record(workspace, relative_path)
|
||||
path = ensure_inside_working(workspace, str(record.get("relative_path") or relative_path))
|
||||
before = read_text(path)
|
||||
result = edit_form_file(path, button_name=button_name, command_name=command_name, keep_on_failure=True)
|
||||
semantic = validate_workspace(workspace)
|
||||
rolled_back = False
|
||||
if not semantic.get("passed") and not keep_on_failure:
|
||||
path.write_text(before, encoding="utf-8")
|
||||
rolled_back = True
|
||||
result.update(
|
||||
{
|
||||
"workspace": str(workspace),
|
||||
"relative_path": str(record.get("relative_path")),
|
||||
"rolled_back": rolled_back,
|
||||
"semantic_validation": {
|
||||
"schema": semantic.get("schema"),
|
||||
"passed": semantic.get("passed"),
|
||||
"counts": semantic.get("counts"),
|
||||
"findings": semantic.get("findings"),
|
||||
},
|
||||
"passed": bool(semantic.get("passed")) and not rolled_back,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Change CommandName for an existing button in 1C managed Form.xml.")
|
||||
parser.add_argument("--workspace", type=Path)
|
||||
parser.add_argument("--relative-path")
|
||||
parser.add_argument("--form-path", type=Path, help="Direct Form.xml path for extension-export XML editing.")
|
||||
parser.add_argument("--button-name", "--name", dest="button_name", required=True)
|
||||
parser.add_argument("--command-name", required=True)
|
||||
parser.add_argument("--keep-on-failure", action="store_true")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.form_path:
|
||||
result = edit_form_file(
|
||||
args.form_path,
|
||||
button_name=args.button_name,
|
||||
command_name=args.command_name,
|
||||
keep_on_failure=args.keep_on_failure,
|
||||
)
|
||||
else:
|
||||
if not args.workspace or not args.relative_path:
|
||||
parser.error("either --form-path or both --workspace and --relative-path are required")
|
||||
result = edit_workspace(
|
||||
args.workspace,
|
||||
safe_relative(args.relative_path),
|
||||
button_name=args.button_name,
|
||||
command_name=args.command_name,
|
||||
keep_on_failure=args.keep_on_failure,
|
||||
)
|
||||
if args.output:
|
||||
write_json(args.output, result)
|
||||
print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "rolled_back": result["rolled_back"], "edit": result["edit"]}, ensure_ascii=False))
|
||||
return 0 if result["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user