#!/usr/bin/env python3 # add-help v1.7 — Add built-in help to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json import os import re import sys from lxml import etree NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} # ============================================================ # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin # present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off, # default deny). Never throws (except sys.exit on deny) — errors degrade to allow. # ============================================================ def _sg_root_uuid(xml_path): if not os.path.isfile(xml_path): return None try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str) and child.get("uuid"): return child.get("uuid") except Exception: return None return None def _sg_find_v8project(start_dir): d = start_dir for _ in range(20): if not d: break pj = os.path.join(d, ".v8-project.json") if os.path.isfile(pj): return pj parent = os.path.dirname(d) if parent == d: break d = parent return None def _sg_get_edit_mode(cfg_dir): try: pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir) if not pj: return "deny" proj = json.loads(open(pj, encoding="utf-8-sig").read()) cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/") for db in proj.get("databases", []): src = db.get("configSrc") if src: src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/") if cfg_full == src_full or cfg_full.startswith(src_full + os.sep): if db.get("editingAllowedCheck"): return db["editingAllowedCheck"] if proj.get("editingAllowedCheck"): return proj["editingAllowedCheck"] return "deny" except Exception: return "deny" def assert_edit_allowed(target_path, require): try: rp = os.path.abspath(target_path) elem_uuid = _sg_root_uuid(rp) cfg_dir = None bin_path = None d = rp if os.path.isdir(rp) else os.path.dirname(rp) for _ in range(12): if not d: break if not elem_uuid: elem_uuid = _sg_root_uuid(d + ".xml") if not cfg_dir: cand = os.path.join(d, "Ext", "ParentConfigurations.bin") if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")): cfg_dir = d bin_path = cand if elem_uuid and cfg_dir: break parent = os.path.dirname(d) if parent == d: break d = parent if not elem_uuid and cfg_dir: elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml")) if not bin_path or not os.path.exists(bin_path): return data = open(bin_path, "rb").read() if len(data) <= 32: return if data[:3] == b"\xef\xbb\xbf": data = data[3:] text = data.decode("utf-8", "replace") h = re.match(r"\{6,(\d+),(\d+),", text) if not h: return g = int(h.group(1)) k = int(h.group(2)) if k == 0: return best = None if elem_uuid: for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text): f1 = int(m.group(1)) if best is None or f1 < best: best = f1 blocked = False code = "" reason = "" if g == 1: blocked = True code = "capability-off" reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" elif require == "removed": if best is not None and best != 2: blocked = True code = "not-removed" reason = "объект не снят с поддержки — удаление сломает обновления" else: if best is not None and best == 0: blocked = True code = "locked" reason = "объект на замке — редактирование сломает обновления" if not blocked: return mode = _sg_get_edit_mode(cfg_dir) if mode == "off": return if mode == "warn": sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n") return head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." if code == "capability-off": state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя." fix = ( "Либо снять защиту явно (навык support-edit, два шага):\n" f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n' f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n' " Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." ) elif code == "not-removed": state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." fix = ( "Либо сначала снять объект с поддержки, затем удалять:\n" f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.' ) else: state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." fix = ( "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n" f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n' f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.' ) sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n") sys.exit(1) except SystemExit: raise except Exception: return def detect_format_version(d): while d: cfg_path = os.path.join(d, "Configuration.xml") if os.path.isfile(cfg_path): with open(cfg_path, "r", encoding="utf-8-sig") as f: head = f.read(2000) m = re.search(r']+version="(\d+\.\d+)"', head) if m: return m.group(1) parent = os.path.dirname(d) if parent == d: break d = parent return "2.17" def save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = xml_bytes.replace(b"", b'') if not xml_bytes.endswith(b"\n"): xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) def write_text_with_bom(path, text): """Write text to file with UTF-8 BOM.""" with open(path, "w", encoding="utf-8-sig") as f: f.write(text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False) parser.add_argument("-ObjectName", required=True) parser.add_argument("-Lang", default="ru") parser.add_argument("-SrcDir", default="src") args = parser.parse_args() object_name = args.ObjectName lang = args.Lang src_dir = args.SrcDir format_version = detect_format_version(os.path.abspath(src_dir)) # --- Checks --- object_dir = os.path.join(src_dir, object_name) ext_dir = os.path.join(object_dir, "Ext") if not os.path.isdir(ext_dir): print(f"Каталог объекта не найден: {ext_dir}. Проверьте путь ObjectName (например Catalogs/МойСправочник).", file=sys.stderr) sys.exit(1) help_xml_path = os.path.join(ext_dir, "Help.xml") if os.path.exists(help_xml_path): print(f"Справка уже существует: {help_xml_path}", file=sys.stderr) sys.exit(1) assert_edit_allowed(object_dir, "editable") # --- 1. Help.xml --- help_xml = ( '\n' '\n' f'\t{lang}\n' '' ) write_text_with_bom(help_xml_path, help_xml) # --- 2. Help/.html --- help_dir = os.path.join(ext_dir, "Help") os.makedirs(help_dir, exist_ok=True) help_html_path = os.path.join(help_dir, f"{lang}.html") help_html = ( '\n' '\n' '\n' ' \n' ' \n' '\n' '\n' f'

{object_name}

\n' '

Описание.

\n' '\n' '' ) write_text_with_bom(help_html_path, help_html) # --- 3. Check IncludeHelpInContents in form metadata --- forms_dir = os.path.join(object_dir, "Forms") if os.path.isdir(forms_dir): for entry in os.listdir(forms_dir): if not entry.endswith(".xml"): continue form_meta_full = os.path.join(forms_dir, entry) if not os.path.isfile(form_meta_full): continue parser_xml = etree.XMLParser(remove_blank_text=False) form_tree = etree.parse(form_meta_full, parser_xml) form_root = form_tree.getroot() include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP) if include_help is not None: continue # Add after form_type = form_root.find(".//md:FormType", NSMAP) if form_type is None: continue parent = form_type.getparent() ns = "http://v8.1c.ru/8.3/MDClasses" new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents") new_elem.text = "false" # Remove SubElement's auto-placement (it appends to end) and insert after FormType parent.remove(new_elem) # Find index of FormType in parent form_type_idx = list(parent).index(form_type) # Insert after FormType parent.insert(form_type_idx + 1, new_elem) # Whitespace handling: copy FormType's tail as new_elem's tail, # and set FormType's tail to include newline + indent new_elem.tail = form_type.tail form_type.tail = "\n\t\t\t" save_xml_with_bom(form_tree, form_meta_full) print(f" IncludeHelpInContents добавлен: {entry}") print(f"[OK] Создана справка: {object_name}") print(f" Метаданные: {help_xml_path}") print(f" Страница: {help_html_path}") if __name__ == "__main__": main()