mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-14 14:55:16 +03:00
Auto-build: codeassistant (powershell) from e473d6c
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
#!/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'<MetaDataObject[^>]+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"<?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(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 = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
f' version="{format_version}">\n'
|
||||
f'\t<Page>{lang}</Page>\n'
|
||||
'</Help>'
|
||||
)
|
||||
|
||||
write_text_with_bom(help_xml_path, help_xml)
|
||||
|
||||
# --- 2. Help/<lang>.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 = (
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
|
||||
'<html>\n'
|
||||
'<head>\n'
|
||||
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
|
||||
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
|
||||
'</head>\n'
|
||||
'<body>\n'
|
||||
f' <h1>{object_name}</h1>\n'
|
||||
' <p>Описание.</p>\n'
|
||||
'</body>\n'
|
||||
'</html>'
|
||||
)
|
||||
|
||||
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 <FormType>
|
||||
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()
|
||||
Reference in New Issue
Block a user