Files
cc-1c-skills/.claude/skills/template-add/scripts/add-template.ps1
T
Nick ShirokovandClaude Opus 5 dc470a3803 fix(template-add,help-add): закрыть находки ревью по коду языка и странице
help-add терял текст справки. Отказ смотрит только на Help.xml, а страница может
пережить его (удалённый дескриптор, частичная выгрузка, справка, сделанная руками):
тогда запись шла безусловно и стирала содержимое с кодом 0 и рапортом [OK].
Теперь существующая страница сохраняется, создаётся только дескриптор — та же
защита, что уже стоит в template-add. Прежняя оценка «в help-add потери данных нет»
была неверной: она опиралась на чтение кода, а не на замер.

Код языка: регулярка пропускала имена устройств Windows. При -Lang nul py-порт
молча писал <Page>nul</Page> и пустой каталог с кодом 0 (страница уходила в NUL),
а PS падал исключением — то есть порты ещё и расходились. Якоря \A…\z вместо ^…$:
последние в обоих языках допускают перевод строки в конце.

Проверка вынесена в Test-LangCode / is_valid_lang и внесена в реестр
check-inline-drift: inline-блок гард не видел, а копий у него две.

Плюс мелочи оттуда же: мёртвый дизъюнкт в $pageExists убран; полумигрированное
дерево (дескриптор есть, старый Ext/Template.html остался рядом) теперь получает
предупреждение, а не молчание; существование страницы в py сверяется без учёта
регистра — иначе на Linux -Lang RU писал бы вторую страницу мимо дескриптора.

Четыре кейса на каждый сценарий. Существующие эталоны не сдвинулись.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:50:58 +03:00

613 lines
33 KiB
PowerShell
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.
# template-add v1.26 — Add template to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias("ProcessorName")]
[string]$ObjectName,
[Parameter(Mandatory)]
[string]$TemplateName,
[Parameter(Mandatory)]
[ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")]
[string]$TemplateType,
[string]$Synonym = $TemplateName,
[string]$Lang = "ru",
[string]$SrcDir = "src",
[switch]$SetMainSKD
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Маппинг типов ---
$typeMap = @{
"HTML" = @{ TemplateType = "HTMLDocument"; Ext = ".xml" }
"Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" }
"SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" }
"BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" }
"DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" }
}
$tmpl = $typeMap[$TemplateType]
# --- Проверки ---
$objectTypeFolders = @(
"Reports", "DataProcessors", "Documents", "Catalogs",
"InformationRegisters", "AccumulationRegisters",
"ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes",
"BusinessProcesses", "Tasks", "ExchangePlans"
)
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
if (-not (Test-Path $rootXmlPath)) {
$candidates = @()
foreach ($folder in $objectTypeFolders) {
$probe = Join-Path (Join-Path $SrcDir $folder) "$ObjectName.xml"
if (Test-Path $probe) { $candidates += (Join-Path $SrcDir $folder) }
}
if ($candidates.Count -eq 1) {
$SrcDir = $candidates[0]
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
Write-Host "[INFO] SrcDir расширен до: $SrcDir"
} elseif ($candidates.Count -gt 1) {
Write-Error "Объект '$ObjectName' найден в нескольких подпапках: $($candidates -join ', ')`nУкажи SrcDir явно"
exit 1
} else {
Write-Error "Корневой файл объекта не найден: $rootXmlPath`nОжидается: <SrcDir>/<ObjectName>.xml`nПодсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации"
exit 1
}
}
$processorDir = Join-Path $SrcDir $ObjectName
$templatesDir = Join-Path $processorDir "Templates"
$templateMetaPath = Join-Path $templatesDir "$TemplateName.xml"
# Код языка идёт и в текст XML, и в имя файла страницы, поэтому проверяем его до записи:
# пустое значение дало бы файл «.html» и пустой <Page></Page>, разделитель пути — запись мимо
# каталога страниц, а зарезервированное имя устройства (nul, con, prn, aux, com1…, lpt1…) на
# Windows уводит запись в само устройство: py-порт при -Lang nul молча писал <Page>nul</Page>
# и пустой каталог с кодом 0. Все отказы платформы были бы тихими.
#
# Якоря `\A…\z`, а не `^…$`: последние в обоих языках допускают перевод строки в конце.
#
# Копия этой функции есть в help-add (навыки автономны, формат «дескриптор + страница» у них
# общий). Держать копии одинаковыми — сознательно; за дрейфом следит check-inline-drift.
function Test-LangCode([string]$code) {
if ($code -notmatch '\A[A-Za-z0-9_-]+\z') { return $false }
if ($code -match '\A(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])\z') { return $false }
return $true
}
if (-not (Test-LangCode $Lang)) {
Write-Error "Недопустимый код языка: '$Lang'`nОжидается код вида ru, en (буквы, цифры, дефис, подчёркивание; имена устройств Windows недопустимы)"
exit 1
}
# Существующий HTML-макет — не всегда повод отказать: в один макет платформа кладёт
# несколько языков (<Page> на каждый, страницы рядом), и повторный вызов с другим -Lang
# добавляет страницу. Для остальных типов поведение прежнее — отказ.
$addLangMode = $false
if (Test-Path $templateMetaPath) {
$existingType = $null
$metaText = [System.IO.File]::ReadAllText((Resolve-Path $templateMetaPath).Path, [System.Text.Encoding]::UTF8)
if ($metaText -match '<TemplateType>([^<]+)</TemplateType>') { $existingType = $Matches[1] }
if ($TemplateType -eq "HTML" -and $existingType -eq "HTMLDocument") {
$addLangMode = $true
} elseif ($TemplateType -eq "HTML") {
Write-Error "Макет уже существует: $templateMetaPath`nЕго тип — $existingType, страницу на языке можно добавить только к HTML-макету"
exit 1
} else {
Write-Error "Макет уже существует: $templateMetaPath"
exit 1
}
}
Assert-EditAllowed $rootXmlPath 'editable'
# --- Создание каталогов ---
$templateExtDir = Join-Path (Join-Path $templatesDir $TemplateName) "Ext"
New-Item -ItemType Directory -Path $templateExtDir -Force | Out-Null
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
# Скелет HTML-страницы макета — в том же виде, в каком его пишет редактор платформы
# (одной строкой, парный </meta>): первое сохранение в Конфигураторе даст минимальный
# дифф. Нужен в двух местах (новый макет и добавление языка) — поэтому одной переменной.
$htmlSkeleton = @"
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta></head><body>
</body></html>
"@
# --- Detect format version ---
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
$formatVersion = $null
$objHead = [System.IO.File]::ReadAllText((Resolve-Path $rootXmlPath).Path, [System.Text.Encoding]::UTF8)
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $formatVersion = $Matches[1] }
if (-not $formatVersion) { $formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path }
# Объявления пространств имён — одной переменной: место эмиссии её только интерполирует.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $formatVersion) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- 1. Метаданные макета (Templates/<TemplateName>.xml) ---
$templateUuid = [guid]::NewGuid().ToString()
$templateMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject $xmlnsDecl version="$formatVersion">
<Template uuid="$templateUuid">
<Properties>
<Name>$TemplateName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<TemplateType>$($tmpl.TemplateType)</TemplateType>
</Properties>
</Template>
</MetaDataObject>
"@
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# HTML-страница макета сюда НЕ идёт — платформа хранит её с LF.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
# --- 1a. Добавление страницы на другом языке в существующий HTML-макет ---
# Метаданные макета и ChildObjects здесь НЕ трогаем: и то и другое уже на месте,
# перезапись сменила бы UUID. Язык, не объявленный в Languages/ конфигурации, платформа
# принимает и возвращает при выгрузке (проверено на 8.3.27) — состав языков не проверяем.
if ($addLangMode) {
$descPath = Join-Path $templateExtDir "Template.xml"
$pageDir = Join-Path $templateExtDir "Template"
$legacyPagePath = Join-Path $templateExtDir "Template.html"
$langs = @()
$descVersion = $formatVersion
# Сначала РАЗБОР состояния, и только в самом конце запись: иначе отказ на полпути
# оставляет макет разобранным (страница есть, дескриптора нет) — а такую раскладку
# платформа снова молча игнорирует.
$legacyPending = $false
$pagePath = Join-Path $pageDir "$Lang.html"
if (Test-Path $descPath) {
$descText = [System.IO.File]::ReadAllText((Resolve-Path $descPath).Path, [System.Text.Encoding]::UTF8)
# Версию берём из самого дескриптора: правка чужой выгрузки не должна менять формат.
if ($descText -match '<Help[^>]+version="(\d+\.\d+)"') { $descVersion = $Matches[1] }
$langs = @([regex]::Matches($descText, '<Page>([^<]+)</Page>') | ForEach-Object { $_.Groups[1].Value })
# Полумигрированное дерево: дескриптор уже есть, а старый Ext/Template.html остался рядом.
# Платформа его игнорирует, поэтому текст в нём пропадёт незаметно — говорим вслух.
if (Test-Path $legacyPagePath) {
Write-Host "[WARN] Рядом лежит старый Ext/Template.html — платформа его игнорирует."
Write-Host " Перенесите нужное в Template/<язык>.html и удалите его."
}
} elseif (Test-Path $legacyPagePath) {
# Раскладка до v1.24 — одиночный Ext/Template.html, который платформа молча игнорирует.
# Языка у него нет, но создать его могла только версия навыка без параметра -Lang,
# то есть это страница на языке по умолчанию.
if (Test-Path (Join-Path $pageDir "ru.html")) {
Write-Error "Макет разобран: есть и старый Ext/Template.html, и Template/ru.html — что из них актуально, решать не навыку.`nОставьте один файл и повторите."
exit 1
}
$legacyPending = $true
$langs = @("ru")
}
# Файл страницы НИКОГДА не перезаписываем: в нём может лежать текст макета.
# Ошибка — только когда добавлять нечего: язык уже в дескрипторе И страница на диске.
# При миграции проверять нечего: langs там задан нами же, а страница появится переносом.
if (($langs -contains $Lang) -and (Test-Path $pagePath) -and -not $legacyPending) {
Write-Error "Страница макета на языке '$Lang' уже существует: $pagePath"
exit 1
}
# Порядок страниц — по коду языка (в выгрузке ERP так во всех 54 двуязычных макетах).
# Сравнение ordinal, а не культурное: иначе порты разойдутся на ровном месте.
if (-not ($langs -contains $Lang)) { $langs = @($langs + $Lang) }
[Array]::Sort($langs, [System.StringComparer]::Ordinal)
$pagesXml = ($langs | ForEach-Object { "`t<Page>$_</Page>" }) -join "`n"
$descXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<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" version="$descVersion">
$pagesXml
</Help>
"@
# --- запись ---
New-Item -ItemType Directory -Path $pageDir -Force | Out-Null
if ($legacyPending) {
Move-Item -LiteralPath $legacyPagePath -Destination (Join-Path $pageDir "ru.html")
Write-Host "[WARN] Макет был в старой раскладке (Ext/Template.html) — платформа её игнорирует."
Write-Host " Страница считана как ru и перенесена в Template/ru.html."
}
Write-XmlFile $descPath $descXml $encBom
if (Test-Path $pagePath) {
# При миграции про сохранённое содержимое уже сказано выше — не повторяемся.
if (-not $legacyPending) {
Write-Host "[WARN] Страница $Lang.html уже лежала на диске — содержимое сохранено, дописан только <Page>."
}
} else {
[System.IO.File]::WriteAllText($pagePath, ($htmlSkeleton -replace "`r`n", "`n"), $encBom)
}
Write-Host "[OK] Добавлена страница макета: $TemplateName ($Lang)"
Write-Host " Содержимое: $pagePath"
Write-Host " Дескриптор: $descPath"
exit 0
}
Write-XmlFile $templateMetaPath $templateMetaXml $encBom
# --- 2. Содержимое макета (Templates/<TemplateName>/Ext/Template.<ext>) ---
$templateFilePath = Join-Path $templateExtDir "Template$($tmpl.Ext)"
# Куда класть текст макета. Совпадает с $templateFilePath у всех типов, кроме HTML:
# там содержимое живёт в отдельной странице, а Template.xml — только дескриптор.
$templateBodyPath = $templateFilePath
switch ($TemplateType) {
"HTML" {
# HTML-макет платформа хранит парой, как справку: дескриптор Ext/Template.xml
# со списком страниц и сама страница Ext/Template/<язык>.html (картинки —
# рядом в _files/). Одиночный Ext/Template.html платформа молча игнорирует:
# загрузка проходит без ошибок, а макет в базе пустой.
$pageXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<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" version="$formatVersion">
<Page>$Lang</Page>
</Help>
"@
Write-XmlFile $templateFilePath $pageXml $encBom
$pageDir = Join-Path $templateExtDir "Template"
New-Item -ItemType Directory -Path $pageDir -Force | Out-Null
$templateBodyPath = Join-Path $pageDir "$Lang.html"
# Страница — с LF: платформа хранит HTML именно так.
$content = ($htmlSkeleton -replace "`r`n", "`n")
[System.IO.File]::WriteAllText($templateBodyPath, $content, $encBom)
}
"Text" {
[System.IO.File]::WriteAllText($templateFilePath, "", $encBom)
}
"SpreadsheetDocument" {
# Пустой макет — самозакрывающимся корнем: пустых пар платформа не пишет
# ни в одной форме (0 на 65 040 XML выгрузки acc_8.3.27, включая разнесённые
# по строкам). Для XML `<A/>` и `<A></A>` тождественны по спецификации.
$content = @"
<?xml version="1.0" encoding="UTF-8"?>
<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document" xmlns:ss="http://v8.1c.ru/spreadsheet/document" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xs="http://www.w3.org/2001/XMLSchema"/>
"@
Write-XmlFile $templateFilePath $content $encBom
}
"BinaryData" {
[System.IO.File]::WriteAllBytes($templateFilePath, @())
}
"DataCompositionSchema" {
$content = @"
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
</DataCompositionSchema>
"@
Write-XmlFile $templateFilePath $content $encBom
}
}
# --- 3. Модификация корневого 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")
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
exit 1
}
# Добавить <Template> в конец ChildObjects — идемпотентно (не дублировать уже зарегистрированный)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Template[text()='$TemplateName']", $nsMgr)
if (-not $alreadyRegistered) {
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
$templateElem.InnerText = $TemplateName
if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($templateElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} else {
$lastChild = $childObjects.LastChild
# Вставить перед закрывающим whitespace (если есть), или в конец
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($templateElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($templateElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
}
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
$mainDCSUpdated = $false
if ($TemplateType -eq "DataCompositionSchema") {
# Определяем корневой элемент объекта
$reportLikeTypes = @("ExternalReport", "Report")
$objectTypeNode = $null
$objectTypeName = $null
foreach ($rt in $reportLikeTypes) {
$node = $xmlDoc.SelectSingleNode("//md:$rt", $nsMgr)
if ($node) {
$objectTypeNode = $node
$objectTypeName = $rt
break
}
}
if ($objectTypeNode) {
$mainDCS = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:MainDataCompositionSchema", $nsMgr)
if ($mainDCS) {
$isEmpty = [string]::IsNullOrWhiteSpace($mainDCS.InnerText)
if ($isEmpty -or $SetMainSKD) {
$objName = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:Name", $nsMgr).InnerText
$mainDCS.InnerText = "$objectTypeName.$objName.Template.$TemplateName"
$mainDCSUpdated = $true
}
}
}
}
# Сохранить с BOM
$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)
$xmlDoc.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 $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
if ($alreadyRegistered) {
Write-Host " Already registered: <Template>$TemplateName</Template> in ChildObjects (skipped duplicate)"
}
Write-Host " Метаданные: $templateMetaPath"
Write-Host " Содержимое: $templateBodyPath"
if ($TemplateType -eq "HTML") {
Write-Host " Дескриптор: $templateFilePath"
}
if ($mainDCSUpdated) {
Write-Host " MainDataCompositionSchema: $($mainDCS.InnerText)"
}