From 0b8231c467c42c121885c6a066034f8bcb82557f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 5 Aug 2026 15:37:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(17=20=D0=BD=D0=B0=D0=B2=D1=8B=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2):=20CRLF=20=D0=B2=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8F=D1=85=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20XML=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Головной дефект тикета: cfe-init выдавал Configuration.xml с 10 CR на 70 строк, а роль и язык — вовсе без CR. Теперь 70/70, последний байт `>`. Правило: меняем только РАЗДЕЛИТЕЛИ строк, содержимое текстовых узлов не трогаем. Платформа его не трогает тоже — в запросах СКД из чистой выгрузки встречаются и CRLF, и одиночный LF. Поэтому сплошная нормализация файла применяется только там, где многострочных текстовых узлов нет по построению (скелеты), а в объектном XML и формах разделители правятся в точке сборки. Источников оказалось четыре, а не один: 1. Скелеты (cf-init, cfe-init, epf-init, erf-init, form-add, help-add, template-add) собираются here-string'ами, а .ps1/.py в репозитории хранятся с LF — отсюда LF и смешанный EOL. 2. py-порты склеивали документ через '\n'.join(lines); PS в тех же местах давал CRLF через AppendLine — порты расходились побайтово. 3. Билдеры Predefined в meta-compile собирались с явным LF и даже сворачивали CRLF→LF из общего эмиттера типов. 4. Чтение существующего файла в python БЕЗ newline='' молча схлопывает CRLF в LF (универсальные переводы строк), и запись потом кладёт LF. Так role-compile и subsystem-compile переписывали в LF весь Configuration.xml. meta-compile это уже делал правильно — там newline='' стоял с фикса #44/#46/#47. Отдельно: XML-парсер по спецификации схлопывает CRLF при разборе, поэтому lxml-порты (xdto-compile, xdto-edit) отдавали LF-документ там, где .NET возвращал CRLF через NewLineHandling. Восстанавливаем EOL исходного файла. Разделение канона и сохранения стиля: - файл СОЗДАЁМ — канон (CRLF, без хвоста); - существующий ПРАВИМ — наследуем его EOL, включая перевод строки вставки (контракт #44/#46/#47). Иначе LF-проект получал бы смешанные файлы — ровно то, на что заведён #57. Кейсы roundtrip-crlf-preserve остаются зелёными. Хелпер записи скопирован в каждый навык (навыки автономны). У meta-compile он называется Write-XmlFileKeepEol / write_xml_file_keep_eol: там нормализовать EOL НЕЛЬЗЯ (многострочные запрос, синоним, значение заполнения), и одинаковое имя при разном поведении было бы ловушкой. Заодно: имя временного файла батча в meta-compile.ps1 получило GUID. Фиксированное "meta-compile-batch-$idx.json" в общем %TEMP% сталкивало два параллельных запуска («file is being used by another process») — из-за этого полный набор приходилось гонять с урезанной параллельностью. py-порт уже брал mkstemp. Аудит: было ~250 файлов с дефектом EOL, стало 0 в обоих портах. Осталось четыре законных случая — фикстуры roundtrip-crlf-preserve (сохранение стиля) и запрос динсписка с многострочным текстом. Дрейф снэпшотов — 1 файл. Тесты 641/641. Co-Authored-By: Claude Opus 5 --- .claude/skills/cf-edit/scripts/cf-edit.ps1 | 15 +++++-- .claude/skills/cf-edit/scripts/cf-edit.py | 2 +- .claude/skills/cf-init/scripts/cf-init.ps1 | 17 ++++++-- .claude/skills/cf-init/scripts/cf-init.py | 15 +++++-- .claude/skills/cfe-init/scripts/cfe-init.ps1 | 17 ++++++-- .claude/skills/cfe-init/scripts/cfe-init.py | 15 +++++-- .claude/skills/epf-init/scripts/init.ps1 | 13 +++++- .claude/skills/epf-init/scripts/init.py | 11 ++++- .claude/skills/erf-init/scripts/init.ps1 | 17 ++++++-- .claude/skills/erf-init/scripts/init.py | 15 +++++-- .claude/skills/form-add/scripts/form-add.ps1 | 15 +++++-- .claude/skills/form-add/scripts/form-add.py | 20 +++++++-- .../form-compile/scripts/form-compile.ps1 | 2 +- .../form-compile/scripts/form-compile.py | 15 ++++--- .claude/skills/help-add/scripts/add-help.ps1 | 13 +++++- .claude/skills/help-add/scripts/add-help.py | 2 +- .../meta-compile/scripts/meta-compile.ps1 | 42 +++++++++++-------- .../meta-compile/scripts/meta-compile.py | 42 ++++++++++++------- .../skills/meta-edit/scripts/meta-edit.ps1 | 5 ++- .claude/skills/meta-edit/scripts/meta-edit.py | 2 +- .../mxl-compile/scripts/mxl-compile.ps1 | 2 +- .../skills/mxl-compile/scripts/mxl-compile.py | 4 +- .../role-compile/scripts/role-compile.ps1 | 2 +- .../role-compile/scripts/role-compile.py | 22 +++++++--- .../skd-compile/scripts/skd-compile.ps1 | 2 +- .../skills/skd-compile/scripts/skd-compile.py | 4 +- .../scripts/subsystem-compile.ps1 | 2 +- .../scripts/subsystem-compile.py | 21 +++++++--- .../subsystem-edit/scripts/subsystem-edit.ps1 | 2 +- .../subsystem-edit/scripts/subsystem-edit.py | 4 +- .../template-add/scripts/add-template.ps1 | 17 ++++++-- .../template-add/scripts/add-template.py | 2 +- .../xdto-compile/scripts/xdto-compile.ps1 | 2 +- .../xdto-compile/scripts/xdto-compile.py | 8 +++- .../skills/xdto-edit/scripts/xdto-edit.ps1 | 2 +- .claude/skills/xdto-edit/scripts/xdto-edit.py | 13 +++++- .../Catalogs/Спр/Ext/Predefined.xml | 2 +- 37 files changed, 289 insertions(+), 117 deletions(-) diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1 index 3974da06..6d854bcb 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.ps1 +++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1 @@ -1,4 +1,4 @@ -# cf-edit v1.12 — Edit 1C configuration root (Configuration.xml) +# cf-edit v1.14 — Edit 1C configuration root (Configuration.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, @@ -163,6 +163,11 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) { Assert-EditAllowed $resolvedPath 'editable' # --- Load XML with PreserveWhitespace --- +# EOL исходного файла запоминаем ДО разбора: парсер XML по спецификации схлопывает +# CRLF в LF, а вставки ниже собираются с явным CRLF — без восстановления в точке +# записи LF-файл стал бы смешанным. Канон CRLF относится к файлам, которые мы +# СОЗДАЁМ; правка существующего сохраняет его стиль (#44/#46/#47). +$script:srcEol = if (([System.IO.File]::ReadAllText($resolvedPath)) -match "`r`n") { "`r`n" } else { "`n" } $script:xmlDoc = New-Object System.Xml.XmlDocument $script:xmlDoc.PreserveWhitespace = $true $script:xmlDoc.Load($resolvedPath) @@ -691,7 +696,8 @@ $bodyBlock$declarations if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } $caiPath = Join-Path $extDir "ClientApplicationInterface.xml" $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom) + # Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце. + [System.IO.File]::WriteAllText($caiPath, (($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n").TrimEnd("`r", "`n"), $utf8Bom) $script:modifyCount++ Info "Wrote panel layout: $caiPath" } @@ -880,7 +886,8 @@ $rightXml if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } $hpPath = Join-Path $extDir "HomePageWorkArea.xml" $utf8Bom = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom) + # Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце. + [System.IO.File]::WriteAllText($hpPath, (($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n").TrimEnd("`r", "`n"), $utf8Bom) $script:modifyCount++ Info "Wrote home page layout: $hpPath" } @@ -985,6 +992,8 @@ $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') # Пустой элемент: XmlWriter пишет ``, Конфигуратор — ``. Гард на CDATA/комментарии: # только там `>` не экранируется, и ` />` может быть содержимым, а не концом тега. if ($text -notmatch '', '/>') } +# Возвращаем EOL исходного файла: вставки собраны с CRLF, а сам документ мог быть LF. +$text = ($text -replace "`r`n", "`n") -replace "`n", $script:srcEol $utf8Bom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py index 4e0c11ec..6f3aff77 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.py +++ b/.claude/skills/cf-edit/scripts/cf-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cf-edit v1.12 — Edit 1C configuration root (Configuration.xml) +# cf-edit v1.14 — Edit 1C configuration root (Configuration.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/.claude/skills/cf-init/scripts/cf-init.ps1 b/.claude/skills/cf-init/scripts/cf-init.ps1 index fb7f0293..9164f8f3 100644 --- a/.claude/skills/cf-init/scripts/cf-init.ps1 +++ b/.claude/skills/cf-init/scripts/cf-init.ps1 @@ -1,4 +1,4 @@ -# cf-init v1.4 — Create empty 1C configuration scaffold +# cf-init v1.5 — Create empty 1C configuration scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -240,11 +240,20 @@ if (-not (Test-Path $extDir)) { # --- Write files with UTF-8 BOM --- $enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile $cfgFile $cfgXml $enc $langFile = Join-Path $langDir "Русский.xml" -[System.IO.File]::WriteAllText($langFile, $langXml, $enc) +Write-XmlFile $langFile $langXml $enc $caiFile = Join-Path $extDir "ClientApplicationInterface.xml" -[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) +Write-XmlFile $caiFile $caiXml $enc # --- Output --- Write-Host "[OK] Создана конфигурация: $Name" diff --git a/.claude/skills/cf-init/scripts/cf-init.py b/.claude/skills/cf-init/scripts/cf-init.py index 150081ee..eae86c7e 100644 --- a/.claude/skills/cf-init/scripts/cf-init.py +++ b/.claude/skills/cf-init/scripts/cf-init.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cf-init v1.4 — Create empty 1C configuration scaffold +# cf-init v1.5 — Create empty 1C configuration scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Generates minimal XML source files for a 1C configuration.""" import sys, os, argparse, uuid @@ -14,6 +14,13 @@ def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) +def write_xml_file(path, content): + # Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце + # файла. Скелет собирается тройными кавычками, а .py хранится с LF — отсюда LF и + # смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в + # скелете нет. Модули .bsl и текстовые макеты через неё НЕ пишутся. + text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n') + write_utf8_bom(path, text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -222,11 +229,11 @@ def main(): os.makedirs(ext_dir, exist_ok=True) # --- Write files --- - write_utf8_bom(cfg_file, cfg_xml) + write_xml_file(cfg_file, cfg_xml) lang_file = os.path.join(lang_dir, "Русский.xml") - write_utf8_bom(lang_file, lang_xml) + write_xml_file(lang_file, lang_xml) cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml") - write_utf8_bom(cai_file, cai_xml) + write_xml_file(cai_file, cai_xml) print(f"[OK] Создана конфигурация: {name}") print(f" Каталог: {output_dir}") diff --git a/.claude/skills/cfe-init/scripts/cfe-init.ps1 b/.claude/skills/cfe-init/scripts/cfe-init.ps1 index 6860f97e..d68b1a4a 100644 --- a/.claude/skills/cfe-init/scripts/cfe-init.ps1 +++ b/.claude/skills/cfe-init/scripts/cfe-init.ps1 @@ -1,4 +1,4 @@ -# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) +# cfe-init v1.3 — Create 1C configuration extension scaffold (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -252,9 +252,18 @@ if (-not (Test-Path $langDir)) { # --- Write files with UTF-8 BOM --- $enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile $cfgFile $cfgXml $enc $langFile = Join-Path $langDir "Русский.xml" -[System.IO.File]::WriteAllText($langFile, $langXml, $enc) +Write-XmlFile $langFile $langXml $enc # --- Role --- if (-not $NoRole) { @@ -263,7 +272,7 @@ if (-not $NoRole) { New-Item -ItemType Directory -Path $roleDir -Force | Out-Null } $roleFile = Join-Path $roleDir "$roleName.xml" - [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) + Write-XmlFile $roleFile $roleXml $enc } # --- Output --- diff --git a/.claude/skills/cfe-init/scripts/cfe-init.py b/.claude/skills/cfe-init/scripts/cfe-init.py index f5aeb90b..35f7569c 100644 --- a/.claude/skills/cfe-init/scripts/cfe-init.py +++ b/.claude/skills/cfe-init/scripts/cfe-init.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) +# cfe-init v1.3 — Create 1C configuration extension scaffold (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Generates minimal XML source files for a 1C configuration extension.""" import sys, os, argparse, uuid @@ -15,6 +15,13 @@ def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) +def write_xml_file(path, content): + # Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце + # файла. Скелет собирается тройными кавычками, а .py хранится с LF — отсюда LF и + # смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в + # скелете нет. Модули .bsl и текстовые макеты через неё НЕ пишутся. + text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n') + write_utf8_bom(path, text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -229,9 +236,9 @@ def main(): os.makedirs(lang_dir, exist_ok=True) # --- Write files --- - write_utf8_bom(cfg_file, cfg_xml) + write_xml_file(cfg_file, cfg_xml) lang_file = os.path.join(lang_dir, "Русский.xml") - write_utf8_bom(lang_file, lang_xml) + write_xml_file(lang_file, lang_xml) # --- Role --- role_file = None @@ -239,7 +246,7 @@ def main(): role_dir = os.path.join(output_dir, "Roles") os.makedirs(role_dir, exist_ok=True) role_file = os.path.join(role_dir, f"{role_name}.xml") - write_utf8_bom(role_file, role_xml) + write_xml_file(role_file, role_xml) # --- Output --- print(f"[OK] Создано расширение: {name}") diff --git a/.claude/skills/epf-init/scripts/init.ps1 b/.claude/skills/epf-init/scripts/init.ps1 index 611659b4..d7c5f926 100644 --- a/.claude/skills/epf-init/scripts/init.ps1 +++ b/.claude/skills/epf-init/scripts/init.ps1 @@ -1,4 +1,4 @@ -# epf-init v1.1 — Init 1C external data processor scaffold +# epf-init v1.2 — Init 1C external data processor scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -64,7 +64,16 @@ $extDir = Join-Path $processorDir "Ext" New-Item -ItemType Directory -Path $extDir -Force | Out-Null $enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc # --- Модуль объекта --- diff --git a/.claude/skills/epf-init/scripts/init.py b/.claude/skills/epf-init/scripts/init.py index 3f532727..5f6ba8c7 100644 --- a/.claude/skills/epf-init/scripts/init.py +++ b/.claude/skills/epf-init/scripts/init.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# epf-init v1.1 — Init 1C external data processor scaffold +# epf-init v1.2 — Init 1C external data processor scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Generates minimal XML source files for a 1C external data processor.""" import sys, os, argparse, uuid @@ -14,6 +14,13 @@ def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) +def write_xml_file(path, content): + # Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце + # файла. Скелет собирается тройными кавычками, а .py хранится с LF — отсюда LF и + # смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в + # скелете нет. Модули .bsl и текстовые макеты через неё НЕ пишутся. + text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n') + write_utf8_bom(path, text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -72,7 +79,7 @@ def main(): ext_dir = os.path.join(processor_dir, "Ext") os.makedirs(ext_dir, exist_ok=True) - write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) + write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) # --- Модуль объекта --- module_bsl = """\ diff --git a/.claude/skills/erf-init/scripts/init.ps1 b/.claude/skills/erf-init/scripts/init.ps1 index 572e940f..ed03e930 100644 --- a/.claude/skills/erf-init/scripts/init.ps1 +++ b/.claude/skills/erf-init/scripts/init.ps1 @@ -1,4 +1,4 @@ -# erf-init v1.1 — Init 1C external report scaffold +# erf-init v1.2 — Init 1C external report scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -98,7 +98,16 @@ $extDir = Join-Path $reportDir "Ext" New-Item -ItemType Directory -Path $extDir -Force | Out-Null $enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc # --- Модуль объекта --- @@ -153,7 +162,7 @@ if ($WithSKD) { "@ - [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) + Write-XmlFile $skdMetaPath $skdMetaXml $enc $skdContent = @" @@ -173,7 +182,7 @@ if ($WithSKD) { "@ $skdFilePath = Join-Path $skdExtDir "Template.xml" - [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) + Write-XmlFile $skdFilePath $skdContent $enc Write-Host " СКД: $skdMetaPath" Write-Host " Тело: $skdFilePath" diff --git a/.claude/skills/erf-init/scripts/init.py b/.claude/skills/erf-init/scripts/init.py index 9941c74e..315b8b44 100644 --- a/.claude/skills/erf-init/scripts/init.py +++ b/.claude/skills/erf-init/scripts/init.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# erf-init v1.1 — Init 1C external report scaffold +# erf-init v1.2 — Init 1C external report scaffold # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Generates minimal XML source files for a 1C external report.""" import sys, os, argparse, uuid @@ -14,6 +14,13 @@ def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) +def write_xml_file(path, content): + # Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце + # файла. Скелет собирается тройными кавычками, а .py хранится с LF — отсюда LF и + # смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в + # скелете нет. Модули .bsl и текстовые макеты через неё НЕ пишутся. + text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n') + write_utf8_bom(path, text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -90,7 +97,7 @@ def main(): ext_dir = os.path.join(report_dir, "Ext") os.makedirs(ext_dir, exist_ok=True) - write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) + write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) # --- Модуль объекта --- module_bsl = """\ @@ -140,7 +147,7 @@ def main(): \t ''' - write_utf8_bom(skd_meta_path, skd_meta_xml) + write_xml_file(skd_meta_path, skd_meta_xml) skd_content = ''' ''' skd_file_path = os.path.join(skd_ext_dir, "Template.xml") - write_utf8_bom(skd_file_path, skd_content) + write_xml_file(skd_file_path, skd_content) print(f" СКД: {skd_meta_path}") print(f" Тело: {skd_file_path}") diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index b30c9bb5..e78d460c 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -1,4 +1,4 @@ -# form-add v1.14 — Add managed form to 1C config object +# form-add v1.16 — Add managed form to 1C config object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -337,7 +337,16 @@ $formMetaXml = @" "@ -[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml.TrimEnd("`r", "`n"), $encBom) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile $formMetaPath $formMetaXml $encBom # --- 3b. Form.xml --- @@ -444,7 +453,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") { if (Test-Path $formXmlPath) { Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting" } else { - [System.IO.File]::WriteAllText($formXmlPath, $formXml.TrimEnd("`r", "`n"), $encBom) + Write-XmlFile $formXmlPath $formXml $encBom } # --- 3c. Module.bsl --- diff --git a/.claude/skills/form-add/scripts/form-add.py b/.claude/skills/form-add/scripts/form-add.py index de54d9db..2656d275 100644 --- a/.claude/skills/form-add/scripts/form-add.py +++ b/.claude/skills/form-add/scripts/form-add.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-add v1.14 — Add managed form to 1C config object +# form-add v1.16 — Add managed form to 1C config object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -259,10 +259,22 @@ def save_xml_with_bom(tree, path): 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: + # newline="" => без трансляции: в текстовом режиме Python на Windows превратил + # бы \n в \r\n, а на macOS оставил \n — вывод зависел бы от ОС. + with open(path, "w", encoding="utf-8-sig", newline="") as f: f.write(text) +def write_xml_file(path, text): + """XML в каноне выгрузки Конфигуратора: CRLF, без перевода строки в конце. + + Скелет формы собирается литералами с \n. Сплошная нормализация здесь + безопасна: многострочных текстовых узлов в скелете нет. Модуль .bsl + пишется отдельно, через write_text_with_bom. + """ + write_text_with_bom(path, text.replace("\r\n", "\n").replace("\n", "\r\n").rstrip("\r\n")) + + def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -428,7 +440,7 @@ def main(): '' ) - write_text_with_bom(form_meta_path, form_meta_xml) + write_xml_file(form_meta_path, form_meta_xml) # --- 3b. Form.xml --- @@ -551,7 +563,7 @@ def main(): if os.path.exists(form_xml_path): print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting") else: - write_text_with_bom(form_xml_path, form_xml) + write_xml_file(form_xml_path, form_xml) # --- 3c. Module.bsl --- diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 2192d4fb..77643cba 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.178 — Compile 1C managed form from JSON or object metadata +# form-compile v1.180 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 731213d8..fde819af 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.178 — Compile 1C managed form from JSON or object metadata +# form-compile v1.180 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -6580,7 +6580,7 @@ def main(): if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) - content = '\n'.join(lines) + content = '\r\n'.join(lines) write_utf8_bom(out_path, content) # --- 4. Auto-register form in parent object XML --- @@ -6598,17 +6598,22 @@ def main(): if forms_leaf == 'Forms': object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') if os.path.exists(object_xml_path): - with open(object_xml_path, 'r', encoding='utf-8-sig') as f: + # newline='' => без трансляции переводов строк: иначе CRLF молча + # схлопнется в LF при чтении и файл будет переписан в LF (#44/#46/#47). + with open(object_xml_path, 'r', encoding='utf-8-sig', newline='') as f: raw_text = f.read() + # Перевод строки вставки берём из самого файла, а не из канона: + # правка существующего файла сохраняет его стиль. + eol = '\r\n' if '\r\n' in raw_text else '\n' # Check if already registered if f'
{form_name}
' not in raw_text: # Insert before if '' in raw_text: - insert_line = f'\t\t\t
{form_name}
\n' + insert_line = f'\t\t\t
{form_name}
' + eol raw_text = raw_text.replace('', insert_line + '\t\t', 1) elif '' in raw_text: - replacement = f'\n\t\t\t
{form_name}
\n\t\t
' + replacement = ('' + eol + f'\t\t\t
{form_name}
' + eol + '\t\t
') raw_text = raw_text.replace('', replacement, 1) write_utf8_bom(object_xml_path, raw_text) diff --git a/.claude/skills/help-add/scripts/add-help.ps1 b/.claude/skills/help-add/scripts/add-help.ps1 index 14f411bc..398aed60 100644 --- a/.claude/skills/help-add/scripts/add-help.ps1 +++ b/.claude/skills/help-add/scripts/add-help.ps1 @@ -1,4 +1,4 @@ -# help-add v1.11 — Add built-in help to 1C object +# help-add v1.12 — Add built-in help to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -195,7 +195,16 @@ $helpXml = @" "@ -[System.IO.File]::WriteAllText($helpXmlPath, $helpXml.TrimEnd("`r", "`n"), $encBom) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile $helpXmlPath $helpXml $encBom # --- 2. Help/.html --- diff --git a/.claude/skills/help-add/scripts/add-help.py b/.claude/skills/help-add/scripts/add-help.py index 7e05c670..3d961288 100644 --- a/.claude/skills/help-add/scripts/add-help.py +++ b/.claude/skills/help-add/scripts/add-help.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# add-help v1.11 — Add built-in help to 1C object +# add-help v1.12 — Add built-in help to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 5ac31312..dcb7badf 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,4 @@ -# meta-compile v1.78 — Compile 1C metadata object from JSON +# meta-compile v1.80 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -161,7 +161,10 @@ if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq ' $idx = 0 foreach ($item in $def) { $idx++ - $tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json" + # Имя с GUID, а не "batch-$idx": фиксированное имя в общем %TEMP% сталкивало + # два параллельных запуска навыка на одной машине — Set-Content падал с + # «file is being used by another process». py-порт уже брал mkstemp. + $tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx-$([guid]::NewGuid().ToString('N')).json" try { $item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru @@ -4611,7 +4614,7 @@ function Build-PredefinedXml { [void]$sb.Append("`n") foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType } [void]$sb.Append("`n") - return $sb.ToString() + return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n" } # --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/ @@ -4699,7 +4702,7 @@ function Build-PredefinedAccountXml { [void]$sb.Append("`n") foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef } [void]$sb.Append("`n") - return $sb.ToString() + return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n" } # --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]" @@ -4726,7 +4729,7 @@ function Build-PredefinedCalcTypeXml { [void]$sb.Append("`n") foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" } [void]$sb.Append("`n") - return $sb.ToString() + return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n" } $extDir = Join-Path $objSubDir "Ext" @@ -4743,13 +4746,18 @@ if ($objType -notin $typesNoSubDir) { $enc = New-Object System.Text.UTF8Encoding($true) # Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла — -# последний байт `>`; сборка через AppendLine (и через явный `n в билдерах -# Predefined/Content) добавляла лишний. Модули .bsl сюда НЕ идут: у них свой хвост. -function Write-XmlFile([string]$path, [string]$text, $encoding) { +# последний байт `>`; сборка через AppendLine добавляла лишний. +# KeepEol в имени — отличие от одноимённой функции в скелетных навыках +# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать +# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос, +# синоним, значение заполнения), и сплошная нормализация меняла бы содержимое. +# Разделители тут и так CRLF: документ собран через AppendLine. +# Модули .bsl сюда НЕ идут: у них свой хвост. +function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) { [System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding) } -Write-XmlFile $mainXmlPath $metadataXml $enc +Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc # Module files $modulesCreated = @() @@ -4830,8 +4838,8 @@ if ($objType -eq "CommonForm") { $cfFormXmlPath = Join-Path $extDir "Form.xml" if (-not (Test-Path $cfFormXmlPath)) { $cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" 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: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: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"' - $cfFormXml = "`n
`n`t`n`t`ttrue`n`t`n`t`n`n" - Write-XmlFile $cfFormXmlPath $cfFormXml $enc + $cfFormXml = "`r`n
`r`n`t`r`n`t`ttrue`r`n`t`r`n`t`r`n`r`n" + Write-XmlFileKeepEol $cfFormXmlPath $cfFormXml $enc $modulesCreated += $cfFormXmlPath } $cfModuleDir = Join-Path $extDir "Form" @@ -4884,7 +4892,7 @@ if ($objType -eq "ExchangePlan") { [void]$sbC.Append("`t`r`n") } [void]$sbC.Append("`r`n") - Write-XmlFile $contentPath $sbC.ToString() $enc + Write-XmlFileKeepEol $contentPath $sbC.ToString() $enc $modulesCreated += $contentPath } elseif (-not (Test-Path $contentPath)) { # При пустом составе платформа всё равно пишет пустой — проверено на @@ -4892,7 +4900,7 @@ if ($objType -eq "ExchangePlan") { # (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.) Ensure-ExtDir $contentXml = "`r`n`r`n" - Write-XmlFile $contentPath $contentXml $enc + Write-XmlFileKeepEol $contentPath $contentXml $enc $modulesCreated += $contentPath } } @@ -4901,7 +4909,7 @@ if ($objType -eq "BusinessProcess") { if (-not (Test-Path $flowchartPath)) { Ensure-ExtDir $flowchartXml = "`r`n`r`n" - Write-XmlFile $flowchartPath $flowchartXml $enc + Write-XmlFileKeepEol $flowchartPath $flowchartXml $enc $modulesCreated += $flowchartPath } } @@ -4916,20 +4924,20 @@ if ($objType -eq 'ChartOfAccounts' -and $def.predefined -and @($def.predefined). $edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' } $predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef $predefPath = Join-Path $extDir "Predefined.xml" - Write-XmlFile $predefPath $predefXml $enc + Write-XmlFileKeepEol $predefPath $predefXml $enc $modulesCreated += $predefPath } elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) { Ensure-ExtDir $predefXml = Build-PredefinedCalcTypeXml @($def.predefined) $predefPath = Join-Path $extDir "Predefined.xml" - Write-XmlFile $predefPath $predefXml $enc + Write-XmlFileKeepEol $predefPath $predefXml $enc $modulesCreated += $predefPath } elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) { Ensure-ExtDir $catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' } $predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType $predefPath = Join-Path $extDir "Predefined.xml" - Write-XmlFile $predefPath $predefXml $enc + Write-XmlFileKeepEol $predefPath $predefXml $enc $modulesCreated += $predefPath } diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 8179cf97..402a437c 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-compile v1.78 — Compile 1C metadata object from JSON +# meta-compile v1.80 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -209,6 +209,16 @@ def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) +def write_xml_file_keep_eol(path, content): + # Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла — + # последний байт `>`. + # keep_eol в имени — отличие от одноимённой функции в скелетных навыках + # (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать + # НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос, + # синоним, значение заполнения). Разделители и так CRLF — их даёт join строк. + # Модули .bsl сюда НЕ идут: у них свой хвост. + write_utf8_bom(path, content.rstrip('\r\n')) + # --------------------------------------------------------------------------- # XML builder (lines list) # --------------------------------------------------------------------------- @@ -4360,7 +4370,7 @@ if obj_type == 'WebService': X(f'\t') X('') -metadata_xml = '\n'.join(lines) +metadata_xml = '\r\n'.join(lines) # --------------------------------------------------------------------------- # 16. Write files @@ -4422,7 +4432,7 @@ os.makedirs(type_dir, exist_ok=True) if obj_type not in types_no_sub_dir: os.makedirs(obj_sub_dir, exist_ok=True) -write_utf8_bom(main_xml_path, metadata_xml) +write_xml_file_keep_eol(main_xml_path, metadata_xml) # Module files modules_created = [] @@ -4498,10 +4508,10 @@ if obj_type == 'CommonForm': '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: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"') - cf_form_xml = ('\n
\n' - '\t\n\t\ttrue\n\t\n' - '\t\n\n') - write_utf8_bom(cf_form_xml_path, cf_form_xml) + cf_form_xml = ('\r\n
\r\n' + '\t\r\n\t\ttrue\r\n\t\r\n' + '\t\r\n\r\n') + write_xml_file_keep_eol(cf_form_xml_path, cf_form_xml) modules_created.append(cf_form_xml_path) cf_module_dir = os.path.join(ext_dir, 'Form') os.makedirs(cf_module_dir, exist_ok=True) @@ -4592,7 +4602,7 @@ def build_predefined_xml(items, xsi_type, code_type): for it in items: emit_predef_item(out, it, '\t', code_type) out.append('') - return '\n'.join(out) + '\n' + return '\r\n'.join(out) # --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/ # ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. --- @@ -4690,7 +4700,7 @@ def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_na for it in items: emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref) out.append('') - return '\n'.join(out) + '\n' + return '\r\n'.join(out) # Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). def emit_predef_calc_type(out, val, indent): @@ -4712,7 +4722,7 @@ def build_predefined_calc_type_xml(items): for it in items: emit_predef_calc_type(out, it, '\t') out.append('') - return '\n'.join(out) + '\n' + return '\r\n'.join(out) # Special files # --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`: @@ -4766,7 +4776,7 @@ if obj_type == 'ExchangePlan': parts.append(f'\t\t{it["autoRecord"]}\r\n') parts.append('\t\r\n') parts.append('
\r\n') - write_utf8_bom(content_path, ''.join(parts)) + write_xml_file_keep_eol(content_path, ''.join(parts)) modules_created.append(content_path) elif not os.path.isfile(content_path): # При пустом составе платформа всё равно пишет пустой — проверено на @@ -4774,7 +4784,7 @@ if obj_type == 'ExchangePlan': # (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.) ensure_ext_dir() content_xml = f'\r\n\r\n' - write_utf8_bom(content_path, content_xml) + write_xml_file_keep_eol(content_path, content_xml) modules_created.append(content_path) if obj_type == 'BusinessProcess': @@ -4782,7 +4792,7 @@ if obj_type == 'BusinessProcess': if not os.path.isfile(flowchart_path): ensure_ext_dir() flowchart_xml = f'\r\n\r\n' - write_utf8_bom(flowchart_path, flowchart_xml) + write_xml_file_keep_eol(flowchart_path, flowchart_xml) modules_created.append(flowchart_path) # Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу. @@ -4795,20 +4805,20 @@ if obj_type == 'ChartOfAccounts' and defn.get('predefined'): edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else '' predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref) predef_path = os.path.join(ext_dir, 'Predefined.xml') - write_utf8_bom(predef_path, predef_xml) + write_xml_file_keep_eol(predef_path, predef_xml) modules_created.append(predef_path) elif obj_type == 'ChartOfCalculationTypes' and defn.get('predefined'): ensure_ext_dir() predef_xml = build_predefined_calc_type_xml(defn['predefined']) predef_path = os.path.join(ext_dir, 'Predefined.xml') - write_utf8_bom(predef_path, predef_xml) + write_xml_file_keep_eol(predef_path, predef_xml) modules_created.append(predef_path) elif obj_type in predef_root_by_type and defn.get('predefined'): ensure_ext_dir() cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String' predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type) predef_path = os.path.join(ext_dir, 'Predefined.xml') - write_utf8_bom(predef_path, predef_xml) + write_xml_file_keep_eol(predef_path, predef_xml) modules_created.append(predef_path) # Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика. diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index 17b3dd02..377e05b2 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,4 +1,4 @@ -# meta-edit v1.25 — Edit existing 1C metadata object XML +# meta-edit v1.26 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -3138,7 +3138,8 @@ function Add-PredefinedItems($items) { $hdr = "`r`n`r`n" $text = "$hdr$itemsXml`r`n" } - [System.IO.File]::WriteAllText($path, $text, $utf8Bom) + # Создаваемый файл — по канону: без перевода строки в конце. + [System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $utf8Bom) $n = @($items).Count Info "Added $n predefined item(s) → $path" $script:addCount += $n diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index 3e6ddeb5..1c693702 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-edit v1.25 — Edit existing 1C metadata object XML +# meta-edit v1.26 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index b8778bfa..4bf0aacf 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -1,4 +1,4 @@ -# mxl-compile v1.6 — Compile 1C spreadsheet from JSON +# mxl-compile v1.7 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 7bbec7d3..518047c5 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-compile v1.6 — Compile 1C spreadsheet from JSON +# mxl-compile v1.7 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -802,7 +802,7 @@ def main(): if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) - content = '\n'.join(lines) + content = '\r\n'.join(lines) write_utf8_bom(out_path, content) # --- 9. Summary --- diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1 index 98bbcf3b..c9fb0006 100644 --- a/.claude/skills/role-compile/scripts/role-compile.ps1 +++ b/.claude/skills/role-compile/scripts/role-compile.ps1 @@ -1,4 +1,4 @@ -# role-compile v1.12 — Compile 1C role from JSON +# role-compile v1.14 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py index 416641ac..9f8235c6 100644 --- a/.claude/skills/role-compile/scripts/role-compile.py +++ b/.claude/skills/role-compile/scripts/role-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-compile v1.12 — Compile 1C role from JSON +# role-compile v1.14 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -203,6 +203,12 @@ def detect_format_version(d): return "2.17" +def detect_eol(text): + # Перевод строки ВСТАВКИ берём из самого файла: канон CRLF относится к файлам, + # которые мы создаём, а правка существующего сохраняет его стиль (#44/#46/#47). + crlf = text.count('\r\n') + return '\r\n' if crlf and crlf >= text.count('\n') - crlf else '\n' + def esc_xml(s): """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует (92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно.""" @@ -708,7 +714,7 @@ def main(): lines.append(' ') lines.append('') - metadata_xml = '\n'.join(lines) + metadata_xml = '\r\n'.join(lines) # --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) --- lines = [] @@ -756,7 +762,7 @@ def main(): lines.append('') - rights_xml = '\n'.join(lines) + rights_xml = '\r\n'.join(lines) # --- 6. Write output files --- out_dir = args.OutputDir @@ -791,9 +797,13 @@ def main(): reg_result = None if os.path.exists(config_xml_path): - with open(config_xml_path, 'r', encoding='utf-8-sig') as f: + # newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется + # в LF при чтении и файл будет переписан в LF (#44/#46/#47). + with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f: raw_text = f.read() + eol = detect_eol(raw_text) + # Check if already registered if f'{role_name}' in raw_text: reg_result = 'already' @@ -807,10 +817,10 @@ def main(): # Insert after last existing last_match = matches[-1] insert_pos = last_match.end() - raw_text = raw_text[:insert_pos] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:] + raw_text = raw_text[:insert_pos] + eol + f'\t\t\t{new_role_tag}' + raw_text[insert_pos:] else: # No existing roles — insert before
- raw_text = raw_text.replace('
', f'\t\t\t{new_role_tag}\n\t\t') + raw_text = raw_text.replace('', f'\t\t\t{new_role_tag}' + eol + '\t\t') write_utf8_bom(config_xml_path, raw_text) reg_result = 'added' diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index fe2f5f61..7cbd1581 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.110 — Compile 1C DCS from JSON +# skd-compile v1.111 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 07fde398..2945f1db 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.110 — Compile 1C DCS from JSON +# skd-compile v1.111 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -3069,7 +3069,7 @@ def main(): if parent_dir and not os.path.exists(parent_dir): os.makedirs(parent_dir, exist_ok=True) - content = '\n'.join(lines) + content = '\r\n'.join(lines) write_utf8_bom(output_path, content) # --- 5. Statistics --- diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 index cbc29bc4..3f2d0285 100644 --- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 +++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 @@ -1,4 +1,4 @@ -# subsystem-compile v1.13 — Create 1C subsystem from JSON definition +# subsystem-compile v1.15 — Create 1C subsystem from JSON definition # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py index 49b4da79..e1c6ddef 100644 --- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py +++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# subsystem-compile v1.13 — Create 1C subsystem from JSON definition +# subsystem-compile v1.15 — Create 1C subsystem from JSON definition # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -204,6 +204,12 @@ def detect_format_version(d): return "2.17" +def detect_eol(text): + # Перевод строки ВСТАВКИ берём из самого файла: канон CRLF относится к файлам, + # которые мы создаём, а правка существующего сохраняет его стиль (#44/#46/#47). + crlf = text.count('\r\n') + return '\r\n' if crlf and crlf >= text.count('\n') - crlf else '\n' + def esc_xml(s): """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует (92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно.""" @@ -279,7 +285,7 @@ def write_child_subsystem_stub(child_path, child_name, format_version): lines.append('\t\t') lines.append('\t') lines.append('') - write_utf8_bom(child_path, '\n'.join(lines)) + write_utf8_bom(child_path, '\r\n'.join(lines)) def main(): @@ -525,7 +531,7 @@ def main(): target_xml = os.path.join(subs_dir, f'{obj_name}.xml') # Write XML - xml_content = '\n'.join(lines) + xml_content = '\r\n'.join(lines) write_utf8_bom(target_xml, xml_content) print(f"[OK] Created: {target_xml}") @@ -555,9 +561,12 @@ def main(): parent_xml_path = config_xml if parent_xml_path and os.path.exists(parent_xml_path): - with open(parent_xml_path, 'r', encoding='utf-8-sig') as f: + # newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется + # в LF при чтении и файл будет переписан в LF (#44/#46/#47). + with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f: raw_text = f.read() + eol = detect_eol(raw_text) doc = ET.ElementTree(ET.fromstring(raw_text)) root = doc.getroot() md_ns = 'http://v8.1c.ru/8.3/MDClasses' @@ -593,10 +602,10 @@ def main(): if not already_exists: # Use raw text manipulation to preserve formatting if '' in raw_text: - replacement = f'\n\t\t\t{esc_xml(obj_name)}\n\t\t' + replacement = ('' + eol + f'\t\t\t{esc_xml(obj_name)}' + eol + '\t\t') raw_text = raw_text.replace('', replacement, 1) elif '' in raw_text: - insert_line = f'\t\t\t{esc_xml(obj_name)}\n' + insert_line = f'\t\t\t{esc_xml(obj_name)}' + eol raw_text = raw_text.replace('', insert_line + '\t\t', 1) write_utf8_bom(parent_xml_path, raw_text) diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 index 8b6d998c..b2d4e770 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 @@ -1,4 +1,4 @@ -# subsystem-edit v1.10 — Edit existing 1C subsystem XML +# subsystem-edit v1.11 — Edit existing 1C subsystem XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py index ae48257c..7bd0da1f 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# subsystem-edit v1.10 — Edit existing 1C subsystem XML +# subsystem-edit v1.11 — Edit existing 1C subsystem XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -242,7 +242,7 @@ def write_child_subsystem_stub(child_path, child_name, format_version): lines.append('\t\t') lines.append('\t') lines.append('') - write_utf8_bom(child_path, '\n'.join(lines)) + write_utf8_bom(child_path, '\r\n'.join(lines)) MD_NS = "http://v8.1c.ru/8.3/MDClasses" XR_NS = "http://v8.1c.ru/8.3/xcf/readable" diff --git a/.claude/skills/template-add/scripts/add-template.ps1 b/.claude/skills/template-add/scripts/add-template.ps1 index 8f55d910..bf21d82e 100644 --- a/.claude/skills/template-add/scripts/add-template.ps1 +++ b/.claude/skills/template-add/scripts/add-template.ps1 @@ -1,4 +1,4 @@ -# template-add v1.12 — Add template to 1C object +# template-add v1.14 — Add template to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -260,7 +260,16 @@ $templateMetaXml = @" "@ -[System.IO.File]::WriteAllText($templateMetaPath, $templateMetaXml, $encBom) +# Канон выгрузки Конфигуратора: CRLF в разделителях строк, без перевода в конце +# файла. Скелет собирается here-string'ами, а .ps1 хранится с LF — отсюда LF и +# смешанный EOL. Нормализация здесь безопасна: многострочных текстовых узлов в +# скелете нет. Модули .bsl и текстовые макеты через эту функцию НЕ пишутся. +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) +} + +Write-XmlFile $templateMetaPath $templateMetaXml $encBom # --- 2. Содержимое макета (Templates//Ext/Template.) --- @@ -290,7 +299,7 @@ switch ($TemplateType) { "@ - [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) + Write-XmlFile $templateFilePath $content $encBom } "BinaryData" { [System.IO.File]::WriteAllBytes($templateFilePath, @()) @@ -312,7 +321,7 @@ switch ($TemplateType) {
"@ - [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) + Write-XmlFile $templateFilePath $content $encBom } } diff --git a/.claude/skills/template-add/scripts/add-template.py b/.claude/skills/template-add/scripts/add-template.py index 7527a308..4b2aee18 100644 --- a/.claude/skills/template-add/scripts/add-template.py +++ b/.claude/skills/template-add/scripts/add-template.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# add-template v1.12 — Add template to 1C object +# add-template v1.14 — Add template to 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 b/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 index cfd7818a..2475cec6 100644 --- a/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 +++ b/.claude/skills/xdto-compile/scripts/xdto-compile.ps1 @@ -1,4 +1,4 @@ -# xdto-compile v1.3 — Build a 1C XDTO package from an XML Schema (XSD) +# xdto-compile v1.4 — Build a 1C XDTO package from an XML Schema (XSD) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true, ParameterSetName='File')] diff --git a/.claude/skills/xdto-compile/scripts/xdto-compile.py b/.claude/skills/xdto-compile/scripts/xdto-compile.py index 9db79ef4..ad4286c1 100644 --- a/.claude/skills/xdto-compile/scripts/xdto-compile.py +++ b/.claude/skills/xdto-compile/scripts/xdto-compile.py @@ -1,4 +1,4 @@ -# xdto-compile v1.3 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) +# xdto-compile v1.4 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -960,6 +960,12 @@ if os.path.exists(config_xml): else: new_elem.tail = child_objects.text data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8") + # Парсер XML по спецификации схлопывает CRLF в LF, поэтому tostring отдаёт + # LF-документ. Возвращаем EOL исходного файла: правка существующего файла + # сохраняет его стиль (#44/#46/#47), а .NET-порт делает это через + # NewLineHandling — иначе порты расходятся побайтово. + src_eol = b"\r\n" if b"\r\n" in raw else b"\n" + data = data.replace(b"\r\n", b"\n").replace(b"\n", src_eol) if had_bom: data = b"\xef\xbb\xbf" + data with open(config_xml, "wb") as f: diff --git a/.claude/skills/xdto-edit/scripts/xdto-edit.ps1 b/.claude/skills/xdto-edit/scripts/xdto-edit.ps1 index 75845d8b..4537ec21 100644 --- a/.claude/skills/xdto-edit/scripts/xdto-edit.ps1 +++ b/.claude/skills/xdto-edit/scripts/xdto-edit.ps1 @@ -1,4 +1,4 @@ -# xdto-edit v1.1 — Point edits of a 1C XDTO package +# xdto-edit v1.2 — Point edits of a 1C XDTO package # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true)] diff --git a/.claude/skills/xdto-edit/scripts/xdto-edit.py b/.claude/skills/xdto-edit/scripts/xdto-edit.py index f29ad78e..58bd010f 100644 --- a/.claude/skills/xdto-edit/scripts/xdto-edit.py +++ b/.claude/skills/xdto-edit/scripts/xdto-edit.py @@ -1,4 +1,4 @@ -# xdto-edit v1.1 — Point edits of a 1C XDTO package (Python port) +# xdto-edit v1.2 — Point edits of a 1C XDTO package (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -194,6 +194,17 @@ def invoke_sibling(script, argv, what): def save_xml(doc, path): raw = etree.tostring(doc, xml_declaration=True, encoding="UTF-8") + # Парсер XML по спецификации схлопывает CRLF в LF, поэтому tostring отдаёт + # LF-документ. Возвращаем EOL исходного файла: правка существующего файла + # сохраняет его стиль (#44/#46/#47), а .NET-порт делает это через + # NewLineHandling — иначе порты расходятся побайтово. + src_eol = b"\r\n" + try: + with open(path, "rb") as f: + src_eol = b"\r\n" if b"\r\n" in f.read() else b"\n" + except OSError: + pass + raw = raw.replace(b"\r\n", b"\n").replace(b"\n", src_eol) with open(path, "wb") as f: f.write(b"\xef\xbb\xbf" + raw) diff --git a/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml index a171fc75..19ee218b 100644 --- a/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml +++ b/tests/skills/cases/meta-edit/snapshots/add-predefined/Catalogs/Спр/Ext/Predefined.xml @@ -26,4 +26,4 @@ - + \ No newline at end of file