Auto-build: cline (powershell) from 7409bac

This commit is contained in:
github-actions[bot]
2026-08-24 08:03:26 +00:00
commit b1bf37a390
335 changed files with 186190 additions and 0 deletions
@@ -0,0 +1,255 @@
# form-remove v1.10 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias("ProcessorName")]
[string]$ObjectName,
[Parameter(Mandatory)]
[string]$FormName,
[string]$SrcDir = "src",
[switch]$Force
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Проверки ---
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
exit 1
}
$processorDir = Join-Path $SrcDir $ObjectName
$formsDir = Join-Path $processorDir "Forms"
$formMetaPath = Join-Path $formsDir "$FormName.xml"
$formDir = Join-Path $formsDir $FormName
if (-not (Test-Path $formMetaPath)) {
Write-Error "Метаданные формы не найдены: $formMetaPath"
exit 1
}
# --- Загрузка корневого XML: вид и имя объекта ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$typeNode = $null
foreach ($c in $xmlDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { $typeNode = $c; break }
}
if (-not $typeNode) {
Write-Error "Не удалось определить вид объекта в $rootXmlPath"
exit 1
}
$mdType = $typeNode.LocalName
$nameNode = $typeNode.SelectSingleNode("md:Properties/md:Name", $nsMgr)
$objMetaName = if ($nameNode -and $nameNode.InnerText.Trim()) { $nameNode.InnerText.Trim() } else { [System.IO.Path]::GetFileNameWithoutExtension($rootXmlPath) }
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
$formRef = "$mdType.$objMetaName.Form.$FormName"
# --- Чистка ссылок и сохранение в стиле файла-источника ---
# Каноничное «не задано» зависит от файла: в корневом XML объекта и в Configuration.xml
# пустой слот штатен (164 508 пустых на корпус), а внутри Ext/Form.xml пустых <ChoiceForm/>
# и <SettingsStorage/> нет ни одного — там свойство просто отсутствует.
function Clear-FormRefs {
param([System.Xml.XmlDocument]$doc, [string]$ref)
$isFormFile = $doc.DocumentElement -and $doc.DocumentElement.LocalName -eq "Form"
$touched = @()
foreach ($node in @($doc.SelectNodes("//*"))) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($node.SelectNodes("*").Count -gt 0) { continue } # только листья
# Сравнение регистронезависимое — как у платформы (в py-порту .lower()).
if ($node.InnerText.Trim() -ne $ref) { continue }
$ln = $node.LocalName
$parent = $node.ParentNode
if ($ln -eq "Form" -and $parent -and $parent.LocalName -eq "Item") {
$touched += "$($parent.LocalName)/$ln"
Remove-NodeWithIndent $parent
} elseif ($isFormFile) {
$touched += $ln
Remove-NodeWithIndent $node
} else {
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
$touched += $ln
$node.IsEmpty = $true
}
}
return $touched
}
function Remove-NodeWithIndent {
param([System.Xml.XmlNode]$node)
$parent = $node.ParentNode
if (-not $parent) { return }
$prev = $node.PreviousSibling
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$parent.RemoveChild($prev) | Out-Null
}
$parent.RemoveChild($node) | Out-Null
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
}
function Save-XmlPreservingStyle {
param([System.Xml.XmlDocument]$doc, [string]$path)
$encBom = New-Object System.Text.UTF8Encoding($true)
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $path) -and ([System.IO.File]::ReadAllText($path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($path, $xmlText, $encBom)
}
# --- Поиск ссылок на форму по всей конфигурации ---
# Get-Item, а не Resolve-Path: последний оставляет короткое имя 8.3 (NSHIRO~1), а
# Get-ChildItem отдаёт длинное (nshirokov) — сравнение путей молча не совпадало.
function Get-LongPath {
param([string]$path)
if (-not (Test-Path -LiteralPath $path)) { return "" }
return (Get-Item -LiteralPath $path -Force).FullName
}
# Корень конфигурации: обычно это сам SrcDir, но объект могут передать и из глубины.
$configDir = $null
$probe = Get-LongPath $SrcDir
for ($depth = 0; $depth -lt 4; $depth++) {
if (-not $probe) { break }
if (Test-Path (Join-Path $probe "Configuration.xml")) { $configDir = $probe; break }
$probe = Split-Path $probe
}
$rootXmlLong = Get-LongPath $rootXmlFull.Path
$formMetaFull = Get-LongPath $formMetaPath
$formDirFull = Get-LongPath $formDir
$references = @()
if ($configDir) {
# Полный обход, как в meta-remove: ссылки лежат и внутри Ext/Form.xml (ChoiceForm,
# SettingsStorage), узкий скан по корневым XML их не видит.
# EnumerateFiles, а не Get-ChildItem -Recurse: на ERP (73 904 XML) обход обёртками
# занимает 180 с против 47 с — чтение файлов не узкое место, узкое место перечисление.
$refPattern = '<([A-Za-z0-9_.]+)>' + [regex]::Escape($formRef) + '</'
$scanSw = [System.Diagnostics.Stopwatch]::StartNew()
$scanned = 0
foreach ($fp in [System.IO.Directory]::EnumerateFiles($configDir, "*.xml", [System.IO.SearchOption]::AllDirectories)) {
if ($fp -eq $rootXmlLong) { continue } # свой файл чистится всегда
if ($fp -eq $formMetaFull) { continue } # файлы удаляемой формы
if ($formDirFull -and $fp.StartsWith($formDirFull)) { continue }
$scanned++
$content = [System.IO.File]::ReadAllText($fp, [System.Text.Encoding]::UTF8)
if (-not $content.Contains($formRef)) { continue }
foreach ($m in [regex]::Matches($content, $refPattern)) {
$references += @{ Path = $fp; Rel = $fp.Substring($configDir.Length + 1); Tag = $m.Groups[1].Value }
}
}
$scanSw.Stop()
if ($scanSw.Elapsed.TotalSeconds -ge 5) {
Write-Host "[INFO] Проверено ссылок в $scanned файлах за $([math]::Round($scanSw.Elapsed.TotalSeconds, 1)) c"
}
}
if ($references.Count -gt 0) {
Write-Host "[WARN] На форму $formRef ссылаются $($references.Count) раз(а):"
foreach ($grp in ($references | Group-Object { "$($_.Rel)|$($_.Tag)" } | Sort-Object Name)) {
$parts = $grp.Name.Split("|")
$suffix = if ($grp.Count -gt 1) { " x$($grp.Count)" } else { "" }
Write-Host " $($parts[0]) — <$($parts[1])>$suffix"
}
Write-Host ""
if (-not $Force) {
Write-Host "[ERROR] Удаление остановлено: форма используется."
Write-Host " Решает пользователь: убрать ссылки, отказаться от удаления или"
Write-Host " повторить с -Force — тогда ссылки будут очищены."
exit 1
}
Write-Host "[WARN] -Force: ссылки будут очищены"
Write-Host ""
} elseif (-not $configDir) {
Write-Host "[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены"
}
# --- Удаление файлов ---
if (Test-Path $formDir) {
Remove-Item -Path $formDir -Recurse -Force
Write-Host "[OK] Удалён каталог: $formDir"
}
Remove-Item -Path $formMetaPath -Force
Write-Host "[OK] Удалён файл: $formMetaPath"
# --- Модификация корневого XML ---
# Удалить <Form>FormName</Form> из ChildObjects
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
foreach ($node in $formNodes) {
if ($node.InnerText -eq $FormName) {
Remove-NodeWithIndent $node
break
}
}
# Очистить слоты своего объекта, указывавшие на удалённую форму: Default*/Auxiliary*Form
# (form-add пишет свойство по назначению) и ChoiceForm у реквизитов.
Clear-FormRefs $xmlDoc $formRef | Out-Null
Save-XmlPreservingStyle $xmlDoc $rootXmlFull.Path
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
# --- Чистка ссылок в других файлах (только с -Force) ---
if ($references.Count -gt 0) {
foreach ($grp in ($references | Group-Object { $_.Path } | Sort-Object Name)) {
$path = $grp.Name
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($path)
$touched = @(Clear-FormRefs $doc $formRef)
if ($touched.Count -eq 0) { continue }
Save-XmlPreservingStyle $doc $path
$rel = $path.Substring($configDir.Length + 1)
Write-Host "[OK] Очищена ссылка в $rel$(($touched | Sort-Object -Unique) -join ', ')"
}
}
@@ -0,0 +1,303 @@
#!/usr/bin/env python3
# form-remove v1.10 — Remove form 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
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
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 long_path(path):
"""Полный путь в длинной форме. Зеркало Get-LongPath в PS: там Resolve-Path оставляет
короткое имя 8.3 (NSHIRO~1), а перечисление отдаёт длинное — сравнение молча не совпадало."""
if not os.path.exists(path):
return ""
return os.path.realpath(path)
def remove_node_with_indent(node):
"""Удалить элемент вместе с предшествующим whitespace; опустевший контейнер сделать
самозакрывающимся. Зеркало Remove-NodeWithIndent в PS."""
parent = node.getparent()
if parent is None:
return
# В DOM (PS) whitespace — отдельные узлы: удаляются предшествующий и сам элемент, а
# whitespace ПОСЛЕ элемента остаётся. В lxml он лежит в node.tail и ушёл бы вместе с
# узлом, поэтому его надо передать предшественнику — иначе `</Attributes></Form>`.
prev = node.getprevious()
if prev is not None:
prev.tail = node.tail
else:
parent.text = node.tail
parent.remove(node)
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
if len(parent) == 0 and not (parent.text or "").strip():
parent.text = None
def clear_form_refs(tree, ref):
"""Очистить ссылки на форму. Каноничное «не задано» зависит от файла: в корневом XML
объекта и в Configuration.xml пустой слот штатен (164 508 пустых на корпус), а внутри
Ext/Form.xml пустых <ChoiceForm/> и <SettingsStorage/> нет ни одного — там свойство
просто отсутствует. Зеркало Clear-FormRefs в PS."""
root = tree.getroot()
is_form_file = etree.QName(root).localname == "Form"
touched = []
ref_lc = ref.lower()
for el in list(root.iter()):
if not isinstance(el.tag, str):
continue
if len(el) > 0: # только листья
continue
# Сравнение регистронезависимое — как у платформы (в PS -eq регистр не различает).
if (el.text or "").strip().lower() != ref_lc:
continue
ln = etree.QName(el).localname
parent = el.getparent()
if ln == "Form" and parent is not None and etree.QName(parent).localname == "Item":
touched.append(f"{etree.QName(parent).localname}/{ln}")
remove_node_with_indent(parent)
elif is_form_file:
touched.append(ln)
remove_node_with_indent(el)
else:
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
touched.append(ln)
el.text = None
return touched
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
parser.add_argument("-Force", action="store_true")
args = ci_parse_args(parser)
object_name = args.ObjectName
form_name = args.FormName
src_dir = args.SrcDir
force = args.Force
# --- 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)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
form_dir = os.path.join(forms_dir, form_name)
if not os.path.exists(form_meta_path):
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Load root XML: kind and object name ---
root_xml_full = long_path(root_xml_path) or 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()
type_node = None
for c in root:
if isinstance(c.tag, str):
type_node = c
break
if type_node is None:
print(f"Не удалось определить вид объекта в {root_xml_path}", file=sys.stderr)
sys.exit(1)
md_type = etree.QName(type_node).localname
name_node = type_node.find("md:Properties/md:Name", NSMAP)
obj_meta_name = (name_node.text or "").strip() if name_node is not None else ""
if not obj_meta_name:
obj_meta_name = os.path.splitext(os.path.basename(root_xml_path))[0]
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
form_ref = f"{md_type}.{obj_meta_name}.Form.{form_name}"
# --- Find references across the configuration ---
config_dir = None
probe = long_path(src_dir) or os.path.abspath(src_dir)
for _ in range(4):
if not probe:
break
if os.path.exists(os.path.join(probe, "Configuration.xml")):
config_dir = probe
break
parent_probe = os.path.dirname(probe)
if parent_probe == probe:
break
probe = parent_probe
form_meta_full = long_path(form_meta_path)
form_dir_full = long_path(form_dir)
references = []
if config_dir:
ref_pattern = re.compile(r"<([A-Za-z0-9_.]+)>" + re.escape(form_ref) + r"</")
for dirpath, _dirnames, filenames in os.walk(config_dir):
for fn in filenames:
if not fn.lower().endswith(".xml"):
continue
fp = os.path.join(dirpath, fn)
if fp == root_xml_full or fp == form_meta_full:
continue # свой файл и файлы удаляемой формы
if form_dir_full and fp.startswith(form_dir_full):
continue
try:
with open(fp, "r", encoding="utf-8-sig") as f:
content = f.read()
except OSError:
continue
if form_ref not in content:
continue
for m in ref_pattern.finditer(content):
references.append({"path": fp, "rel": os.path.relpath(fp, config_dir),
"tag": m.group(1)})
if references:
print(f"[WARN] На форму {form_ref} ссылаются {len(references)} раз(а):")
grouped = {}
for r in references:
grouped[(r["rel"], r["tag"])] = grouped.get((r["rel"], r["tag"]), 0) + 1
for (rel, tag) in sorted(grouped):
suffix = f" x{grouped[(rel, tag)]}" if grouped[(rel, tag)] > 1 else ""
print(f" {rel} — <{tag}>{suffix}")
print()
if not force:
print("[ERROR] Удаление остановлено: форма используется.")
print(" Решает пользователь: убрать ссылки, отказаться от удаления или")
print(" повторить с -Force — тогда ссылки будут очищены.")
sys.exit(1)
print("[WARN] -Force: ссылки будут очищены")
print()
elif not config_dir:
print("[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены")
# --- Delete files ---
if os.path.isdir(form_dir):
shutil.rmtree(form_dir)
print(f"[OK] Удалён каталог: {form_dir}")
os.remove(form_meta_path)
print(f"[OK] Удалён файл: {form_meta_path}")
# --- Modify root XML ---
# Remove <Form>FormName</Form> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
if node.text and node.text.strip() == form_name:
remove_node_with_indent(node)
break
# Очистить слоты своего объекта: Default*/Auxiliary*Form и ChoiceForm у реквизитов.
clear_form_refs(tree, form_ref)
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
# --- Clean references in other files (only with -Force) ---
for fp in sorted({r["path"] for r in references}):
other_tree = etree.parse(fp, parser_xml)
touched = clear_form_refs(other_tree, form_ref)
if not touched:
continue
save_xml_with_bom(other_tree, fp)
rel = os.path.relpath(fp, config_dir)
print(f"[OK] Очищена ссылка в {rel}{', '.join(sorted(set(touched)))}")
if __name__ == "__main__":
main()