Files
cc-1c-skills/.claude/skills/template-remove/scripts/remove-template.py
T
Nick ShirokovandClaude Opus 5 a7406d0982 test(8 навыков): байтовые проверки канона там, где их не было (#57)
Из ~46 навыков-эмиттеров XML восемь не имели ни одной проверки `preserves`:
form-edit, form-remove, meta-remove, skd-edit, template-remove, xdto-edit,
support-edit, cfe-patch-method. Шесть из них правились в предыдущем коммите,
и канон у них держался на том, что правка была механической, а не на проверке.

Ожидания писались ПО КАНОНУ, а не по текущему поведению — и это сразу вскрыло
три дефекта:

1. form-remove очищал слот формы через InnerText="" / text="" — получалась
   пустая пара <DefaultObjectForm></DefaultObjectForm>. Платформа пустых пар
   не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None.

2. Опустевший <ChildObjects> оставался парой, разнесённой по строкам, —
   в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся
   <ChildObjects/> на acc+erp, пустых пар 0 в обеих формах.

3. template-add писал пустой макет парой <SpreadsheetDocument></...>.
   Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет
   ни для одного тега.

Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги,
поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки
внутри; дискриминатором служит сам перевод строки — значащий пробельный
текст-узел (<xr:FillValue xsi:type="xs:string">   </xr:FillValue>) его не
содержит и под проверку не попадает.

support-edit покрыт частично (BOM у ParentConfigurations.bin — проверено, что
платформа пишет его с BOM во всех трёх выгрузках), cfe-patch-method — BOM+EOL
у .bsl: хвостовой перевод строки у модулей неканоничен (1235 с ним, 766 без),
поэтому не утверждается.

Регресс: 647/647 ps1, 644/647 py (3 skipped). Эталоны переснятые: три места,
каждое — ровно ожидаемая пара строк.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:05:27 +03:00

144 lines
5.8 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.
#!/usr/bin/env python3
# template-remove v1.7 — Remove template from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None → файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Remove template from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
templates_dir = os.path.join(processor_dir, "Templates")
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
template_dir = os.path.join(templates_dir, template_name)
if not os.path.exists(template_meta_path):
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(template_dir):
shutil.rmtree(template_dir)
print(f"[OK] Удалён каталог: {template_dir}")
os.remove(template_meta_path)
print(f"[OK] Удалён файл: {template_meta_path}")
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Template>TemplateName</Template> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
if node.text and node.text.strip() == template_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
if len(parent) == 0 and not (parent.text or "").strip():
parent.text = None
break
# Clear MainDataCompositionSchema if it pointed to this template
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None and main_dcs.text:
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
main_dcs.text = ""
print("[OK] Очищён MainDataCompositionSchema")
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
if __name__ == "__main__":
main()