fix(skd-edit): не срезать значимое локальное объявление xmlns при пересборке поля

При пересборке поля навык сохранял <valueType> через OuterXml/tostring и срезал
из фрагмента ВСЕ объявления namespace. Для префиксов, объявленных в корне схемы,
это верно — DOM переобъявляет их на фрагменте избыточно. Но объявление, которого
в корне нет, значимо: xmlns:d5p1 живёт только локально на <v8:Type> и связывает
префикс, которым квалифицировано значение узла (d5p1:CatalogRef.X). Срез оставлял
висячий префикс — XML остаётся well-formed (префикс в тексте парсер не проверяет),
а 1С отвергает тип. Радиус — modify-field.

Слепой срез заменён на Strip-InheritedXmlns / strip_inherited_xmlns: объявление
выбрасывается, только если корень объявляет тот же префикс с тем же URI. Карта
корня снимается с уже имевшегося RawRootOpening; разделитель \s+, поскольку
корневой тег бывает разложен по строкам. Префикс и URI сравниваются ординально —
в PS -eq и @{} регистронезависимы. Подставлено во все пять точек среза каждого
порта; для корневых префиксов поведение не меняется.

Кейс modify-field-ref-type: modify-field по ref-полю не был покрыт. Проверено,
что кейс краснеет до правки — и по снэпшоту, и по skd-validate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-19 14:27:03 +03:00
co-authored by Claude Opus 5
parent b6857c9406
commit 9475aa6171
4 changed files with 147 additions and 12 deletions
+36 -6
View File
@@ -1,4 +1,4 @@
# skd-edit v1.37 — Atomic 1C DCS editor (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# skd-edit v1.38 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: парный .py собирает выражения автодат вне f-string ради совместимости с python 3.9 (PEP 701).
param(
@@ -335,6 +335,36 @@ function Parse-FieldShorthand {
return $result
}
# DOM отдаёт фрагмент через OuterXml как самостоятельный документ и переобъявляет на нём каждый
# префикс, который был в области видимости. Такие объявления избыточны: родительский контекст в точке
# вставки их уже даёт. Но объявление, которого в корне НЕТ, значимо — например xmlns:d5p1 на <v8:Type>:
# только оно связывает префикс, которым квалифицировано значение узла (d5p1:CatalogRef.X). Срезать его
# нельзя, иначе отказ будет тихим: XML останется well-formed (префикс в тексте парсер не проверяет), а
# 1С отвергнет тип. Поэтому выбрасываем лишь унаследованное — префикс, объявленный в корне с тем же URI.
function Strip-InheritedXmlns([string]$raw) {
if ([string]::IsNullOrEmpty($raw)) { return $raw }
# Корень не распознан — сохраняем прежнее поведение, чтобы не менять статус-кво.
if (-not $script:RawRootOpening) {
return [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
}
# Dictionary, а не @{}: хэш в PS сравнивает ключи регистронезависимо, а префикс XML регистрозависим.
$rootNs = New-Object 'System.Collections.Generic.Dictionary[string,string]'
# В корне атрибуты бывают разложены по строкам — разделитель \s+, а не пробел.
foreach ($m in [regex]::Matches($script:RawRootOpening, '\s+xmlns(?::(\w+))?="([^"]*)"')) {
$rootNs[$m.Groups[1].Value] = $m.Groups[2].Value
}
return [regex]::Replace($raw, ' xmlns(?::(\w+))?="([^"]*)"', {
param($m)
$prefix = $m.Groups[1].Value
$uri = $m.Groups[2].Value
# URI регистрозависим: -eq в PS этого не даёт, сравниваем ординально.
if ($rootNs.ContainsKey($prefix) -and [string]::Equals($rootNs[$prefix], $uri, [System.StringComparison]::Ordinal)) {
return ''
}
return $m.Value
})
}
function Read-FieldProperties($fieldEl) {
$props = @{
dataPath = ""; field = ""; title = ""; type = ""
@@ -353,7 +383,7 @@ function Read-FieldProperties($fieldEl) {
# siblings when shorthand overrides only the ru content. Strip xmlns
# redeclarations that OuterXml adds for sub-elements.
$raw = $ch.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$raw = Strip-InheritedXmlns $raw
$props._rawTitle = $raw
# Also extract ru content as plain string (backward compat — used by
# external consumers reading $existing.title).
@@ -376,7 +406,7 @@ function Read-FieldProperties($fieldEl) {
# .NET OuterXml re-declares xmlns on every element where the prefix is in
# scope (because the fragment is treated as standalone). Strip these since
# the parent context at insertion point already provides them.
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$raw = Strip-InheritedXmlns $raw
$props["_rawValueType"] = $raw
$typeEl = $null
foreach ($gc in $ch.ChildNodes) {
@@ -412,7 +442,7 @@ function Read-FieldProperties($fieldEl) {
# Defense in depth: preserve OuterXml of unknown children so rebuild
# doesn't silently drop them (custom <editFormat>, <appearance>, etc.).
$raw = $ch.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$raw = Strip-InheritedXmlns $raw
$props._unknownChildren += $raw
}
}
@@ -2455,7 +2485,7 @@ switch ($Operation) {
$titleFrag = $null
if ($existingTitle) {
$rawTitle = $existingTitle.OuterXml
$rawTitle = [regex]::Replace($rawTitle, ' xmlns(?::\w+)?="[^"]*"', '')
$rawTitle = Strip-InheritedXmlns $rawTitle
# Count <v8:item> occurrences — if >1, treat as multi-lang.
$itemCount = ([regex]::Matches($rawTitle, '<v8:item>')).Count
if ($itemCount -gt 1) {
@@ -3730,7 +3760,7 @@ switch ($Operation) {
# what the user explicitly set.
if ($kv.Contains($gc.LocalName)) { continue }
$raw = $gc.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$raw = Strip-InheritedXmlns $raw
$preservedRoleChildren += $raw
}
Remove-NodeWithWhitespace $oldRole
+30 -6
View File
@@ -1,4 +1,4 @@
# skd-edit v1.37 — Atomic 1C DCS editor (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# skd-edit v1.38 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -435,6 +435,30 @@ def parse_field_shorthand(s):
return result
# lxml отдаёт фрагмент через tostring как самостоятельный документ и переобъявляет на нём каждый
# префикс, который был в области видимости. Такие объявления избыточны: родительский контекст в точке
# вставки их уже даёт. Но объявление, которого в корне НЕТ, значимо — например xmlns:d5p1 на <v8:Type>:
# только оно связывает префикс, которым квалифицировано значение узла (d5p1:CatalogRef.X). Срезать его
# нельзя, иначе отказ будет тихим: XML останется well-formed (префикс в тексте парсер не проверяет), а
# 1С отвергнет тип. Поэтому выбрасываем лишь унаследованное — префикс, объявленный в корне с тем же URI.
def strip_inherited_xmlns(raw):
if not raw:
return raw
# Корень не распознан — сохраняем прежнее поведение, чтобы не менять статус-кво.
if not raw_root_opening:
return re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
root_ns = {}
# В корне атрибуты бывают разложены по строкам — разделитель \s+, а не пробел.
for m in re.finditer(r'\s+xmlns(?::(\w+))?="([^"]*)"', raw_root_opening):
root_ns[m.group(1) or ""] = m.group(2)
def _drop_inherited(m):
prefix = m.group(1) or ""
return "" if root_ns.get(prefix) == m.group(2) else m.group(0)
return re.sub(r' xmlns(?::(\w+))?="([^"]*)"', _drop_inherited, raw)
def read_field_properties(field_el):
props = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": [], "_rawTypeText": "",
"_rawTitle": None, "_unknownChildren": []}
@@ -450,7 +474,7 @@ def read_field_properties(field_el):
elif ln == "title":
# Preserve full multi-lang title OuterXml; also extract ru content for compat.
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
raw = strip_inherited_xmlns(raw)
props["_rawTitle"] = raw
for item in ch:
if isinstance(item.tag, str) and local_name(item) == "item":
@@ -469,7 +493,7 @@ def read_field_properties(field_el):
# expressible via shorthand. Strip xmlns declarations that lxml re-emits when
# serializing a sub-element (parent context already provides them).
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
raw = strip_inherited_xmlns(raw)
props["_rawValueType"] = raw
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "Type":
@@ -494,7 +518,7 @@ def read_field_properties(field_el):
# Defense in depth: preserve OuterXml of unknown children so rebuild
# doesn't silently drop them (custom <editFormat>, <appearance>, etc.).
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
raw = strip_inherited_xmlns(raw)
props["_unknownChildren"].append(raw)
return props
@@ -2238,7 +2262,7 @@ elif operation == "modify-parameter":
title_frag = None
if existing_title is not None:
raw_title = etree.tostring(existing_title, encoding="unicode", with_tail=False)
raw_title = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw_title)
raw_title = strip_inherited_xmlns(raw_title)
if raw_title.count("<v8:item>") > 1:
title_frag = child_indent + patch_mltext_ru(raw_title, title_val, child_indent)
remove_node_with_whitespace(existing_title)
@@ -3201,7 +3225,7 @@ elif operation == "set-field-role":
if ln in kv_keys:
continue
raw = etree.tostring(gc, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
raw = strip_inherited_xmlns(raw)
preserved_role_children.append(raw)
remove_node_with_whitespace(old_role)
@@ -0,0 +1,22 @@
{
"name": "modify-field: локальное объявление xmlns у ссылочного типа сохраняется",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Сотрудник, Т.Сумма ИЗ Регистр КАК Т",
"fields": ["Сотрудник: CatalogRef.Сотрудники", "Сумма: decimal(15,2)"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "modify-field",
"value": "Сотрудник [Работник]"
},
"idempotent": true
}
@@ -0,0 +1,59 @@
<?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>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Сотрудник</dataPath>
<field>Сотрудник</field>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Работник</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.Сотрудники</v8:Type>
</valueType>
</field>
<field xsi:type="DataSetFieldField">
<dataPath>Сумма</dataPath>
<field>Сумма</field>
<valueType>
<v8:Type>xs:decimal</v8:Type>
<v8:NumberQualifiers>
<v8:Digits>15</v8:Digits>
<v8:FractionDigits>2</v8:FractionDigits>
<v8:AllowedSign>Any</v8:AllowedSign>
</v8:NumberQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.Сотрудник, Т.Сумма ИЗ Регистр КАК Т</query>
</dataSet>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>