mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-21 02:29:42 +03:00
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:
co-authored by
Claude Opus 5
parent
b6857c9406
commit
9475aa6171
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user